diff --git a/packages/insomnia/src/renderers/utility-process/object-auth.ts b/packages/insomnia/src/renderers/utility-process/object-auth.ts new file mode 100644 index 0000000000..4802e5407d --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-auth.ts @@ -0,0 +1,34 @@ +import { Property } from './object-base'; + +type AuthType = 'none' | 'apikey' | 'bearer' | 'jwt' | 'basic'; + +export class RequestAuth extends Property { + private authType: AuthType; + + constructor(options: { type: string }, parent?: Property) { + super(); + this.authType = options.type as AuthType; + this._parent = parent; + } + + // static isValidType(type: string) { + // return + // } + + // clear(type) { + + // } + + // defined + // describe(content, typeopt) + // findInParents(property, customizeropt) → {*| undefined } + // forEachParent(options, iterator) + // meta() → {*} + // parent() → {*| undefined } + + // undefined + // parameters() → { VariableList } + // toJSON() + // update(options, typeopt) + // use(type, options) +} diff --git a/packages/insomnia/src/renderers/utility-process/object-base.ts b/packages/insomnia/src/renderers/utility-process/object-base.ts new file mode 100644 index 0000000000..e4b45d3e33 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-base.ts @@ -0,0 +1,388 @@ +import clone from 'clone'; +import equal from 'deep-equal'; + +export interface JSONer { + toJSON: () => object; +} + +export class PropertyBase { + public kind = 'PropertyBase'; + protected description: string; + protected _parent: PropertyBase | undefined = undefined; + + constructor(def: { description: string }) { + this.description = def.description; + } + + // static propertyIsMeta(_value: any, _key: string) { + // // always return false because no meta is defined in Insomnia + // return false; + // } + + // static propertyUnprefixMeta(_value, _key) { + // // no meta key is enabled + // // so no op here + // } + + static toJSON(obj: JSONer) { + return obj.toJSON(); + } + + meta() { + return {}; + }; + + parent() { + return this._parent; + } + + forEachParent(_options: { withRoot?: boolean }, iterator: (obj: PropertyBase) => boolean) { + const currentParent = this.parent(); + if (!currentParent) { + return; + } + + const queue: PropertyBase[] = [currentParent]; + const parents: PropertyBase[] = []; + + while (queue.length > 0) { + const ancester = queue.shift(); + if (!ancester) { + continue; + } + + // TODO: check options + const cloned = clone(ancester); + const keepIterating = iterator(cloned); + parents.push(cloned); + if (!keepIterating) { + break; + } + + const olderAncester = ancester.parent(); + if (olderAncester) { + queue.push(olderAncester); + } + } + + return parents; + } + + findInParents(property: string, customizer?: (ancester: PropertyBase) => boolean): PropertyBase | undefined { + const currentParent = this.parent(); + if (!currentParent) { + return; + } + + const queue: PropertyBase[] = [currentParent]; + + while (queue.length > 0) { + const ancester = queue.shift(); + if (!ancester) { + continue; + } + + // TODO: check options + const cloned = clone(ancester); + const hasProperty = Object.keys(cloned.meta()).includes(property); + if (!hasProperty) { + // keep traversing until parent has the property + // no op + } else { + if (customizer) { + if (customizer(cloned)) { + // continue until customizer returns a truthy value + return cloned; + } + } else { + // customizer is not specified, stop at the first parent that contains the property + return cloned; + } + } + + const olderAncester = ancester.parent(); + if (olderAncester) { + queue.push(olderAncester); + } + } + + return undefined; + } + + toJSON() { + const entriesToExport = Object + .entries(this) + .filter((kv: [string, any]) => + typeof kv[1] !== 'function' && typeof kv[1] !== 'undefined' + ); + + return Object.fromEntries(entriesToExport); + } + + toObject() { + return this.toJSON(); + } + + toString() { + return JSON.stringify(this.toJSON()); + } +} + +export class Property extends PropertyBase { + id?: string; + name?: string; + disabled?: boolean; + info?: object; + + constructor(def?: { + id?: string; + name?: string; + disabled?: boolean; + info?: object; + }) { + super({ description: 'Property' }); + this.id = def?.id || ''; + this.name = def?.name || ''; + this.disabled = def?.disabled || false; + this.info = def?.info || {}; + } + + // static replaceSubstitutions(_str: string, _variables: object): string { + // // TODO: unsupported + // return ''; + // } + + // static replaceSubstitutionsIn(obj: string, variables: object): object { + // // TODO: unsupported + // return {}; + // } + + describe(content: string, typeName: string) { + this.kind = typeName; + this.description = content; + } +} + +export class PropertyList { + kind: string = 'PropertyList'; + protected _parent: PropertyList | undefined = undefined; + protected list: T[] = []; + + constructor( + // public readonly typeClass: { new(...arg: any): T }, + // public readonly parent: string, + populate: T[], + ) { + this.list = populate; + } + + static isPropertyList(obj: object) { + return 'kind' in obj && obj.kind === 'PropertyList'; + } + + add(item: T) { + this.list.push(item); + } + + all() { + return new Map( + this.list.map( + pp => [pp.id, pp.toJSON()] + ), + ); + } + + append(item: T) { + // it doesn't move item to the end of list for avoiding side effect + this.add(item); + } + + assimilate(source: T[] | PropertyList, prune?: boolean) { + // it doesn't update values from a source list + if (prune) { + this.clear(); + } + if ('list' in source) { // it is PropertyList + this.list.push(...source.list); + } else { + this.list.push(...source); + } + } + + clear() { + this.list = []; + } + + count() { + return this.list.length; + } + + each(iterator: (item: T) => void, context: object) { + interface Iterator { + context?: object; + (item: T): void; + } + const it: Iterator = iterator; + it.context = context; + + this.list.forEach(it); + } + + // TODO: unsupported + // eachParent(iterator, contextopt) {} + + filter(rule: (item: T) => boolean, context: object) { + interface Iterator { + context?: object; + (item: T): boolean; + } + const it: Iterator = rule; + it.context = context; + + return this.list.filter(it); + } + + // TODO: support returning {Item|ItemGroup} + find(rule: (item: T) => boolean, context?: object) { + interface Finder { + context?: object; + (item: T): boolean; + } + const finder: Finder = rule; + finder.context = context; + + return this.list.find(finder); + } + + // it does not return underlying type of the item because they are not supported + get(key: string) { + return this.one(key); + } + + // TODO: value is not used as its usage is unknown + // eslint-disable-next-line @typescript-eslint/no-unused-vars + has(item: T, _value: any) { + return this.indexOf(item) >= 0; + } + + idx(index: number) { + if (index <= this.list.length - 1) { + return this.list[index]; + } + return undefined; + } + + indexOf(item: string | T) { + for (let i = 0; i < this.list.length; i++) { + if (typeof item === 'string') { + if (item === this.list[i].id) { + return i; + } + } else { + if (equal(item, this.list[i])) { + return i; + } + } + } + return -1; + } + + insert(item: T, before?: number) { + if (before && before <= this.list.length - 1) { + this.list = [...this.list.slice(0, before), item, ...this.list.slice(before)]; + } else { + this.append(item); + } + } + + insertAfter(item: T, after?: number) { + if (after && after <= this.list.length - 1) { + this.list = [...this.list.slice(0, after + 1), item, ...this.list.slice(after + 1)]; + } else { + this.append(item); + } + } + + map(iterator: (item: T) => any, context: object) { + interface Iterator { + context?: object; + (item: T): any; + } + const it: Iterator = iterator; + it.context = context; + + return this.list.map(it); + } + + one(id: string) { + for (let i = this.list.length - 1; i >= 0; i--) { + if (this.list[i].id === id) { + return this.list[i]; + } + } + + return undefined; + } + + populate(items: T[]) { + this.list = [...this.list, ...items]; + } + + prepend(item: T) { + this.list = [item, ...this.list]; + } + + reduce(iterator: ((acc: any, item: T) => any), accumulator: any, context: object) { + interface Iterator { + context?: object; + (acc: any, item: T): any; + } + const it: Iterator = iterator; + it.context = context; + + this.list.reduce(it, accumulator); + } + + remove(predicate: T | ((item: T) => boolean), context: object) { + if (typeof predicate === 'function') { + this.list = this.filter(predicate, context); + } else { + this.list = this.filter(item => equal(predicate, item), context); + } + } + + repopulate(items: T[]) { + this.clear(); + this.populate(items); + } + + // unsupportd as _postman_propertyIndexKey is not supported + // toObject(excludeDisabled?: boolean, caseSensitive?: boolean, multiValue?: boolean, sanitizeKeys?: boolean) { + // const itemObjects = this.list + // .filter(item => { + // if (excludeDisabled) { + // return !item.disabled; + // } + // return true; + // }) + // .map(item => { + // return item.toJSON(); + // }); + // } + + toString() { + const itemStrs = this.list.map(item => item.toString()); + return `[${itemStrs.join(',')}]`; + } + + upsert(item: T): boolean { + const itemIdx = this.indexOf(item); + if (itemIdx >= 0) { + this.list = [...this.list.splice(0, itemIdx), item, ...this.list.splice(itemIdx + 1)]; + return false; + } + + this.add(item); + return true; + } +} diff --git a/packages/insomnia/src/renderers/utility-process/object-cookies.ts b/packages/insomnia/src/renderers/utility-process/object-cookies.ts new file mode 100644 index 0000000000..ee2f0daaf4 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-cookies.ts @@ -0,0 +1,153 @@ +import { Property, PropertyList } from './object-base'; + +export interface CookieDef { + key: string; + value: string; + expires?: Date | string; + maxAge?: Number; + domain?: string; + path?: string; + secure?: Boolean; + httpOnly?: Boolean; + hostOnly?: Boolean; + session?: Boolean; + extensions?: { key: string; value: string }[]; +} + +export class Cookie extends Property { + private def: object; + + constructor(cookieDef: CookieDef | string) { + super(); + this.kind = 'Cookie'; + this.description = 'Cookie'; + + if (typeof cookieDef === 'string') { + this.def = Cookie.parse(cookieDef); + } else { + this.def = cookieDef; + } + } + + static isCookie(obj: Property) { + return obj.kind === 'Cookie'; + } + + static parse(cookieStr: string) { + const parts = cookieStr.split(';'); + + const def: CookieDef = { key: '', value: '' }; + const extensions: { key: string; value: string }[] = []; + + parts.forEach((part, i) => { + const kvParts = part.split('='); + const key = kvParts[0]; + + if (i === 0) { + const value = kvParts.length > 1 ? kvParts[1] : ''; + def.key, def.value = key, value; + } else { + switch (key) { + case 'Expires': + // TODO: it should be timestamp + const expireVal = kvParts.length > 1 ? kvParts[1] : '0'; + def.expires = expireVal; + break; + case 'Max-Age': + let maxAgeVal = 0; + if (kvParts.length > 1) { + maxAgeVal = parseInt(kvParts[1], 10); + } + def.maxAge = maxAgeVal; + break; + case 'Domain': + const domainVal = kvParts.length > 1 ? kvParts[1] : ''; + def.domain = domainVal; + break; + case 'Path': + const pathVal = kvParts.length > 1 ? kvParts[1] : ''; + def.path = pathVal; + break; + case 'Secure': + def.secure = true; + break; + case 'HttpOnly': + def.httpOnly = true; + break; + case 'HostOnly': + def.hostOnly = true; + break; + case 'Session': + def.session = true; + break; + default: + const value = kvParts.length > 1 ? kvParts[1] : ''; + extensions.push({ key, value }); + def.extensions = extensions; + } + } + }); + + return def; + } + + static stringify(cookie: Cookie) { + return cookie.toString(); + } + + static unparseSingle(cookie: Cookie) { + return cookie.toString(); + } + + // TODO: support PropertyList + static unparse(cookies: Cookie[]) { + const cookieStrs = cookies.map(cookie => cookie.toString()); + return cookieStrs.join(';'); + } + + toString = () => { + // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie + const cookieDef = this.def as CookieDef; + const kvPair = `${cookieDef.key}=${cookieDef.value};`; + const expires = cookieDef.expires ? `Expires=${cookieDef.expires?.toString()};` : ''; + const maxAge = cookieDef.maxAge ? `Max-Age=${cookieDef.maxAge};` : ''; + const domain = cookieDef.domain ? `Domain=${cookieDef.domain};` : ''; + const path = cookieDef.path ? `Path=${cookieDef.path};` : ''; + const secure = cookieDef.secure ? 'Secure;' : ''; + const httpOnly = cookieDef.httpOnly ? 'HttpOnly;' : ''; + // TODO: SameSite, Partitioned is not suported + + const hostOnly = cookieDef.hostOnly ? 'HostOnly;' : ''; + const session = cookieDef.session ? 'Session;' : ''; + + // TODO: extension key may be conflict with pre-defined keys + const extensions = cookieDef.extensions ? + cookieDef.extensions + .map((kv: { key: string; value: string }) => `${kv.key}=${kv.value}`) + .join(';') : ''; // the last field doesn't have ';' + + return `${kvPair} ${expires} ${maxAge} ${domain} ${path} ${secure} ${httpOnly} ${hostOnly} ${session} ${extensions}`; + }; + + valueOf = () => { + return (this.def as CookieDef).value; + }; +} + +export class CookieList extends PropertyList { + kind: string = 'CookieList'; + cookies: Cookie[]; + + constructor(parent: CookieList, cookies: Cookie[]) { + super( + // Cookie, parent.toString() + cookies + ); + this._parent = parent; + this.cookies = cookies; + } + + static isCookieList(obj: object) { + return 'kind' in obj && obj.kind === 'CookieList'; + } +} diff --git a/packages/insomnia/src/renderers/utility-process/object-headers.ts b/packages/insomnia/src/renderers/utility-process/object-headers.ts new file mode 100644 index 0000000000..9be89ab8ad --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-headers.ts @@ -0,0 +1,104 @@ +import { Property, PropertyList } from './object-base'; + +export class Header extends Property { + kind: string = 'Header'; + type: string = ''; + key: string; + value: string; + + constructor( + opts: { + id?: string; + name?: string; + type?: string; + disabled?: boolean; + key: string; + value: string; + } | string, + name?: string, // if it is defined, it overrides 'key' (not 'name') + ) { + super(); + + if (typeof opts === 'string') { + const obj = Header.parseSingle(opts); + this.key = obj.key; + this.value = obj.value; + } else { + this.id = opts.id ? opts.id : ''; + this.key = opts.key ? opts.key : ''; + this.name = name ? name : (opts.name ? opts.name : ''); + this.value = opts.value ? opts.value : ''; + this.type = opts.type ? opts.type : ''; + this.disabled = opts ? opts.disabled : false; + } + } + + static create(input?: { key: string; value: string } | string, name?: string): Header { + return new Header(input || { key: '', value: '' }, name); + } + + static isHeader(obj: object) { + return 'kind' in obj && obj.kind === 'Header'; + } + + // example: 'Content-Type: application/json\nUser-Agent: MyClientLibrary/2.0\n' + static parse(headerString: string): { key: string; value: string }[] { + return headerString + .split('\n') + .map(kvPart => Header.parseSingle(kvPart)); + } + + static parseSingle(headerStr: string): { key: string; value: string } { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers + // the first colon is the separator + const separatorPos = headerStr.indexOf(':'); + const key = headerStr.slice(0, separatorPos); + const value = headerStr.slice(separatorPos + 1); + + return { key, value }; + } + + static unparse(headers: { key: string; value: string }[] | PropertyList
, separator?: string): string { + const headerStrs = headers.map( + header => this.unparseSingle(header), {} + ); + + return headerStrs.join(separator || '\n'); + } + + static unparseSingle(header: { key: string; value: string } | Header): string { + // both PropertyList and object contains 'key' and 'value' + return `${header.key}: ${header.value}`; + } + + update(newHeader: { key: string; value: string }) { + this.key = newHeader.key; + this.value = newHeader.value; + } + + valueOf() { + return this.value; + } +} + +export class HeaderList extends PropertyList { + constructor(parent: PropertyList | undefined, populate: T[]) { + super(populate); + this._parent = parent; + } + + static isHeaderList(obj: any) { + return 'kind' in obj && obj.kind === 'HeaderList'; + } + + // unsupported + // eachParent(iterator, contextopt) + // toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object} + + contentSize(): number { + return this.list + .map(header => header.toString()) + .map(headerStr => headerStr.length) // TODO: handle special characters + .reduce((totalSize, headerSize) => totalSize + headerSize, 0); + } +} diff --git a/packages/insomnia/src/renderers/utility-process/object-proxy-configs.ts b/packages/insomnia/src/renderers/utility-process/object-proxy-configs.ts new file mode 100644 index 0000000000..99db408caf --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-proxy-configs.ts @@ -0,0 +1,157 @@ +import { Property, PropertyList } from './object-base'; + +export class ProxyConfig extends Property { + kind: string = 'ProxyConfig'; + type: string; + + host: string; + match: string; + port: number; + tunnel: boolean; + authenticate: boolean; + username: string; + password: string; + + static authenticate: boolean; + // static bypass: UrlMatchPatternList; + static host: string; + static match: string; + static password: string; + static port: number; + static tunnel: boolean; // unsupported + static username: string; + + static { + ProxyConfig.authenticate = false; + // ProxyConfig.bypass: UrlMatchPatternList; + ProxyConfig.host = ''; + ProxyConfig.match = ''; + ProxyConfig.password = ''; + ProxyConfig.port = 0; + ProxyConfig.tunnel = false; + ProxyConfig.username = ''; + } + + constructor(def: { + id?: string; + name?: string; + type?: string; + disabled?: boolean; + + host: string; + match: string; + port: number; + tunnel: boolean; + authenticate: boolean; + username: string; + password: string; + + }) { + super(); + + this.id = def.id ? def.id : ''; + this.name = def.name ? def.name : ''; + this.type = def.type ? def.type : ''; + this.disabled = def.disabled ? def.disabled : false; + + this.host = def.host; + this.match = def.match; + this.port = def.port; + this.tunnel = def.tunnel; + this.authenticate = def.authenticate; + this.username = def.username; + this.password = def.password; + } + + static isProxyConfig(obj: object) { + return 'kind' in obj && obj.kind === 'ProxyConfig'; + } + + // TODO: should not read from match? + getProtocols(): string[] { + // match field example: 'http+https://example.com/*' + const protoSeparator = this.match.indexOf('://'); + if (protoSeparator <= 0 || protoSeparator >= this.match.length) { + return []; // invalid match value + } + + return this.match + .slice(0, protoSeparator) + .split('+'); + } + + getProxyUrl(): string { + const protos = this.getProtocols(); + // TODO: how to pick up a protocol? + if (protos.length === 0) { + return ''; + } + + // http://proxy_username:proxy_password@proxy.com:8080 + if (this.authenticate) { + return `protos[0]://${this.username}:${this.password}@${this.host}:${this.port}`; + } + return `protos[0]://${this.host}:${this.port}`; + } + + // TODO: unsupported yet + // test(urlStropt) + + update(options: { + host: string; + match: string; + port: number; + tunnel: boolean; + authenticate: boolean; + username: string; + password: string; + }) { + this.host = options.host; + this.match = options.match; + this.port = options.port; + this.tunnel = options.tunnel; + this.authenticate = options.authenticate; + this.username = options.username; + this.password = options.password; + } + + updateProtocols(protocols: string[]) { + const protoSeparator = this.match.indexOf('://'); + if (protoSeparator <= 0 || protoSeparator >= this.match.length) { + return; // invalid match value + } + + this.match = protocols.join('+') + this.match.slice(protoSeparator); + } +} + +// myProxyConfig = new ProxyConfigList({}, [ +// {match: 'https://example.com/*', host: 'proxy.com', port: 8080, tunnel: true}, +// {match: 'http+https://example2.com/*', host: 'proxy2.com'}, +// ]); + +export class ProxyConfigList extends PropertyList { + constructor(parent: PropertyList | undefined, populate: T[]) { + super(populate); + this._parent = parent; + } + + static isProxyConfigList(obj: any) { + return 'kind' in obj && obj.kind === 'ProxyConfigList'; + } + + // TODO: need support URL at first + // resolve(url?: URL) { + // return { + // host: string; + // match: string; + // port: number; + // tunnel: boolean; + // authenticate: boolean; + // username: string; + // password: string; + // } + // } + + // toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object} +} diff --git a/packages/insomnia/src/renderers/utility-process/object-variables.ts b/packages/insomnia/src/renderers/utility-process/object-variables.ts new file mode 100644 index 0000000000..34e8f1f848 --- /dev/null +++ b/packages/insomnia/src/renderers/utility-process/object-variables.ts @@ -0,0 +1,57 @@ +import { Property, PropertyList } from './object-base'; + +export class Variable extends Property { + key: string; + value: any; + type: string; + kind: string = 'Variable'; + + constructor(def?: { + id: string; + key: string; + name: string; + value: string; + type: string; + disabled: boolean; + }) { + super(); + + this.id = def ? def.id : ''; + this.key = def ? def.key : ''; + this.name = def ? def.name : ''; + this.value = def ? def.value : ''; + this.type = def ? def.type : ''; + this.disabled = def ? def.disabled : false; + } + + // unknown usage and unsupported + // static readonly types() => { + // } + + // cast typecasts a value to the Variable.types of this Variable. + cast(value: any) { + if ('kind' in value && value.kind === 'Variable') { + return value.value; + } + return undefined; + } + + get() { + return this.value; + } + + set(value: any) { + this.value = value; + } +} + +export class VariableList extends PropertyList { + constructor(parent: PropertyList | undefined, populate: T[]) { + super(populate); + this._parent = parent; + } + + static isVariableList(obj: any) { + return 'kind' in obj && obj.kind === 'VariableList'; + } +}