let HOST_DELIMETER = '-'; let NETWORK_HOST_DELIMETER = '.'; //let WORD_DELIMETER = ""; let STAR = "*"; let CURLY_BRACE_START = "{"; let CURLY_BRACE_END = "}"; let PORT_DELIMETER = ':'; // let BRACKET_START = "["; // let BRACKET_END = "]"; //let CAMEL_CASE = true; type GenarationType = "none" | "no-origin" | "origin" | "auto-origin"; export type DnsType = "regular" | "dashed"; export type Name = { name: string, dns: DnsType }; export class HostComponent { constructor(public name: string, public dns: DnsType, public key: string, public generatedStart: number, public generatedEnd: number, public generatedType: string, public generationContext: string | null, public generationOrigin: string | null, public serialPosition: number ) { } combineWithParent(parent: HostComponent) { let delimeter = (parent.dns == "dashed") ? HOST_DELIMETER : NETWORK_HOST_DELIMETER; return `${this.name}${delimeter}${parent.name}` } } /** * Host templates are used to allocated new host names or store existing ones. * Host names can be allocated by their parent host using name generators. * * Virtual host names are seperated by "-" where networked host names are seperated by ".". * The reason for this is that https (ssl) certificates does not allow for deep wildcards such * that nested random host names is avoided. For example foo.bar.mydomain.com requires that * the name "bar" is known as a fixed name in the certificated whereas "foo" can be a wildcard. * The certificate may certify *.bar.mydomain.com but not *.*.mydomain.com. The burpa network * having deep nested hosts looks like "foo-bar.mycomain.com" rather than "foo.bar.mydomain.com". * Thus, "-" is not allowed in host names unless they are also reflected in the network DNS. * * A name generator is employed using the wildcard "*" character or using braces/brackets. * Curly braces are used to generate names that are easy to remember (like the name "dog") * based on an `origin` string. If the same origin string is provided to the name generator, * the same name will be returned provided that that name is available. If it is not available, * a second name is provided that is the same as previously provided second names (and so forth). * * By storing the origin in cookies or local storages or generating it from an unique identifier * of the device it is likely that a device will receive the same host name as it may have had * in the past. * * The bracket builds on the origin idea by providing a default origin based on the implementation. * For example, on browsers a random number stored in the local storage is used and on iOS devices * the vendor device id is used as the origin. To be able to create multiple origins that are stable, * the bracket allows a context for the origin to be used. The context can be thought of as the origin * of the origin. For example lets assume that the user spawns the two hosts * "[mycontext].burpa.net" and "[myothercontext].burpa.net" on a device * with the serial id "12345". The orgin will then become "12345_mycontext" for the first host and * "12345_myothercontext" for the second. The parent host "burpa.net" will then stably provide the * host names "dog.burpa.net" and "cat.burpa.net" if these names are available. * * Examples: * "{12345}.stockholm.burpa.net" * "[userbrowser]-acmecompany.heads.com" * "*.stockholm.burpa.net" * "foo*bar.polyjuice.com" */ export abstract class HostNameBase { port: string = "80"; nameTemplate: string; components: HostComponent[] = []; get parent(): string { return HostNameBase.reconstruct(this.components.slice(1), this.port); } isLocal() { let root = this.components[this.components.length - 1].name; return root.startsWith("vg66") || root.startsWith("gw55"); } isNameChar(char: string) { let code = char.charCodeAt(0); return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57); } isNumberChar(char: string) { let code = char.charCodeAt(0); return (code >= 48 && code <= 57); } isDownstreamOf(parent: HostNameTemplate): boolean { let ti = this.components.length - 1; for (let pi = parent.components.length - 1; pi >= 0; pi--) { if (ti < 0 || parent.components[pi].name != this.components[ti].name) { return false; } ti--; } return true; } // static commonRoot(a: HostNameBase, b: HostNameBase): HostNameBase { // let common = 0; // let ai = a.components.length - 1; // let bi = b.components.length - 1; // if (a.components.length == 0 || b.components.length == 0) { // return new HostName(""); // } // while (true) { // if (ai >= 0 && bi >= 0 && a.components[ai].name == b.components[bi].name) { // common++; // ai--; // bi--; // } // else { // break; // } // } // let arr = takeRight(a.components, common); // return HostName.fromComponents(arr); // } // static fromComponents(arr: HostComponent[], port: string) { // return new HostName(HostName.reconstruct(arr, port)); // } static reconstruct(components: HostComponent[], port: string) { let dashed = components.filter((n) => n.dns == "dashed").map(c => c.name); let regular = components.filter((n) => n.dns == "regular").map(c => c.name); let str = (dashed.length > 0) ? (regular.length > 0 ? `${dashed.join("-")}-${regular.join(".")}` : dashed.join("-")) : regular.join("."); return `${str}${port == "80" ? "" : `:${port}`}`; } getRoot(rootHostname: HostName, extra: number) { if (!this.isDownstreamOf(rootHostname) && this.name !== rootHostname.name) { throw new Error(`Internal error`); } let arr = takeRight(this.components, rootHostname.components.length + extra); return new HostName(HostNameBase.reconstruct(arr, this.port)); // let rootCount = rootHostname.components.length + 1; // console.log("LENGTH", this.components.length, "EXTRA", extra, "ROOT COUNT", rootCount); // let arr = this.components.slice(this.components.length - (extra + rootCount)); // console.log(arr.map(e => e.name)); // return new HostNameTemplate(HostNameTemplate.reconstruct(arr)); } get name(): string | null { if (this.isFixed) { return this.nameTemplate; } else { return null; } } get first(): HostComponent { return this.components[0]; } get hosts(): Name[] { return this.components.map((e) => { return { name: e.name, dns: e.dns }; }); } constructor(nameTemplate: string) { this.nameTemplate = nameTemplate; let pos = 0; function peekChar() { return nameTemplate.charAt(pos); } let parseOrigin = (): { origin: string, generatedEnd: number, serialPosition: number } => { pos++; let endChar = CURLY_BRACE_END; let done = false; let start = pos; let error = false; let serialPosition = -1; let generatedEnd = -1; while (true) { let char = peekChar(); switch (char) { case "": case "$": serialPosition = pos; break; case HOST_DELIMETER: case NETWORK_HOST_DELIMETER: error = true; done = true; break; case endChar: generatedEnd = pos; done = true; break; } if (done) { break; } pos++; } if (error) { console.warn(`Expected closing "${endChar}" parsing ${nameTemplate}`) } return { origin: nameTemplate.substring(start, pos), generatedEnd, serialPosition }; } let readTemplatedName = (allowTemplate: boolean) => { let done = false; let start = pos; let generatedStart = -1; let generatedEnd = -1; let generatedType = "none"; let generationContext: string | null = null; let generationOrigin: string | null = null; let serialPosition = -1; let dns: "regular" | "dashed" = "regular"; while (!done) { let char = peekChar(); //console.log("char", char); let consumed = false; if (allowTemplate) { switch (char) { case STAR: //console.log("DETECTED STAR") consumed = true; generatedStart = pos; generatedEnd = pos; generatedType = "no-origin" break; case CURLY_BRACE_START: consumed = true; generatedStart = pos; generatedType = "origin" let org = parseOrigin(); generationOrigin = org.origin; generatedEnd = org.generatedEnd; serialPosition = org.serialPosition; // readUntil(CURLY_BRACE_END); //console.log("generatedstart", generatedStart, "end", generatedEnd, "origin", org.origin); break; // case BRACKET_START: // consumed = true; // generatedType = "auto-origin" // generatedStart = pos; // generationContext = readUntil(BRACKET_END); // break; } } if (consumed) { pos++; } else { switch (char) { case HOST_DELIMETER: dns = "dashed"; done = true; break; case "": case NETWORK_HOST_DELIMETER: done = true; break; case PORT_DELIMETER: done = true; break; default: // A-Z a-z 0-9 if (!this.isNameChar(char)) { let err = `Illegal character "${char}" in name template ${nameTemplate}`; console.warn(err) throw err; } pos++; } } } return new HostComponent(nameTemplate.substring(start, pos), dns, `${nameTemplate}!`.substring(start, pos + 1), generatedStart, generatedEnd, generatedType, generationContext, generationOrigin, serialPosition); } let readPort = () => { let done = false; let start = pos; while (!done) { pos++; let char = peekChar(); switch (char) { case "": done = true; break; default: // 0-9 if (!this.isNumberChar(char)) { let err = `Illegal character "${char}" in name template ${nameTemplate}`; console.warn(err) throw err; } } } return nameTemplate.substring(start, pos); } let first = readTemplatedName(true); if (first) { this.components.push(first); } pos++; while (peekChar() !== "") { let more = readTemplatedName(false); if (more) { this.components.push(more); } if (peekChar() == PORT_DELIMETER) { break; } pos++; } if (peekChar() == PORT_DELIMETER) { pos++; this.port = readPort(); } } resolveConstants(localHost: () => string, localGateway: string) { for (let c of this.components) { if (c.name == "local") { c.name = localHost(); // throw "found local!" } if (c.name == "localgw") { c.name = localGateway; // throw "found local!" } } } get isFixed() { return this.first.generatedType == "none" } get leftmost() { return this.components[0].name; } getHostName(root: string) { let left = this.components.filter((n) => n.dns == "dashed").map( (c) => c.name ).join(HOST_DELIMETER); // let nethosts = this.hosts.filter((n) => !n.roamable); // let right = nethosts.slice(nethosts.length - 2).map((c) => c.name).join(NETWORK_HOST_DELIMETER); return `${left}${NETWORK_HOST_DELIMETER}${root}`; } get reconstructed() { return HostNameTemplate.reconstruct(this.components, this.port); } get origin(): string | null { if (this.first.generatedType == "origin" || this.first.generatedType == "auto-origin") { return this.nameTemplate.slice(this.first.generatedStart, this.first.generatedEnd); } return null; } resolveWithOrigin(origin: string) { return new HostNameTemplate(`${this.leftOfGeneration}{${origin}}${this.rightOfGeneration}`); } get leftOfGeneration(): string { if (this.first.generatedStart == -1) { return ""; } return this.nameTemplate.substring(0, this.first.generatedStart); } get networkHost() { let arr = this.components.filter(e => e.dns == "regular"); arr = arr.splice(-(arr.length - 1)); return HostNameBase.reconstruct(arr, this.port); } get rightOfGeneration(): string { if (this.first.generatedStart == -1) { return ""; } return this.nameTemplate.substring(this.first.generatedEnd + 1); } } // function upperFirst(text: string): string { // return `${text.charAt(0).toUpperCase()}${text.substr(1)}`; // } // export function combineWords(...words: string[]): string { // let arr: string[]; // if (CAMEL_CASE) { // arr = words.map((word) => upperFirst(word), words) // } else { // arr = words; // } // return arr.join(WORD_DELIMETER); // } export class HostName extends HostNameBase { get name(): string { return this.reconstructed; } isGateway() { for (let c of this.components) { if (c.name.startsWith("gw55") || c.name == "gw") { return true; } } return false; } } export class HostNameTemplate extends HostNameBase { } function takeRight(arr: any[], count: number) { return arr.slice(arr.length - count); }