feat: isolate major DOM handlers

This commit is contained in:
George He
2024-01-12 15:37:15 +08:00
parent 0b0f79f204
commit a1fe7fb514
12 changed files with 13 additions and 2413 deletions

View File

@@ -1,4 +1,5 @@
import { initGlobalObject, InsomniaObject, require } from './inso-object';
import { initGlobalObject, InsomniaObject } from './inso-object';
import { require } from './sdk-objects/require';
const ErrorTimeout = 'executing script timeout';
const ErrorInvalidResult = 'result is invalid, null or custom value may be returned';
@@ -22,6 +23,11 @@ async function init() {
const executeScript = AsyncFunction(
'insomnia',
'require',
// isolate DOM objects
// window properties
'closed', 'console', 'credentialless', 'customElements', 'devicePixelRatio', 'document', 'documentPictureInPicture', 'event', 'external', 'frameElement', 'frames', 'fullScreen', 'history', 'innerHeight', 'innerWidth', 'launchQueue', 'length', 'localStorage', 'location', 'locationbar', 'menubar', 'name', 'navigation', 'navigator', 'opener', 'orientation', 'originAgentCluster', 'outerHeight', 'outerWidth', 'parent', 'personalbar', 'screen', 'screenLeft', 'screenTop', 'screenX', 'screenY', 'scrollbars', 'scrollMaxX', 'scrollMaxY', 'scrollX', 'scrollY', 'self', 'sessionStorage', 'sharedStorage', 'sidebar', 'speechSynthesis', 'status', 'statusbar', 'toolbar', 'top', 'visualViewport', 'window',
// window methods
'alert', 'backalert', 'bluralert', 'cancelAnimationFramealert', 'cancelIdleCallbackalert', 'captureEventsalert', 'clearImmediatealert', 'closealert', 'confirmalert', 'dumpalert', 'findalert', 'focusalert', 'forwardalert', 'getComputedStylealert', 'getDefaultComputedStylealert', 'getScreenDetailsalert', 'getSelectionalert', 'matchMediaalert', 'moveByalert', 'moveToalert', 'openalert', 'postMessagealert', 'printalert', 'promptalert', 'queryLocalFontsalert', 'releaseEventsalert', 'requestAnimationFramealert', 'requestFileSystemalert', 'requestIdleCallbackalert', 'resizeByalert', 'resizeToalert', 'scrollalert', 'scrollByalert', 'scrollByLinesalert', 'scrollByPagesalert', 'scrollToalert', 'setImmediatealert', 'setResizablealert', 'showDirectoryPickeralert', 'showModalDialogalert', 'showOpenFilePickeralert', 'showSaveFilePickeralert', 'sizeToContentalert', 'stopalert', 'updateCommandsalert', 'webkitConvertPointFromNodeToPagealert', 'webkitConvertPointFromPageToNodealert',
// if possible, avoid adding code to the following part
`
const $ = insomnia, pm = insomnia;
@@ -39,6 +45,10 @@ async function init() {
const insoObject = await executeScript(
insomniaObject,
require,
// window properties
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
// window methods
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
);
clearTimeout(timeoutChecker);
if (insoObject instanceof InsomniaObject) {
@@ -46,6 +56,7 @@ async function init() {
} else {
throw { message: ErrorInvalidResult };
}
} catch (e) {
reject(e);
}

View File

@@ -1,6 +1,4 @@
import * as uuid from 'uuid';
import { getIntepolator } from './intepolator';
import { getIntepolator } from './sdk-objects/intepolator';
export type EventName = 'prerequest' | 'test';
@@ -225,21 +223,3 @@ export function initGlobalObject(rawObj: RawObject) {
requestInfo,
});
};
const builtinModules = new Map<string, any>([
['uuid', uuid],
]);
const nodeModules = new Map<string, any>([]);
export function require(moduleName: string) {
if (builtinModules.has(moduleName)) {
return builtinModules.get(moduleName);
}
if (nodeModules.has(moduleName)) {
// invoke main.js
}
throw Error(`no module is found for "${moduleName}"`);
}

View File

@@ -1,34 +0,0 @@
import { configure, type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks';
class Intepolator {
private engine: NunjuncksEnv;
constructor(config: ConfigureOptions) {
this.engine = configure(config);
}
render = (template: string, context: object) => {
// TODO: handle timeout
// TODO: support plugin?
return this.engine.renderString(template, context);
};
}
const intepolator = new Intepolator({
autoescape: false,
// Don't escape HTML
throwOnUndefined: true,
// Strict mode
tags: {
blockStart: '{%',
blockEnd: '%}',
variableStart: '{{',
variableEnd: '}}',
commentStart: '{#',
commentEnd: '#}',
},
});
export function getIntepolator() {
return intepolator;
}

View File

@@ -1,336 +0,0 @@
import { Property } from './object-base';
import { Variable, VariableList } from './object-variables';
const AuthTypes = new Set([
'noauth',
'basic',
'bearer',
'jwt',
'digest',
'oauth1',
'oauth2',
'hawk',
'awsv4',
'ntlm',
'apikey',
'edgegrid',
'asap',
]);
export interface AuthOption {
type: string;
key: string;
value: string;
}
// 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[];
jwt?: AuthOption[];
digest?: AuthOption[];
oauth1?: AuthOption[];
oauth2?: AuthOption[];
hawk?: AuthOption[];
awsv4?: AuthOption[];
ntlm?: AuthOption[];
apikey?: AuthOption[];
edgegrid?: AuthOption[];
asap?: AuthOption[];
}
function rawOptionsToVariables(options: VariableList<Variable> | Variable[] | object, targetType?: string): VariableList<Variable>[] {
if (VariableList.isVariableList(options)) {
return [options as VariableList<Variable>];
} else if ('type' in options) { // object
const optsObj = options as AuthOptions;
const optsVarLists = Object.entries(optsObj)
.filter(optsObjEntry => optsObjEntry[0] === targetType)
.map(optsEntry => {
return new VariableList(
undefined,
optsEntry.map(opt => new Variable({
key: opt.key,
value: opt.value,
type: opt.type,
})),
);
});
return optsVarLists;
} else if ('length' in options) { // array
return [new VariableList(undefined, options)];
}
throw Error('options is not valid: it must be VariableList<Variable> | Variable[] | object');
}
export class RequestAuth extends Property {
private type: string;
private authOptions: Map<string, VariableList<Variable>> = new Map();
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 AuthOptions;
Object.entries(optsObj)
.filter(optsObjEntry => optsObjEntry[0] !== 'type')
.map(optsEntry => {
return {
type: optsEntry[0],
options: new VariableList(
undefined,
optsEntry.map(opt => new Variable({
key: opt.key,
value: opt.value,
type: opt.type,
})),
),
};
})
.forEach(authOpts => {
this.authOptions.set(authOpts.type, authOpts.options);
});
this._parent = parent;
}
static isValidType(authType: string) {
return AuthTypes.has(authType);
}
clear(type: string) {
if (RequestAuth.isValidType(type)) {
this.authOptions.delete(type);
}
}
parameters() {
return this.authOptions.get(this.type);
}
toJSON() {
const obj: AuthOptions = { type: this.type };
const authOption = this.authOptions.get(this.type);
if (!authOption) {
return obj;
}
const authOptionJSON = authOption.map(optValue => optValue.toJSON(), {});
switch (this.type) {
case 'basic':
obj.basic = authOptionJSON;
break;
case 'bearer':
obj.bearer = authOptionJSON;
break;
case 'jwt':
obj.jwt = authOptionJSON;
break;
case 'digest':
obj.digest = authOptionJSON;
break;
case 'oauth1':
obj.oauth1 = authOptionJSON;
break;
case 'oauth2':
obj.oauth2 = authOptionJSON;
break;
case 'hawk':
obj.hawk = authOptionJSON;
break;
case 'awsv4':
obj.awsv4 = authOptionJSON;
break;
case 'ntlm':
obj.ntlm = authOptionJSON;
break;
case 'apikey':
obj.apikey = authOptionJSON;
break;
case 'edgegrid':
obj.edgegrid = authOptionJSON;
break;
case 'asap':
obj.asap = authOptionJSON;
break;
default: // noauth, no op
}
return obj;
}
update(options: VariableList<Variable> | Variable[] | object, type?: string) {
const currentType = type ? type : this.type;
const authOpts = rawOptionsToVariables(options, currentType);
if (authOpts.length > 0) {
this.authOptions.set(currentType, authOpts[0]);
} else {
throw Error('no valid RequestAuth options is found');
}
}
use(type: string, options: VariableList<Variable> | Variable[] | object) {
if (!RequestAuth.isValidType(type)) {
throw Error(`invalid type (${type}), it must be noauth | basic | bearer | jwt | digest | oauth1 | oauth2 | hawk | awsv4 | ntlm | apikey | edgegrid | asap.`);
}
const authOpts = rawOptionsToVariables(options, type);
if (authOpts.length > 0) {
this.type = type;
this.authOptions.set(type, authOpts[0]);
} else {
throw Error('no valid RequestAuth options is found');
}
}
}

View File

@@ -1,388 +0,0 @@
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<T> {
kind: string = 'PropertyList';
protected _parent: PropertyList<T> | 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<T>, prune?: boolean) {
// it doesn't update values from a source list
if (prune) {
this.clear();
}
if ('list' in source) { // it is PropertyList<T>
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;
}
}

View File

@@ -1,58 +0,0 @@
import { Property } from './object-base';
import { UrlMatchPattern, UrlMatchPatternList } from './object-urls';
export interface CertificateOptions {
name?: string;
matches?: string[];
key?: object;
cert?: object;
passphrase?: string;
pfx?: object; // PFX or PKCS12 Certificate
}
export class Certificate extends Property {
name?: string;
matches?: UrlMatchPatternList<UrlMatchPattern>;
key?: object;
cert?: object;
passphrase?: string;
pfx?: object; // PFX or PKCS12 Certificate
constructor(options: CertificateOptions) {
super();
this.kind = 'Certificate';
this.name = options.name;
this.matches = new UrlMatchPatternList(
undefined,
options.matches ?
options.matches.map(matchStr => new UrlMatchPattern(matchStr)) :
[],
);
this.key = options.key;
this.cert = options.cert;
this.passphrase = options.passphrase;
this.pfx = options.pfx;
}
static isCertificate(obj: object) {
return 'kind' in obj && obj.kind === 'Certificate';
}
canApplyTo(url: string) {
return this.matches ? this.matches.test(url) : false;
}
update(options: CertificateOptions) {
this.name = options.name;
this.matches = new UrlMatchPatternList(
undefined,
options.matches ?
options.matches.map(matchStr => new UrlMatchPattern(matchStr)) :
[],
);
this.key = options.key;
this.cert = options.cert;
this.passphrase = options.passphrase;
this.pfx = options.pfx;
}
}

View File

@@ -1,153 +0,0 @@
import { Property, PropertyList } from './object-base';
export interface CookieOptions {
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: CookieOptions | 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: CookieOptions = { 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 CookieOptions;
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 CookieOptions).value;
};
}
export class CookieList extends PropertyList<Cookie> {
kind: string = 'CookieList';
cookies: Cookie[];
constructor(parent: CookieList | undefined, cookies: Cookie[]) {
super(
// Cookie, parent.toString()
cookies
);
this._parent = parent;
this.cookies = cookies;
}
static isCookieList(obj: object) {
return 'kind' in obj && obj.kind === 'CookieList';
}
}

View File

@@ -1,106 +0,0 @@
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 = '';
key: string;
value: string;
constructor(
opts: HeaderOptions | 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<Header>, 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<T extends Header> extends PropertyList<T> {
constructor(parent: PropertyList<T> | 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);
}
}

View File

@@ -1,168 +0,0 @@
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;
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;
match: string;
host: string;
port: number;
tunnel: boolean;
disabled?: 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<T extends ProxyConfig> extends PropertyList<T> {
constructor(parent: PropertyList<T> | 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}
}

View File

@@ -1,526 +0,0 @@
import { RESPONSE_CODE_REASONS } from '../../common/constants';
import { AuthOptions, RequestAuth } from './object-auth';
import { Property, PropertyBase, PropertyList } from './object-base';
import { CertificateOptions } from './object-certificates';
import { Certificate } from './object-certificates';
import { Cookie, CookieList, CookieOptions } from './object-cookies';
import { HeaderOptions } from './object-headers';
import { Header, HeaderList } from './object-headers';
import { ProxyConfig, ProxyConfigOptions } from './object-proxy-configs';
import { QueryParam, Url } from './object-urls';
import { Variable, VariableList } from './object-variables';
// 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 }[];
}
class FormParam {
key: string;
value: string;
constructor(options: { key: string; value: string }) {
this.key = options.key;
this.value = options.value;
}
// TODO
// (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;
}
}
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<FormParam>;
graphql?: object; // raw graphql data
options?: object; // request body options
raw?: string; // raw body
urlencoded?: PropertyList<QueryParam>; // 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 interface RequestSize {
body: number;
header: number;
total: number;
source: string;
}
export class Request extends Property {
kind: string = 'Request';
url: Url;
method: string;
headers: HeaderList<Header>;
body?: RequestBody;
auth: RequestAuth;
proxy: ProxyConfig;
certificate?: Certificate;
constructor(options: RequestOptions) {
super();
this.url = typeof options.url === 'string' ? new Url(options.url) : options.url;
this.method = options.method;
this.headers = new HeaderList(
undefined,
options.header.map(header => new Header(header)),
);
this.body = new RequestBody(options.body);
this.auth = new RequestAuth(options.auth);
this.proxy = new ProxyConfig(options.proxy);
this.certificate = new Certificate(options.certificate);
}
static isRequest(obj: object) {
return 'kind' in obj && obj.kind === 'Request';
}
addHeader(header: Header | object) {
if (Header.isHeader(header)) {
const headerInstance = header as Header;
this.headers.add(headerInstance);
} else if ('key' in header && 'value' in header) {
const headerInstance = new Header(header);
this.headers.add(headerInstance);
} else {
throw Error('header must be Header | object');
}
}
addQueryParams(params: QueryParam[] | string) {
this.url.addQueryParams(params);
}
authorizeUsing(authType: string | AuthOptions, options?: VariableList<Variable>) {
const selectedAuth = typeof authType === 'string' ? authType : authType.type;
this.auth.use(selectedAuth, options || {});
}
clone() {
return new Request({
url: this.url,
method: this.method,
header: this.headers.map(header => header.toJSON(), {}),
body: {
mode: this.body?.mode,
file: this.body?.file,
formdata: this.body?.formdata?.map(formParam => formParam.toJSON(), {}),
graphql: this.body?.graphql,
raw: this.body?.raw,
urlencoded: this.body?.urlencoded?.map(queryParam => queryParam.toJSON(), {}),
},
auth: this.auth.toJSON(),
proxy: {
match: this.proxy.match,
host: this.proxy.host,
port: this.proxy.port,
tunnel: this.proxy.tunnel,
disabled: this.proxy.disabled,
authenticate: this.proxy.authenticate,
username: this.proxy.username,
password: this.proxy.password,
},
certificate: {
name: this.certificate?.name,
matches: this.certificate?.matches?.map(match => match.toString(), {}),
key: this.certificate?.key,
cert: this.certificate?.cert,
passphrase: this.certificate?.passphrase,
pfx: this.certificate?.pfx,
},
});
}
forEachHeader(callback: (header: Header, context?: object) => void) {
this.headers.each(callback, {});
}
getHeaders(options?: {
ignoreCase: boolean;
enabled: boolean;
multiValue: boolean;
sanitizeKeys: boolean;
}) {
// merge headers with same key into an array
const headerMap = new Map<string, string[]>();
this.headers.each(header => {
const enabled = options?.enabled ? !header.disabled : true;
const sanitized = options?.sanitizeKeys ? !!header.value : true;
const hasName = !!header.key;
if (!enabled || !sanitized || !hasName) {
return;
}
header.key = options?.ignoreCase ? header.key?.toLocaleLowerCase() : header.key;
if (headerMap.has(header.key)) {
const existingHeader = headerMap.get(header.key) || [];
headerMap.set(header.key, [...existingHeader, header.value]);
} else {
headerMap.set(header.key, [header.value]);
}
}, {});
const obj: Record<string, string[] | string> = {};
Array.from(headerMap.entries())
.forEach(headerEntry => {
obj[headerEntry[0]] = headerEntry[1];
});
return obj;
}
removeHeader(toRemove: string | Header, options: { ignoreCase: boolean }) {
const filteredHeaders = this.headers.filter(
header => {
if (!header.key) {
return false;
}
if (typeof toRemove === 'string') {
return options.ignoreCase ?
header.key.toLocaleLowerCase() !== toRemove.toLocaleLowerCase() :
header.key !== toRemove;
} else if ('name' in toRemove) {
if (!toRemove.key) {
return false;
}
return options.ignoreCase ?
header.key.toLocaleLowerCase() !== toRemove.key.toLocaleLowerCase() :
header.key !== toRemove.key;
} else {
throw Error('type of the "toRemove" must be: string | Header');
}
},
{},
);
this.headers = new HeaderList(undefined, filteredHeaders);
}
removeQueryParams(params: string | string[] | QueryParam[]) {
this.url.removeQueryParams(params);
}
// TODO:
// size() → { Object }
toJSON() {
return {
url: this.url,
method: this.method,
header: this.headers.map(header => header.toJSON(), {}),
body: {
mode: this.body?.mode,
file: this.body?.file,
formdata: this.body?.formdata?.map(formParam => formParam.toJSON(), {}),
graphql: this.body?.graphql,
raw: this.body?.raw,
urlencoded: this.body?.urlencoded?.map(queryParam => queryParam.toJSON(), {}),
},
auth: this.auth.toJSON(),
proxy: {
match: this.proxy.match,
host: this.proxy.host,
port: this.proxy.port,
tunnel: this.proxy.tunnel,
disabled: this.proxy.disabled,
authenticate: this.proxy.authenticate,
username: this.proxy.username,
password: this.proxy.password,
},
certificate: {
name: this.certificate?.name,
matches: this.certificate?.matches?.map(match => match.toString(), {}),
key: this.certificate?.key,
cert: this.certificate?.cert,
passphrase: this.certificate?.passphrase,
pfx: this.certificate?.pfx,
},
};
}
update(options: RequestOptions) {
this.url = typeof options.url === 'string' ? new Url(options.url) : options.url;
this.method = options.method;
this.headers = new HeaderList(
undefined,
options.header.map(header => new Header(header)),
);
this.body = new RequestBody(options.body);
this.auth = new RequestAuth(options.auth);
this.proxy = new ProxyConfig(options.proxy);
this.certificate = new Certificate(options.certificate);
}
upsertHeader(header: HeaderOptions) {
// remove keys with same name
this.headers = new HeaderList(
undefined,
this.headers
.filter(
existingHeader => existingHeader.key !== header.key,
{},
)
);
// append new
this.headers.append(new Header(header));
}
}
export interface ResponseOptions {
code: number;
reason?: string;
header?: HeaderOptions[];
cookie?: CookieOptions[];
body?: string;
stream?: Buffer | ArrayBuffer; // TODO: check if it works in both node.js and browser
responseTime: number;
status?: string;
}
export interface ResponseContentInfo {
mimeType: string;
mimeFormat: string;
charset: string;
fileExtension: string;
fileName: string;
contentType: string;
}
export class Response extends Property {
kind: string = 'Response';
body: string;
code: number;
cookies: CookieList;
headers: HeaderList<Header>;
// originalRequest: Request;
responseTime: number;
status: string;
constructor(options: ResponseOptions) {
super();
this.body = options.body || '';
this.code = options.code;
this.cookies = new CookieList(
undefined,
options.cookie?.map(cookie => new Cookie(cookie)) || [],
);
this.headers = new HeaderList(
undefined,
options.header?.map(headerOpt => new Header(headerOpt)) || [],
);
// TODO: how to init request?
// this.originalRequest = options.originalRequest;
this.responseTime = options.responseTime;
this.status = RESPONSE_CODE_REASONS[options.code];
}
// TODO: accurate type of response should be given
// static createFromNode(response: object, cookies: CookieOptions[]) {
// return new Response({
// cookie: cookies,
// body: response.body.toString(),
// stream: response.body,
// header: response.headers,
// code: response.statusCode,
// status: response.statusMessage,
// responseTime: response.elapsedTime,
// });
// }
static isResponse(obj: object) {
return 'kind' in obj && obj.kind === 'Response';
}
// TODO: need a library for this
// contentInfo(): ResponseContentInfo {
// return {
// mimeType: string;
// mimeFormat: string;
// charset: string;
// fileExtension: string;
// fileName: string;
// contentType: string;
// };
// }
// dataURI() {
// }
// need a library for this
// json(reviver?, strict?) {
// }
// jsonp(reviver?, strict?) {
// }
reason() {
return this.status;
}
// TODO:
// size() → {Number}
text() {
return this.body;
}
}

View File

@@ -1,563 +0,0 @@
import queryString from 'query-string';
import { Property, PropertyBase, PropertyList } from './object-base';
import { Variable, VariableList } from './object-variables';
// export class QueryParam extends Property {
// key: string = '';
// value: string = '';
// constructor(options: {
// id?: string;
// name?: string;
// key: string;
// value: string;
// }) {
// super();
// this.id = options.id ? options.id : '';
// this.name = options.name ? options.name : '';
// this.key = options.key;
// this.value = options.value;
// }
// // TODO: improve following fields
// static _postman_propertyAllowsMultipleValues: boolean = true;
// static _postman_propertyIndexKey: string = 'formData';
// // parse a form data string into an array of objects, where each object contains a key and a value.
// static parse(query: string): { key: string; value: string }[] {
// try {
// const keyValues = JSON.parse(query);
// return keyValues.filter((keyValue: object) => {
// if (!('key' in keyValue) || !('value' in keyValue)) {
// console.error('ignored some formdata as "key" or "value" is not found in it');
// return false;
// }
// return true;
// });
// } catch (e) {
// console.error(`failed to parse QueryParams: ${e.message}`);
// return [];
// }
// }
// valueOf() {
// return this.value;
// }
// }
export 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 interface UrlObject {
auth: {
username: string;
password: string;
} | undefined;
hash: string;
host: string[];
path: string[];
port: string;
protocol: string;
query: { key: string; value: string }[];
variables: { key: string; value: string }[];
}
export class Url extends PropertyBase {
kind: string = 'Url';
auth?: { username: string; password: string };
hash?: string;
host: string[];
path?: string[];
port?: string;
protocol?: string;
query: PropertyList<QueryParam>;
variables: VariableList<Variable>;
// TODO: user could pass anything
constructor(def: {
auth?: { username: string; password: string }; // TODO: should be related to RequestAuth
hash?: string;
host: string[];
path?: string[];
port?: string;
protocol: string;
query: PropertyList<QueryParam>;
variables: VariableList<Variable>;
} | string) {
super({ description: 'Url' });
if (typeof def === 'string') {
const urlObj = Url.parse(def);
if (urlObj) {
this.auth = urlObj.auth;
this.hash = urlObj.hash;
this.host = urlObj.host;
this.path = urlObj.path;
this.port = urlObj.port;
this.protocol = urlObj.protocol;
const queryList = urlObj.query ? urlObj.query.map(kvObj => new QueryParam(kvObj)) : [];
this.query = new PropertyList<QueryParam>(queryList);
const varList = urlObj.variables ?
urlObj.variables
.map((kvObj: { key: string; value: string }) => new Variable(kvObj)) :
[];
this.variables = new VariableList(undefined, varList);
} else {
throw Error(`url is invalid: ${def}`); // TODO:
}
} else {
this.auth = def.auth ? { username: def.auth.username, password: def.auth.password } : undefined;
this.hash = def.hash ? def.hash : '';
this.host = def.host;
this.path = def.path ? def.path : [];
this.port = def.port ? def.port : '';
this.protocol = def.protocol ? def.protocol : '';
this.query = def.query ? def.query : new PropertyList<QueryParam>([]);
this.variables = def.variables ? def.variables : new VariableList(undefined, new Array<Variable>());
}
}
static isUrl(obj: object) {
return 'kind' in obj && obj.kind === 'Url';
}
static parse(urlStr: string): UrlObject | undefined {
if (!URL.canParse(urlStr)) {
console.error(`invalid URL string ${urlStr}`);
return undefined;
}
const url = new URL(urlStr);
const query = Array.from(url.searchParams.entries())
.map((kv: [string, string]) => {
return { key: kv[0], value: kv[1] };
});
return {
auth: url.username !== '' ? { // TODO: make it compatible with RequestAuth
username: url.username,
password: url.password,
} : undefined,
hash: url.hash,
host: url.hostname.split('/'),
path: url.pathname.split('/'),
port: url.port,
protocol: url.protocol, // e.g. https:
query,
variables: [],
};
}
addQueryParams(params: { key: string; value: string }[] | string) {
let queryParams: { key: string; value: string }[];
if (typeof params === 'string') {
queryParams = QueryParam.parse(params);
} else {
queryParams = params;
}
queryParams.forEach((param: { key: string; value: string }) => {
this.query.append(new QueryParam({ key: param.key, value: param.value }));
});
}
getHost() {
return this.host.join('/');
}
getPath(unresolved?: boolean) {
const path = this.path ? this.path.join('/') : '';
if (unresolved) {
return '/' + path;
}
// TODO: handle variables
return path;
}
getPathWithQuery() {
const path = this.path ? this.path.join('/') : '';
const pathHasPrefix = path.startsWith('/') ? path : '/' + path;
const query = this.query
.map(param => `${param.key}=${param.value}`, {})
.join('&');
return `${pathHasPrefix}?${query}`;
}
getQueryString() {
return this.query
.map(queryParam => (`${queryParam.key}=${queryParam.key}`), {})
.join('&');
}
// TODO:
getRemote(forcePort?: boolean) {
const host = this.host.join('/');
if (forcePort) {
const port = this.protocol && (this.protocol === 'https:') ? 443 : 80; // TODO: support ws, gql, grpc
return `${host}:${port}`;
}
return this.port ? `${host}:${this.port}` : `${host}`;
}
removeQueryParams(params: QueryParam[] | string[] | string) {
if (typeof params === 'string') {
// it is a string
this.query = new PropertyList(
this.query.filter(queryParam => queryParam.key === params, {})
);
} else if (params.length > 0) {
let toBeRemoved: Set<string>;
if (typeof params[0] === 'string') {
// it is a string[]
toBeRemoved = new Set(params as string[]);
} else {
// it is a QueryParam[]
toBeRemoved = new Set(
(params as QueryParam[])
.map(param => param.key)
);
}
this.query = new PropertyList(
this.query.filter(queryParam => !toBeRemoved.has(queryParam.key), {})
);
} else {
console.error('failed to remove query params: unknown params type, only supports QueryParam[], string[] or string');
}
}
toString(forceProtocol?: boolean) {
const queryStr = this.query
.map(param => `${param.key}=${param.value}`, {})
.join('&');
const auth = this.auth ? `${this.auth.username}:${this.auth.password}@` : '';
if (forceProtocol) {
return `${this.protocol}//${auth}${this.host}:${this.port}${this.path}?${queryStr}#${this.hash}`; // TODO: improve auth
}
return `${auth}${this.host}:${this.port}${this.path}?${queryStr}#${this.hash}`;
}
update(url: string | object) {
// user could pass anything in script
if (typeof url === 'string') {
const urlObj = Url.parse(url);
if (urlObj) {
this.auth = urlObj.auth ? {
username: urlObj.auth.username,
password: urlObj.auth.password,
} : {
username: '',
password: '',
};
this.hash = urlObj.hash ? urlObj.hash : '';
this.host = urlObj.host ? urlObj.host : [];
this.path = urlObj.path ? urlObj.path : [];
this.port = urlObj.port ? urlObj.port : '';
this.protocol = urlObj.protocol ? urlObj.protocol : '';
this.query = urlObj.query ?
new PropertyList(urlObj.query.map(kv => new QueryParam(kv))) :
new PropertyList<QueryParam>([]);
// TODO: update variables
// this.variables = new VariableList(undefined, new Array<Variable>());
} else {
throw Error(`failed to parse url: ${url}`);
}
} else {
if ('auth' in url && typeof url.auth === 'object' && url.auth) {
if ('username' in url.auth
&& typeof url.auth.username === 'string'
&& 'password' in url.auth
&& typeof url.auth.password === 'string'
) {
this.auth = { username: url.auth.username, password: url.auth.password };
} else {
console.error('the auth field must have "username" and "password" fields');
}
}
if ('hash' in url && typeof url.hash === 'string') {
this.hash = url.hash;
} else {
this.hash = '';
}
if ('host' in url && Array.isArray(url.host)) {
const isStringArray = url.host.length > 0 ? typeof url.host[0] === 'string' : true;
if (isStringArray) {
this.host = url.host;
} else {
console.error('type of "host" is invalid');
}
} else {
this.host = [];
}
if ('path' in url && Array.isArray(url.path)) {
const isStringArray = url.path.length > 0 ? typeof url.path[0] === 'string' : true;
if (isStringArray) {
this.path = url.path;
} else {
console.error('type of "path" is invalid');
}
} else {
this.path = [];
}
this.port = 'port' in url && url.port && typeof url.port === 'string' ? url.port : '';
this.protocol = 'protocol' in url && url.protocol && typeof url.protocol === 'string' ? url.protocol : '';
if ('query' in url && Array.isArray(url.query)) {
const queryParams = url.query
.filter(obj => 'key' in obj && 'value' in obj)
.map(kv => new QueryParam(kv));
this.query = new PropertyList(queryParams);
}
// TODO: update variables
// this.variables = new VariableList(undefined, new Array<Variable>());
}
}
}
// UrlMatchPattern implements chrome extension match patterns: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns
export class UrlMatchPattern extends Property {
// scheme
scheme: 'http:' | 'https:' | '*' | 'file:';
// host
// About wildcard:
// If you use a wildcard in the host pattern
// it must be the first or only character, and it must be followed by a period (.) or forward slash (/).
host: string;
// path:
// Must contain at least a forward slash
// The slash by itself matches any path.
path: string;
private port: string;
// Special cases: https://developer.chrome.com/docs/extensions/develop/concepts/match-patterns#special
// "<all_urls>"
// "file:///"
// "http://localhost/*"
// It doesn't support match patterns for top Level domains (TLD).
constructor(pattern: string) {
super();
const patternObj = UrlMatchPattern.parseAndValidate(pattern);
this.scheme = patternObj.scheme;
this.host = patternObj.host.join('/');
this.path = patternObj.path.join('/');
this.port = patternObj.port;
}
static parseAndValidate(pattern: string): {
scheme: 'http:' | 'https:' | '*' | 'file:';
host: string[];
path: string[];
port: string;
} {
// TODO: validate the pattern
const urlObj = Url.parse(pattern);
if (!urlObj || urlObj.host.length === 0) {
throw Error(`match pattern (${pattern}) is invalid and failed to parse`);
}
if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:' && urlObj.protocol !== '*' && urlObj.protocol !== 'file:') {
throw Error(`scheme (${urlObj.protocol}) is invalid and failed to parse`);
}
return { scheme: urlObj.protocol, host: urlObj.host, path: urlObj.path, port: `${urlObj.port}` };
}
static readonly MATCH_ALL_URLS: string = '<all_urls>';
static pattern: string | undefined = undefined; // TODO: its usage is unknown
static readonly PROTOCOL_DELIMITER: string = '+';
// TODO: the url can not start with -
private readonly starRegPattern = '[a-zA-Z0-9\-]*';
getProtocols(): string[] {
switch (this.scheme) {
case 'http:':
return ['http'];
case 'https:':
return ['https'];
case '*':
return ['http', 'https'];
case 'file:':
return ['file'];
default:
throw `invalid scheme ${this.scheme}`;
}
}
test(urlStr: string) {
const urlObj = Url.parse(urlStr);
if (!urlObj) {
return false;
}
return this.testProtocol(urlObj.protocol)
&& this.testHost(urlObj.host.join('/'))
&& this.testPort(urlObj.port, urlObj.protocol)
&& this.testPath(urlObj.path.join('/'));
}
testHost(host: string) {
const hostRegPattern = new RegExp(this.host.replace('*', this.starRegPattern), 'ig');
return hostRegPattern.test(host);
}
testPath(path: string) {
const pathRegPattern = new RegExp(this.path.replace('*', this.starRegPattern), 'ig');
return pathRegPattern.test(path);
}
// TODO: it is confused to verify both port and protocol
// testPort verifies both port and protocol, but not the relationship between them
testPort(port: string, protocol: string) {
if (!this.testProtocol(protocol)) {
return false;
}
const portRegPattern = new RegExp(this.port.replace('*', this.starRegPattern), 'ig');
if (!portRegPattern.test(port)) {
return false;
}
return true;
}
testProtocol(protocol: string) {
switch (protocol) {
case 'http:':
return this.scheme === 'http:' || this.scheme === '*';
case 'https:':
return this.scheme === 'https:' || this.scheme === '*';
case '*':
return this.scheme === 'http:' || this.scheme === 'https:' || this.scheme === '*';
case 'file:':
return this.scheme === 'file:';
default:
throw `invalid scheme ${protocol}`;
}
}
toString() {
return `${this.scheme}//${this.host}${this.path}`;
}
update(pattern: string) {
const patternObj = UrlMatchPattern.parseAndValidate(pattern);
this.scheme = patternObj.scheme;
this.host = patternObj.host.join('/');
this.path = patternObj.path.join('/');
this.port = patternObj.port;
}
}
export class UrlMatchPatternList<T extends UrlMatchPattern> extends PropertyList<T> {
kind: string = 'UrlMatchPatternList';
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
super(populate);
this._parent = parent;
}
static isUrlMatchPatternList(obj: any) {
return 'kind' in obj && obj.kind === 'UrlMatchPatternList';
}
// TODO: unsupported yet
// toObject(excludeDisabledopt, nullable, caseSensitiveopt, nullable, multiValueopt, nullable, sanitizeKeysopt) → {Object}
test(urlStr: string) {
return this
.filter(matchPattern => matchPattern.test(urlStr), {})
.length > 0;
}
}

View File

@@ -1,59 +0,0 @@
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 ? def.type : 'Variable';
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<T extends Variable> extends PropertyList<T> {
kind: string = 'VariableList';
constructor(parent: PropertyList<T> | undefined, populate: T[]) {
super(populate);
this._parent = parent;
}
static isVariableList(obj: any) {
return 'kind' in obj && obj.kind === 'VariableList';
}
}