diff --git a/package-lock.json b/package-lock.json index 52ba590e5c..9559363c66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10938,6 +10938,14 @@ "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", "dev": true }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "engines": { + "node": ">=14.16" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -13568,6 +13576,17 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -21088,6 +21107,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-8.1.0.tgz", + "integrity": "sha512-BFQeWxJOZxZGix7y+SByG3F36dA0AbTy9o6pSmKFcFz7DAj0re9Frkty3saBn3nHo3D0oZJ/+rx3r8H8r8Jbpw==", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -22558,6 +22593,17 @@ "spdx-ranges": "^2.0.0" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -24955,6 +25001,7 @@ "oauth-1.0a": "^2.2.6", "papaparse": "^5.4.1", "prettier": "2.4.1", + "query-string": "^8.1.0", "shell-quote": "^1.8.1", "swagger-ui-dist": "5.0.0-alpha.6", "tough-cookie": "^4.1.3", diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 4cf0d9327b..35af0a501a 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -81,6 +81,7 @@ "oauth-1.0a": "^2.2.6", "papaparse": "^5.4.1", "prettier": "2.4.1", + "query-string": "^8.1.0", "shell-quote": "^1.8.1", "swagger-ui-dist": "5.0.0-alpha.6", "tough-cookie": "^4.1.3", diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-auth.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-auth.ts index e4574fc94d..9ac6284470 100644 --- a/packages/insomnia/src/renderers/hidden-browser-window/object-auth.ts +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-auth.ts @@ -23,7 +23,152 @@ export interface AuthOption { value: string; } -export interface RawAuthOptions { +// export interface AuthBasic { +// password: string; +// username: string; +// id: string; +// } + +// export interface AuthBearer { +// token: string; +// id: string; +// } + +// export interface AuthJWT { +// secret: string; +// algorithm: string; +// isSecretBase64Encoded: boolean; +// payload: string; // e.g. "{}" +// addTokenTo: string; +// headerPrefix: string; +// queryParamKey: string; +// header: string; // e.g. "{}" +// id: string; +// } + +// export interface AuthDigest { +// opaque: string; +// clientNonce: string; +// nonceCount: string; +// qop: string; +// nonce: string; +// realm: string; +// password: string; +// username: string; +// algorithm: string; +// id: string; +// } + +// export interface AuthOAuth1 { +// addEmptyParamsToSign: boolean; +// includeBodyHash: boolean; +// realm: string; +// nonce: string; +// timestamp: string; +// verifier: string; +// callback: string; +// tokenSecret: string; +// token: string; +// consumerSecret: string; +// consumerKey: string; +// signatureMethod: string; // "HMAC-SHA1" +// version: string; +// addParamsToHeader: string; +// id: string; +// } + +// export interface OAuth2Param { +// key: string; +// value: string; +// enabled: boolean; +// send_as: string; // it follows exising naming +// } + +// export interface AuthOAuth2 { +// accessToken: string; +// refreshRequestParams: OAuth2Param[]; +// tokenRequestParams: OAuth2Param[]; +// authRequestParams: OAuth2Param[]; +// refreshTokenUrl: string; +// state: string; +// scope: string; +// clientSecret: string; +// clientId: string; +// tokenName: string; +// addTokenTo: string; +// id: string; +// } + +// export interface AuthHAWK { +// includePayloadHash: boolean; +// timestamp: string; +// delegation: string; +// app: string; +// extraData: string; +// nonce: string; +// user: string; +// authKey: string; +// authId: string; +// algorithm: string; +// id: string; +// } + +// export interface AuthAWSV4 { +// sessionToken: string; +// service: string; +// region: string; +// secretKey: string; +// accessKey: string; +// id: string; +// } + +// export interface AuthNTLM { +// workstation: string; +// domain: string; +// password: string; +// username: string; +// id: string; +// } + +// export interface AuthAPIKey { +// key: string; +// value: string; +// id: string; +// } + +// export interface AuthEdgegrid { +// headersToSign: string; +// baseURL: string; +// timestamp: string; +// nonce: string; +// clientSecret: string; +// clientToken: string; +// accessToken: string; +// id: string; +// } + +// export interface AuthASAP { +// exp: string; // expiry +// claims: string; // e.g., { "additional claim": "claim value" } +// sub: string; // subject +// privateKey: string; // private key +// kid: string; // key id +// aud: string; // audience +// iss: string; // issuer +// alg: string; // e.g., RS256 +// id: string; +// } + +// function AuthMethodToParams(authMethod: AuthNoAuth | AuthBasic | AuthBasic | AuthBearer | AuthJWT | AuthDigest | AuthOAuth1 | AuthOAuth2 | AuthHAWK | AuthAWSV4 | AuthNTLM | AuthAPIKey | AuthEdgegrid | AuthASAP) { +// return Object.entries(authMethod). +// map(entry => ({ +// type: 'any', +// key: entry[0], +// value: entry[1], +// })); +// } + +export interface AuthOptions { type: string; basic?: AuthOption[]; bearer?: AuthOption[]; @@ -43,7 +188,7 @@ function rawOptionsToVariables(options: VariableList | Variable[] | ob if (VariableList.isVariableList(options)) { return [options as VariableList]; } else if ('type' in options) { // object - const optsObj = options as RawAuthOptions; + const optsObj = options as AuthOptions; const optsVarLists = Object.entries(optsObj) .filter(optsObjEntry => optsObjEntry[0] === targetType) .map(optsEntry => { @@ -69,14 +214,14 @@ export class RequestAuth extends Property { private type: string; private authOptions: Map> = new Map(); - constructor(options: RawAuthOptions, parent?: Property) { + constructor(options: AuthOptions, parent?: Property) { super(); if (!RequestAuth.isValidType(options.type)) { throw Error(`invalid auth type ${options.type}`); } this.type = options.type; - const optsObj = options as RawAuthOptions; + const optsObj = options as AuthOptions; Object.entries(optsObj) .filter(optsObjEntry => optsObjEntry[0] !== 'type') .map(optsEntry => { @@ -114,7 +259,7 @@ export class RequestAuth extends Property { } toJSON() { - const obj: RawAuthOptions = { type: this.type }; + const obj: AuthOptions = { type: this.type }; const authOption = this.authOptions.get(this.type); if (!authOption) { return obj; diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-base.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-base.ts index e4b45d3e33..5d2f1d223c 100644 --- a/packages/insomnia/src/renderers/hidden-browser-window/object-base.ts +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-base.ts @@ -163,7 +163,7 @@ export class Property extends PropertyBase { } } -export class PropertyList { +export class PropertyList { kind: string = 'PropertyList'; protected _parent: PropertyList | undefined = undefined; protected list: T[] = []; diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-certificates.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-certificates.ts index ce6af0adc8..206f210a9d 100644 --- a/packages/insomnia/src/renderers/hidden-browser-window/object-certificates.ts +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-certificates.ts @@ -1,7 +1,7 @@ import { Property } from './object-base'; import { UrlMatchPattern, UrlMatchPatternList } from './object-urls'; -export interface RawCertificateOptions { +export interface CertificateOptions { name?: string; matches?: string[]; key?: object; @@ -18,7 +18,7 @@ export class Certificate extends Property { passphrase?: string; pfx?: object; // PFX or PKCS12 Certificate - constructor(options: RawCertificateOptions) { + constructor(options: CertificateOptions) { super(); this.kind = 'Certificate'; this.name = options.name; @@ -42,7 +42,7 @@ export class Certificate extends Property { return this.matches ? this.matches.test(url) : false; } - update(options: RawCertificateOptions) { + update(options: CertificateOptions) { this.name = options.name; this.matches = new UrlMatchPatternList( undefined, diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-headers.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-headers.ts index 9be89ab8ad..08143070a6 100644 --- a/packages/insomnia/src/renderers/hidden-browser-window/object-headers.ts +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-headers.ts @@ -1,5 +1,14 @@ import { Property, PropertyList } from './object-base'; +export interface HeaderOptions { + id?: string; + name?: string; + type?: string; + disabled?: boolean; + key: string; + value: string; +} + export class Header extends Property { kind: string = 'Header'; type: string = ''; @@ -7,14 +16,7 @@ export class Header extends Property { value: string; constructor( - opts: { - id?: string; - name?: string; - type?: string; - disabled?: boolean; - key: string; - value: string; - } | string, + opts: HeaderOptions | string, name?: string, // if it is defined, it overrides 'key' (not 'name') ) { super(); diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-proxy-configs.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-proxy-configs.ts index 99db408caf..f07a7773b4 100644 --- a/packages/insomnia/src/renderers/hidden-browser-window/object-proxy-configs.ts +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-proxy-configs.ts @@ -1,5 +1,16 @@ import { Property, PropertyList } from './object-base'; +export interface ProxyConfigOptions { + match: string; + host: string; + port: number; + tunnel: boolean; + disabled?: boolean; + authenticate: boolean; + username: string; + password: string; +} + export class ProxyConfig extends Property { kind: string = 'ProxyConfig'; type: string; @@ -36,12 +47,12 @@ export class ProxyConfig extends Property { id?: string; name?: string; type?: string; - disabled?: boolean; - host: string; match: string; + host: string; port: number; tunnel: boolean; + disabled?: boolean; authenticate: boolean; username: string; password: string; diff --git a/packages/insomnia/src/renderers/hidden-browser-window/object-req-resp.ts b/packages/insomnia/src/renderers/hidden-browser-window/object-req-resp.ts new file mode 100644 index 0000000000..44c1ea94cb --- /dev/null +++ b/packages/insomnia/src/renderers/hidden-browser-window/object-req-resp.ts @@ -0,0 +1,260 @@ +import queryString from 'query-string'; + +import { AuthOptions } from './object-auth'; +import { Property, PropertyBase, PropertyList } from './object-base'; +import { CertificateOptions } from './object-certificates'; +import { HeaderOptions } from './object-headers'; +import { ProxyConfigOptions } from './object-proxy-configs'; +import { Url } from './object-urls'; + +// export type RequestBodyMode = +// file string +// formdata string +// graphql string +// raw string +// urlencoded string + +export type RequestBodyMode = undefined | 'formdata' | 'urlencoded' | 'raw' | 'file' | 'graphql'; + +export interface RequestBodyOptions { + mode: RequestBodyMode; + file?: string; + formdata?: { key: string; value: string }[]; + graphql?: object; + raw?: string; + urlencoded?: { key: string; value: string }[] | string; +} + +class FormParam { + key: string; + value: string; + + constructor(options: { key: string; value: string }) { + this.key = options.key; + this.value = options.value; + } + + // (static) _postman_propertyAllowsMultipleValues :Boolean + // (static) _postman_propertyIndexKey :String + + // not implemented either + // static parse(_: FormParam) { + // throw Error('unimplemented yet'); + // } + + toJSON() { + return { key: this.key, value: this.value }; + } + + toString() { + return `${this.key}=${this.value}`; // validate key, value contains '=' + } + + valueOf() { + return this.value; + } +} + +class QueryParam extends Property { + key: string; + value: string; + + constructor(options: { key: string; value: string } | string) { + super(); + + if (typeof options === 'string') { + try { + const optionsObj = JSON.parse(options); + // validate key and value fields + this.key = optionsObj.key; + this.value = optionsObj.value; + } catch (e) { + throw Error(`invalid QueryParam options ${e}`); + } + } else if (typeof options === 'object' && ('key' in options) && ('value' in options)) { + this.key = options.key; + this.value = options.value; + } else { + throw Error('unknown options for new QueryParam'); + } + } + + // TODO: + // (static) _postman_propertyAllowsMultipleValues :Boolean + // (static) _postman_propertyIndexKey :String + + static parse(queryStr: string) { + // this may not always be executed in the browser + return queryString.parse(queryStr); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + static parseSingle(param: string, _idx?: number, _all?: string[]) { + // it seems that _idx and _all are not useful + return queryString.parse(param); + + } + + static unparse(params: object) { + return Object.entries(params) + .map(entry => `${entry[0]}=${entry[1] || ''}`) + .join('&'); + } + + static unparseSingle(obj: { key: string; value: string }) { + if ('key' in obj && 'value' in obj) { + // TODO: validate and unescape + return `${obj.key}=${obj.value}`; + } + return {}; + } + + toString() { + return `${this.key}=${this.value}`; // validate key, value contains '=' + } + + update(param: string | { key: string; value: string }) { + if (typeof param === 'string') { + const paramObj = QueryParam.parse(param); + this.key = typeof paramObj.key === 'string' ? paramObj.key : ''; + this.value = typeof paramObj.value === 'string' ? paramObj.value : ''; + } else if ('key' in param && 'value' in param) { + this.key = param.key; + this.value = param.value; + } else { + throw Error('the param for update must be: string | { key: string; value: string }'); + } + } +} + +export class RequestBody extends PropertyBase { + mode: RequestBodyMode; // type of request data + file?: string; // It can be a file path (when used with Node.js) or a unique ID (when used with the browser). + formdata?: PropertyList; + graphql?: object; // raw graphql data + options?: object; // request body options + raw?: string; // raw body + urlencoded?: PropertyList | string; // URL encoded body params + + constructor(opts: RequestBodyOptions) { + super({ description: '' }); + + this.file = opts.file; + this.formdata = opts.formdata ? + new PropertyList( + opts.formdata. + map(formParamObj => new FormParam({ key: formParamObj.key, value: formParamObj.value })) + ) : + undefined; + this.graphql = opts.graphql; + this.mode = opts.mode; + // this.options = opts.options; + this.raw = opts.raw; + + if (typeof opts.urlencoded === 'string') { + const queryParamObj = QueryParam.parse(opts.urlencoded); + this.urlencoded = opts.urlencoded ? + new PropertyList( + Object.entries(queryParamObj) + .map(entry => ({ key: entry[0], value: JSON.stringify(entry[1]) })) + .map(kv => new QueryParam(kv)), + ) : + undefined; + } else { + // TODO: validate key, value in each entry + this.urlencoded = opts.urlencoded ? + new PropertyList( + opts.urlencoded + .map(entry => ({ key: entry.key, value: entry.value })) + .map(kv => new QueryParam(kv)), + ) : + undefined; + } + } + + isEmpty() { + switch (this.mode) { + case 'formdata': + return this.formdata == null; + case 'urlencoded': + return this.urlencoded == null; + case 'raw': + return this.raw == null; + case 'file': + return this.file == null; + case 'graphql': + return this.graphql == null; + default: + throw Error(`mode (${this.mode}) is unexpected`); + } + } + + toString() { + try { + switch (this.mode) { + case 'formdata': + return this.formdata ? this.formdata?.toString() : ''; + case 'urlencoded': + return this.urlencoded ? this.urlencoded.toString() : ''; + case 'raw': + return this.raw ? this.raw.toString() : ''; + case 'file': + return this.file || ''; // TODO: check file id or file content + case 'graphql': + return this.graphql ? JSON.stringify(this.graphql) : ''; + default: + throw Error(`mode (${this.mode}) is unexpected`); + } + } catch (e) { + return ''; + } + } + + update(opts: RequestBodyOptions) { + this.file = opts.file; + this.formdata = opts.formdata ? + new PropertyList( + opts.formdata. + map(formParamObj => new FormParam({ key: formParamObj.key, value: formParamObj.value })) + ) : + undefined; + this.graphql = opts.graphql; + this.mode = opts.mode; + // this.options = opts.options; + this.raw = opts.raw; + + if (typeof opts.urlencoded === 'string') { + const queryParamObj = QueryParam.parse(opts.urlencoded); + this.urlencoded = opts.urlencoded ? + new PropertyList( + Object.entries(queryParamObj) + .map(entry => ({ key: entry[0], value: JSON.stringify(entry[1]) })) + .map(kv => new QueryParam(kv)), + ) : + undefined; + } else { + // TODO: validate key, value in each entry + this.urlencoded = opts.urlencoded ? + new PropertyList( + opts.urlencoded + .map(entry => ({ key: entry.key, value: JSON.stringify(entry.value) })) + .map(kv => new QueryParam(kv)), + ) : + undefined; + } + } +} + +export interface RequestOptions { + url: string | Url; + method: string; + header: HeaderOptions; + body: RequestBodyOptions; + auth: AuthOptions; + proxy: ProxyConfigOptions; + certificate: CertificateOptions; +} + +export class Request extends Property { + +}