mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-27 14:44:07 +00:00
* feat: add OAuth 1.0 authentication support Add full OAuth 1.0 (RFC 5849) authentication with support for HMAC-SHA1/256/512, RSA-SHA1/256/512, and PLAINTEXT signature methods. Includes UI components, bru/yml serialization, Postman import, code generation, CLI support, and comprehensive playwright and unit tests. * test: replace real-looking PEM literals with fake markers in oauth1 tests Avoid tripping secret scanners by using obviously fake BEGIN/END markers and non-sensitive base64 content in serialization and round-trip tests. * fix: remove invalid OAuth1 placeholder header from code generator OAuth1 requires runtime-computed nonce, timestamp, and signature that cannot be pre-computed for a static code snippet. Return an empty array instead of emitting an Authorization header with literal <signature>, <timestamp>, <nonce> placeholders. * fix: remove unreachable oauth1 case from WSAuth component The oauth1 switch branch was dead code since it was not in supportedAuthModes and the useEffect would reset it to 'none' before it could render. * fix: remove unused collectionPath param and use path.basename for filename extraction * refactor: rename OAuth1 fields for clarity - tokenSecret → accessTokenSecret - signatureMethod → signatureEncoding - addParamsTo value 'queryparams' → 'query' * refactor: rename addParamsTo to placement in OAuth1 auth * fix: add missing oauth1: null in buildOAuth2Config and upgrade @opencollection/types to 0.9.0 * test: add oauth1 import tests and fix missing oauth1: null in auth assertions * ci: add auth playwright tests workflow for Linux, macOS, and Windows * refactor: rename signatureEncoding to signatureMethod and fix timeline race condition - Rename OAuth1 signatureEncoding to signatureMethod across all packages - Fix timeline showing "No Headers/Body found" when request-sent IPC event arrives after response by retroactively updating the timeline entry - Store requestUid in timeline entries for precise matching - Correct timeline entry timestamp on retroactive update for proper sort order * ci: add OAuth1 CLI tests and reorganize auth actions under oauth1/ - Add CLI tests that run full BRU and YML collections via bru run - Add start-test-server actions for Linux, macOS, and Windows - Move auth e2e and setup actions under auth/oauth1/ directory - Fix Windows Playwright failures caused by unescaped backslashes in collectionPath template variable * ci: reorder auth tests to run E2E tests before CLI tests * ci: start test server after E2E tests to fix port 8081 conflict
285 lines
6.7 KiB
JavaScript
285 lines
6.7 KiB
JavaScript
class BrunoRequest {
|
|
/**
|
|
* The following properties are available as shorthand:
|
|
* - req.url
|
|
* - req.method
|
|
* - req.headers
|
|
* - req.timeout
|
|
* - req.body
|
|
*
|
|
* Above shorthands are useful for accessing the request properties directly in the scripts
|
|
* It must be noted that the user cannot set these properties directly.
|
|
* They should use the respective setter methods to set these properties.
|
|
*/
|
|
constructor(req) {
|
|
this.req = req;
|
|
this.url = req.url;
|
|
this.method = req.method;
|
|
this.headers = req.headers;
|
|
this.timeout = req.timeout;
|
|
this.name = req.name;
|
|
this.pathParams = req.pathParams;
|
|
this.tags = req.tags || [];
|
|
/**
|
|
* We automatically parse the JSON body if the content type is JSON
|
|
* This is to make it easier for the user to access the body directly
|
|
*
|
|
* It must be noted that the request data is always a string and is what gets sent over the network
|
|
* If the user wants to access the raw data, they can use getBody({raw: true}) method
|
|
*/
|
|
const isJson = this.hasJSONContentType(this.req.headers);
|
|
if (isJson) {
|
|
this.body = this.__safeParseJSON(req.data);
|
|
}
|
|
}
|
|
|
|
getUrl() {
|
|
return this.req.url;
|
|
}
|
|
|
|
setUrl(url) {
|
|
this.url = url;
|
|
this.req.url = url;
|
|
}
|
|
|
|
getHost() {
|
|
try {
|
|
const url = new URL(this.req.url);
|
|
return url.host;
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
getPath() {
|
|
try {
|
|
const url = new URL(this.req.url);
|
|
let pathname = url.pathname;
|
|
|
|
// If path params exist, interpolate them into the pathname
|
|
if (this.req.pathParams && Array.isArray(this.req.pathParams)) {
|
|
pathname = pathname
|
|
.split('/')
|
|
.map((segment) => {
|
|
if (segment.startsWith(':')) {
|
|
const paramName = segment.slice(1);
|
|
const pathParam = this.req.pathParams.find((param) => param.name === paramName);
|
|
if (pathParam && pathParam.value) {
|
|
return pathParam.value;
|
|
}
|
|
}
|
|
return segment;
|
|
})
|
|
.join('/');
|
|
}
|
|
|
|
return pathname;
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
getQueryString() {
|
|
try {
|
|
const url = new URL(this.req.url);
|
|
// Return query string without the leading '?'
|
|
return url.search ? url.search.substring(1) : '';
|
|
} catch (e) {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
getMethod() {
|
|
return this.req.method;
|
|
}
|
|
|
|
getAuthMode() {
|
|
if (this.req?.oauth2) {
|
|
return 'oauth2';
|
|
} else if (this.req?.oauth1config) {
|
|
return 'oauth1';
|
|
} else if (this.headers?.['Authorization']?.startsWith('Bearer')) {
|
|
return 'bearer';
|
|
} else if (this.headers?.['Authorization']?.startsWith('Basic') || this.req?.auth?.username) {
|
|
return 'basic';
|
|
} else if (this.req?.awsv4) {
|
|
return 'awsv4';
|
|
} else if (this.req?.digestConfig) {
|
|
return 'digest';
|
|
} else if (this.headers?.['X-WSSE'] || this.req?.auth?.username) {
|
|
return 'wsse';
|
|
} else {
|
|
return 'none';
|
|
}
|
|
}
|
|
|
|
setMethod(method) {
|
|
this.method = method;
|
|
this.req.method = method;
|
|
}
|
|
|
|
getHeaders() {
|
|
return this.req.headers;
|
|
}
|
|
|
|
setHeaders(headers) {
|
|
this.headers = headers;
|
|
this.req.headers = headers;
|
|
}
|
|
|
|
deleteHeaders(headers) {
|
|
headers.forEach((name) => this.deleteHeader(name));
|
|
}
|
|
|
|
getHeader(name) {
|
|
return this.req.headers[name];
|
|
}
|
|
|
|
setHeader(name, value) {
|
|
this.headers[name] = value;
|
|
this.req.headers[name] = value;
|
|
}
|
|
|
|
deleteHeader(name) {
|
|
delete this.headers[name];
|
|
delete this.req.headers[name];
|
|
|
|
/**
|
|
Store header name to be applied in the axios request interceptor.
|
|
Default headers (user-agent, accept, accept-encoding, etc.) are added after
|
|
the pre-request script runs, so we track them here and delete them later.
|
|
*/
|
|
if (!this.req.__headersToDelete) {
|
|
this.req.__headersToDelete = [];
|
|
}
|
|
if (!this.req.__headersToDelete.includes(name)) {
|
|
this.req.__headersToDelete.push(name);
|
|
}
|
|
}
|
|
|
|
hasJSONContentType(headers) {
|
|
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
|
return contentType.includes('json');
|
|
}
|
|
|
|
/**
|
|
* Get the body of the request
|
|
*
|
|
* We automatically parse and return the JSON body if the content type is JSON
|
|
* If the user wants the raw body, they can pass the raw option as true
|
|
*/
|
|
getBody(options = {}) {
|
|
if (options.raw) {
|
|
return this.req.data;
|
|
}
|
|
|
|
const isJson = this.hasJSONContentType(this.req.headers);
|
|
if (isJson) {
|
|
return this.__safeParseJSON(this.req.data);
|
|
}
|
|
|
|
return this.req.data;
|
|
}
|
|
|
|
/**
|
|
* If the content type is JSON and if the data is an object
|
|
* - We set the body property as the object itself
|
|
* - We set the request data as the stringified JSON as it is what gets sent over the network
|
|
* Otherwise
|
|
* - We set the request data as the data itself
|
|
* - We set the body property as the data itself
|
|
*
|
|
* If the user wants to override this behavior, they can pass the raw option as true
|
|
*/
|
|
setBody(data, options = {}) {
|
|
if (options.raw) {
|
|
this.req.data = data;
|
|
this.body = data;
|
|
return;
|
|
}
|
|
|
|
const isJson = this.hasJSONContentType(this.req.headers);
|
|
if (isJson && this.__isObject(data)) {
|
|
this.body = data;
|
|
this.req.data = this.__safeStringifyJSON(data);
|
|
return;
|
|
}
|
|
|
|
this.req.data = data;
|
|
this.body = data;
|
|
}
|
|
|
|
setMaxRedirects(maxRedirects) {
|
|
this.req.maxRedirects = maxRedirects;
|
|
}
|
|
|
|
getTimeout() {
|
|
return this.req.timeout;
|
|
}
|
|
|
|
setTimeout(timeout) {
|
|
this.timeout = timeout;
|
|
this.req.timeout = timeout;
|
|
}
|
|
|
|
onFail(callback) {
|
|
if (typeof callback === 'function') {
|
|
this.req.onFailHandler = callback;
|
|
} else if (callback) {
|
|
throw new Error(`${callback} is not a function`);
|
|
}
|
|
}
|
|
|
|
__safeParseJSON(str) {
|
|
try {
|
|
return JSON.parse(str);
|
|
} catch (e) {
|
|
return str;
|
|
}
|
|
}
|
|
|
|
__safeStringifyJSON(obj) {
|
|
try {
|
|
return JSON.stringify(obj);
|
|
} catch (e) {
|
|
return obj;
|
|
}
|
|
}
|
|
|
|
__isObject(obj) {
|
|
return obj !== null && typeof obj === 'object';
|
|
}
|
|
|
|
disableParsingResponseJson() {
|
|
this.req.__brunoDisableParsingResponseJson = true;
|
|
}
|
|
|
|
getExecutionMode() {
|
|
return this.req.__bruno__executionMode;
|
|
}
|
|
|
|
getName() {
|
|
return this.req.name;
|
|
}
|
|
|
|
getPathParams() {
|
|
const params = Array.isArray(this.req.pathParams) ? this.req.pathParams : [];
|
|
|
|
return params.map((param) => ({
|
|
name: param.name,
|
|
value: param.value,
|
|
type: param.type
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get the tags associated with this request
|
|
* @returns {Array<string>} Array of tag strings
|
|
*/
|
|
getTags() {
|
|
return this.req.tags || [];
|
|
}
|
|
}
|
|
|
|
module.exports = BrunoRequest;
|