import { RouteDatagram } from "./datagram-routing.js"; import { Datagram, DatagramInitObject, Message, Request, Response, ResponseInit } from "../Independents/Datagram/datagram-types.js"; import { DatagramShort, isRequest, isResponse } from "../Independents/Datagram/datagram.js" import { Either, Right, Left } from "../Independents/util/Either.js"; import { Gateway, SpawningError } from "./Gateway.js"; import { HostMachine } from "./HostMachine.js"; import { Machine, MachineInit, SinglePortMachineInit } from "../Independents/Machine.js"; import { HostName, HostNameTemplate } from "../Independents/DNS/HostName.js"; import { OpenPromise } from "../Independents/util/OpenPromise.js"; import { RequestResponse } from "./GatewayProxy.js"; import { generateHostName, RandomString } from "../Independents/RememberableNames/random-host-name.js"; import { HostType, Resolve } from "../Independents/DNS/Resolve.js"; import { Subscription } from "../Independents/util/Subscription.js"; import { FocusedObject, key, Lens, lens } from "functional" import { THost, THostOwnershipRecord } from "../Independents/DNS/datagram-router.js"; import { CBurpastateWrapper, TBurpaState } from "./burpastate.js"; import { request } from "http"; export type NamingFn = (components: number, origin?: string) => string function assert(assertion: boolean) { if (!assertion) { throw new Error("assertion error"); } } const BurpaStateLens = lens(); export class HostExtension { public machine: Machine; pendingSentRequests: { [reqno: number]: RequestResponse } = {}; constructor(state: CBurpastateWrapper, burpaport: string[]) { this.machine = new HostMachine(state, burpaport); } } export interface IHost { get aliases2(): string[]; get machine(): Machine; set machine(machine: Machine); get childHosts(): { [key: string]: CHostOwnershipRecord }; addChild(name: string, secret: string, registred: boolean, gateway: string): CHostOwnershipRecord; allocateChildName(hostTemplate: HostNameTemplate, secret: string): Either; onReceiveDatagram(datagram: Datagram): Promise; } export const HostExtensions: { [hostid: string]: HostExtension } = {}; /** * Burpa hosts and burpa gateways are trees of named nodes. * Each node has a set of children with unique names amongst their siblings. * The naming function is different for gateways and hosts. Gateways have are using * person names, potentially followed by a number if there are many siblings. Hosts * have single, dual or tripple word names, potentially followed by a number. */ export abstract class Host extends FocusedObject implements IHost { _wrapper: CBurpastateWrapper | null = null; get hostId() { return this.pointer[key] as string; } get state() { if (!this._wrapper) { this._wrapper = new CBurpastateWrapper(this.whole); } return this._wrapper; } /** * Eeach request datagram is marked with a request number, unique * to the Transport. By providing the same request number in the response * datagram, requests and responses can be paired. */ get nextReqno() { return this.self.nextReqno; } set nextReqno(val: number) { this.self.nextReqno = val; } get childHosts() { let ret: { [key: string]: CHostOwnershipRecord } = {}; for (let key of Object.keys(this.self.owned)) { let ptr = this.pointer.owned[key]; ret[key] = new CHostOwnershipRecord(this.whole, ptr); } return ret; } get extension(): HostExtension { let e = HostExtensions[this.hostId]; if (!e) { e = new HostExtension(this.state, this.self.burpaport); HostExtensions[this.hostId] = e; } return e; } // set(lens: Lens, value: V) { // console.warn("SETTING HOST2", lens[key]); // if (lens[key] == "") { // this._nextReqno = value as unknown as number; // } // return super.set(lens, value); // } get machine(): Machine { return this.extension.machine; } set machine(machine: Machine) { this.extension.machine = machine; } get pendingSentRequests(): { [reqno: number]: RequestResponse } { return this.extension.pendingSentRequests; } set pendingSentRequests(p: { [reqno: number]: RequestResponse }) { this.extension.pendingSentRequests = p; } abstract addChild(name: string, secret: string, registred: boolean, gateway: string): CHostOwnershipRecord; hasAlias(name: string) { return this.whole.persistent.aliases[name] == this.hostId; } getInfo(): any { let entries = Object.entries(this.childHosts).map(e => [e[0], e[1].getInfo()]); let aliases = this.aliases2; //.map(e => e.name); console.log("HOST CHILDREN", aliases, entries, this.childHosts); return { aliases, children: Object.fromEntries(entries) } } async onReceiveDatagram(datagram: Datagram) { let d = datagram as any; this.state.log("Received", DatagramShort(datagram, true)); //console.log(`Received ${ DatagramShort(datagram) } from ${ datagram.from } at ${ this.alias.name }`); if (isResponse(datagram)) { let pending = this.pendingSentRequests[datagram.reqno]; if (!pending) { let msg = `Received ${DatagramShort(datagram)} without pending request.Reqno is ${datagram.reqno}`; this.state.log("Error", msg); console.error(msg); return; } // pending.response.resolve(datagram); pending.subscription.setCurrent(datagram); if (datagram.final) { //console.error("REMOVING PENDING", datagram.reqno); delete this.pendingSentRequests[datagram.reqno]; } } else if (isRequest(datagram)) { let response = this.machine.tryRequest(datagram); if (response == null) { let h = this.state.getHost502("Reject", (request) => { return { status: 502, body: `The host ${datagram.to} does not listen to burpa port "${request.port}".` }; }) response = h.machine.tryRequest(datagram); } if (response) { for await (let r of response) { this.state.log("Publish", `In response to:${datagram.method} ${datagram.path} result:${DatagramShort(r)}`); this.sendDatagram(r); } } else { throw new Error(`Cannot deliver ${DatagramShort(datagram)} to ${datagram.to}`); } } else { this.machine.tryMessage(datagram as Message); } } getGateway(): Gateway { return this.state.LocalGateway(); } sendRequest(request: Request) { let sender: HostName = this.alias; request.from = sender.name; this.sendDatagram(request); let ret = this.getResponse(request.reqno) // ret.then((resp) => { // console.warn("RECEIVED RESPONSE", resp) // }) return ret; } sendDatagram(datagram: Datagram) { if (isRequest(datagram)) { this.reportSentRequest(datagram) } else { this.reportSentDatagram(datagram); } RouteDatagram(new CBurpastateWrapper(this.whole), datagram); } getResponse(reqno: number): Subscription { let pending = this.pendingSentRequests[reqno]; if (!pending) { throw new Error(`Have not issued request ${reqno}.Issued requests are[${Object.keys(this.pendingSentRequests).join(",")}].`) } return pending.subscription; } reportSentDatagram(datagram: Datagram): void { //console.warn("sending datagram", DatagramShort(datagram)); this.state.log("Posted", DatagramShort(datagram, true)); } reportSentRequest(request: Request): Subscription { let reqno = this.nextReqno; this.nextReqno++; // let n = this.get(this.pointer.nextReqno2); // this.set(this.pointer.nextReqno2, n); request.reqno = reqno; //let response = new OpenPromise() let psr: RequestResponse = { request: { from: request.from, to: request.to, reqno: request.reqno, method: request.method, port: request.port, headers: request.headers, path: request.path }, subscription: new Subscription() }; // response.then((resp) => { // psr.subscription.setCurrent(resp); // }) this.pendingSentRequests[reqno] = psr; //console.log(`Making note of sent request ${ reqno }.Pending requests are[${ Object.keys(this.pendingSentRequests).join(",") }]`) this.reportSentDatagram(request); return psr.subscription; } get publicAlias(): HostName | null { let a = this.alias; if (a.name.startsWith("gw55") || a.name.startsWith("vg66")) { return null; //throw err("no public alias") } return a; } get alias(): HostName { let aliases = this.aliases2; for (let a of aliases) { if (!a.startsWith("gw55") && !a.startsWith("vg66")) { return new HostName(a); } } for (let a of aliases) { if (a.startsWith("gw55") || a.startsWith("vg66")) { if (a.indexOf("-") !== -1) { return new HostName(a); } } } for (let a of aliases) { if (a.startsWith("gw55") || a.startsWith("vg66")) { return new HostName(a); } } for (let a of aliases) { return new HostName(a); } throw "Impossible" // return new HostName("notnamed"); } public get aliases2() { return Object.entries(this.whole.persistent.aliases).filter(e => e[1] == this.hostId).map(e => e[0]); } allocateChildName(hostTemplate: HostNameTemplate, secret: string): Either { //console.log("ALLOCATING NAME ACCORDING TO TEMPLATE", hostTemplate.nameTemplate, hostTemplate.name); let name = hostTemplate.name ?? this.allocateGeneratedChildName(hostTemplate, secret); // if (secret == "") { // return Left(new SpawningError(name, `Secret cannot be empty for fixed host names`)); // } if (!this.canUseName(name, secret)) { return Left(new SpawningError(name, `The name is already used and the secrets does not match.`)); } return Right(name); }; getNamingFunction(): NamingFn { return generateHostName; } allocateGeneratedChildName(hostTemplate: HostNameTemplate, secret: string): string { let cnt = 0; let host: string; let namingFn = this.getNamingFunction(); let origin = hostTemplate.components[0].generationOrigin as string; if (hostTemplate.components[0].serialPosition != -1) { let serial = this.state.getSerialId(); origin = origin.replace("$", serial); } do { let hostCount = Object.keys(this.childHosts).length; this.collectGarbage(); // Close hosts that are not used (maybe frees host name) let somename = origin ? namingFn(cnt.toString().length, `${cnt}-${origin}`) : namingFn((hostCount > 2) ? ((hostCount > 20) ? ((hostCount > 1000) ? ((hostCount > 10000) ? 5 : 4) : 3) : 2) : 1); // if there are many hosts, use a three component name, otherwise use two components host = `${hostTemplate.leftOfGeneration}${somename}${hostTemplate.rightOfGeneration}`; cnt++; } while (!this.canUseName(host, secret)); //console.log(`Allocated name "${host}"`) return host; } collectGarbage() { // TODO! } canUseName(host: string, secret: string): boolean { let preexisting = this.childHosts[host]; return (!preexisting) || preexisting.self.secret == secret } getOwnershipRecord(host: string): CHostOwnershipRecord | null { let rec = this.self.owned[host]; if (!rec) { return null; } return new CHostOwnershipRecord(this.whole, this.pointer.owned[host]); } addToInbox(type: "vagrant" | "gateway", datagram: Datagram) { let kid = this.getOwnershipRecord(datagram.to); if (!kid) { kid = this._addChild(type, datagram.to, "", false) } //console.error(`Added to inbox of ${this.hostId} for kid ${datagram.to}`); datagram.trail = []; let inbox = this.whole.transient.inboxes[kid.self.name]; if (!inbox) { inbox = []; this.whole.transient.inboxes[kid.self.name] = inbox; } inbox.push(datagram); } _addChild(type: "vagrant" | "gateway", name: string, secret: string, registred: boolean, gateway?: string) { let kid = this.getOwnershipRecord(name); if (kid) { kid.self.type = type; kid.self.gateway = gateway; kid.self.registred = registred; kid.self.secret = secret; } else { let rec = { type, name, registred, secret, gateway, remoteInbox: [] } this.self.owned[name] = rec; } kid = this.getOwnershipRecord(name); // this.whole.transient.todos.push(() => { // kid!.emptyInbox(); // }) return kid!; } } /** * The master record for a host or a gateway. This is the source of truth * for all hosts and gateways including the roots. */ export class CHostOwnershipRecord extends FocusedObject { get state() { return new CBurpastateWrapper(this.whole); } getInfo(): any { return { ...this.getPublicInfo(), secret: this.self.secret }; } getPublicInfo() { return { type: this.self.type, name: this.self.name } } getResolve() { let resolve = Resolve.fromHostName(new HostName(this.self.name)); resolve.type = this.self.type; resolve.gateway = this.self.gateway ?? null; return resolve; } emptyInbox() { this.state.log("SendInbox", `${this.getInfo()}`) //console.log("EMPTYING INBOX", this.self.inbox); let inbox = this.whole.transient.inboxes[this.self.name]; for (let d of inbox) { RouteDatagram(new CBurpastateWrapper(this.whole), d); } this.whole.transient.inboxes[this.self.name] = []; } } export class TemporaryHost extends Host { // constructor(state: TBurpaState, pointer: Lens, public responseFn: (request: Request) => ResponseInit | Promise) { // super(state, pointer, "temphost", "burpasys") // let m: SinglePortMachineInit = { // onRequest: responseFn // } // this.machine = new Machine({ // ports: { // "burpasys": m, // "default": m // } // }); // } action: string = "Reject"; addChild(name: string, secret: string, registred: boolean, gateway: string): CHostOwnershipRecord { throw "error" } async onReceiveDatagram(datagram: Datagram): Promise { this.state.log(this.action, DatagramShort(datagram, true)); super.onReceiveDatagram(datagram); } getNamingFunction(): NamingFn { throw "error" } } export class Vagrant extends Host { addChild(name: string, secret: string, registred: boolean, gateway: string) { return this._addChild("vagrant", name, secret, registred, gateway); } }