{"version":3,"file":"sip-client.mjs","names":[],"sources":["../src/sip-client.ts"],"sourcesContent":["import EventEmitter from \"./event-emitter.js\";\nimport RcMessage from \"./rc-message/rc-message.js\";\nimport InboundMessage from \"./sip-message/inbound.js\";\nimport type OutboundMessage from \"./sip-message/outbound/index.js\";\nimport RequestMessage from \"./sip-message/outbound/request.js\";\nimport ResponseMessage from \"./sip-message/outbound/response.js\";\nimport type { SipClient, SipClientOptions, SipInfo } from \"./types.js\";\nimport {\n  branch,\n  fakeDomain,\n  fakeEmail,\n  generateAuthorization,\n  uuid,\n} from \"./utils.js\";\n\nconst maxExpires = 60;\nexport class DefaultSipClient extends EventEmitter implements SipClient {\n  public disposed = false;\n  public wsc!: WebSocket;\n  public sipInfo: SipInfo;\n  public instanceId: string;\n  private debug: boolean;\n\n  private timeoutHandle!: ReturnType<typeof setTimeout>;\n\n  public constructor(options: SipClientOptions) {\n    super();\n    this.sipInfo = options.sipInfo;\n    this.instanceId = options.instanceId ?? this.sipInfo.authorizationId;\n    this.debug = options.debug ?? false;\n  }\n\n  public async start() {\n    await this.connect();\n    if (this.timeoutHandle) {\n      clearInterval(this.timeoutHandle);\n    }\n    await this.register(maxExpires);\n  }\n\n  private useBackupOutboundProxy = false;\n  public toggleBackupOutboundProxy(enabled = true) {\n    this.useBackupOutboundProxy = enabled;\n  }\n\n  public connect(): Promise<void> {\n    this.wsc = new WebSocket(\n      \"wss://\" +\n        (this.useBackupOutboundProxy\n          ? this.sipInfo.outboundProxyBackup\n          : this.sipInfo.outboundProxy),\n      \"sip\",\n    );\n    if (this.debug) {\n      const wscSend = this.wsc.send.bind(this.wsc);\n      this.wsc.send = (message) => {\n        console.log(`Sending...(${new Date()})\\n` + message);\n        return wscSend(message);\n      };\n    }\n\n    this.wsc.addEventListener(\"message\", async (event) => {\n      const inboundMessage = InboundMessage.fromString(event.data);\n      if (inboundMessage.subject.startsWith(\"MESSAGE sip:\")) {\n        const rcMessage = await RcMessage.fromXml(inboundMessage.body);\n        if (\n          rcMessage.body.Cln &&\n          rcMessage.body.Cln !== this.sipInfo.authorizationId\n        ) {\n          return; // the message is not for this instance\n        }\n      }\n      if (this.debug) {\n        console.log(`Receiving...(${new Date()})\\n` + event.data);\n      }\n      this.emit(\"inboundMessage\", inboundMessage);\n      if (\n        inboundMessage.subject.startsWith(\"MESSAGE sip:\") ||\n        inboundMessage.subject.startsWith(\"BYE sip:\") ||\n        inboundMessage.subject.startsWith(\"CANCEL sip:\") ||\n        inboundMessage.subject.startsWith(\"INFO sip:\") ||\n        inboundMessage.subject.startsWith(\"NOTIFY sip:\") ||\n        inboundMessage.subject.startsWith(\"UPDATE sip:\")\n      ) {\n        // Auto reply 200 OK to MESSAGE, BYE, CANCEL, INFO, NOTIFY\n        await this.reply(\n          new ResponseMessage(inboundMessage, { responseCode: 200 }),\n        );\n      }\n    });\n\n    return new Promise<void>((resolve, reject) => {\n      const openEventHandler = () => {\n        this.wsc.removeEventListener(\"open\", openEventHandler);\n        resolve();\n      };\n      this.wsc.addEventListener(\"open\", openEventHandler);\n      const errorEventHandler = (e: Event) => {\n        this.wsc.removeEventListener(\"error\", errorEventHandler);\n        reject(e);\n      };\n      this.wsc.addEventListener(\"error\", errorEventHandler);\n    });\n  }\n\n  public async dispose() {\n    this.disposed = true;\n    clearInterval(this.timeoutHandle);\n    this.removeAllListeners();\n    await this.unregister();\n    this.wsc.close();\n  }\n\n  public async register(expires: number) {\n    const requestMessage = new RequestMessage(\n      `REGISTER sip:${this.sipInfo.domain} SIP/2.0`,\n      {\n        \"Call-Id\": uuid(),\n        Contact: `<sip:${fakeEmail};transport=wss>;+sip.instance=\"<urn:uuid:${this.instanceId}>\";expires=${expires}`,\n        From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${uuid()}`,\n        To: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>`,\n        Via: `SIP/2.0/WSS ${fakeDomain};branch=${branch()}`,\n      },\n    );\n    // if cannot get response in 5 seconds, we close the connection\n    const closeHandle = setTimeout(() => this.wsc.close(), 5000);\n    let inboundMessage = await this.request(requestMessage);\n    clearTimeout(closeHandle);\n    const wwwAuth =\n      inboundMessage.headers[\"Www-Authenticate\"] ||\n      inboundMessage.headers[\"WWW-Authenticate\"];\n    if (wwwAuth) {\n      const nonce = wwwAuth.match(/, nonce=\"(.+?)\"/)![1];\n      const newMessage = requestMessage.fork();\n      newMessage.headers.Authorization = generateAuthorization(\n        this.sipInfo,\n        nonce,\n        \"REGISTER\",\n      );\n      inboundMessage = await this.request(newMessage);\n    } else if (inboundMessage.subject.startsWith(\"SIP/2.0 603 \")) {\n      throw new Error(\"Registration failed: \" + inboundMessage.subject);\n    }\n    if (expires > 0) {\n      // not for unregister\n      const serverExpires = Number(\n        inboundMessage.headers.Contact.match(/;expires=(\\d+)/)![1],\n      );\n      this.timeoutHandle = setTimeout(\n        () => {\n          this.register(expires);\n        },\n        (serverExpires - 3) * 1000, // 3 seconds before server expires\n      );\n    }\n  }\n  public async unregister() {\n    await this.register(0);\n  }\n\n  public async request(message: RequestMessage): Promise<InboundMessage> {\n    return await this.send(message, true);\n  }\n  public async reply(message: ResponseMessage): Promise<void> {\n    await this.send(message, false);\n  }\n  public send(\n    message: OutboundMessage,\n    waitForReply = false,\n  ): Promise<InboundMessage> {\n    this.wsc.send(message.toString());\n    this.emit(\"outboundMessage\", message);\n    if (!waitForReply) {\n      return new Promise<InboundMessage>((resolve) => {\n        resolve(new InboundMessage());\n      });\n    }\n    return new Promise<InboundMessage>((resolve) => {\n      const messageListerner = (inboundMessage: InboundMessage) => {\n        if (\n          inboundMessage.headers.CSeq.trim().split(/\\s+/)[0] !==\n          message.headers.CSeq.trim().split(/\\s+/)[0]\n        ) {\n          return;\n        }\n        if (inboundMessage.subject.startsWith(\"SIP/2.0 100 \")) {\n          return; // ignore\n        }\n        this.off(\"inboundMessage\", messageListerner);\n        resolve(inboundMessage);\n      };\n      this.on(\"inboundMessage\", messageListerner);\n    });\n  }\n}\n\n// this is for multiple instances with shared worker, dummy phones do not talk to SIP server at all\nexport class DummySipClient extends EventEmitter implements SipClient {\n  private static inboundMessage: InboundMessage = new InboundMessage();\n  public disposed = false;\n  public wsc!: WebSocket;\n  public async start() {}\n  public request() {\n    return Promise.resolve(DummySipClient.inboundMessage);\n  }\n  public async reply() {}\n  public dispose() {\n    this.disposed = true;\n    return Promise.resolve();\n  }\n}\n"],"mappings":";;;;;;;AAeA,MAAM,aAAa;AACnB,IAAa,mBAAb,cAAsC,aAAkC;CACtE,WAAkB;CAClB;CACA;CACA;CACA;CAEA;CAEA,YAAmB,SAA2B;AAC5C,SAAO;AACP,OAAK,UAAU,QAAQ;AACvB,OAAK,aAAa,QAAQ,cAAc,KAAK,QAAQ;AACrD,OAAK,QAAQ,QAAQ,SAAS;;CAGhC,MAAa,QAAQ;AACnB,QAAM,KAAK,SAAS;AACpB,MAAI,KAAK,cACP,eAAc,KAAK,cAAc;AAEnC,QAAM,KAAK,SAAS,WAAW;;CAGjC,yBAAiC;CACjC,0BAAiC,UAAU,MAAM;AAC/C,OAAK,yBAAyB;;CAGhC,UAAgC;AAC9B,OAAK,MAAM,IAAI,UACb,YACG,KAAK,yBACF,KAAK,QAAQ,sBACb,KAAK,QAAQ,gBACnB,MACD;AACD,MAAI,KAAK,OAAO;GACd,MAAM,UAAU,KAAK,IAAI,KAAK,KAAK,KAAK,IAAI;AAC5C,QAAK,IAAI,QAAQ,YAAY;AAC3B,YAAQ,IAAI,8BAAc,IAAI,MAAM,CAAC,OAAO,QAAQ;AACpD,WAAO,QAAQ,QAAQ;;;AAI3B,OAAK,IAAI,iBAAiB,WAAW,OAAO,UAAU;GACpD,MAAM,iBAAiB,eAAe,WAAW,MAAM,KAAK;AAC5D,OAAI,eAAe,QAAQ,WAAW,eAAe,EAAE;IACrD,MAAM,YAAY,MAAM,UAAU,QAAQ,eAAe,KAAK;AAC9D,QACE,UAAU,KAAK,OACf,UAAU,KAAK,QAAQ,KAAK,QAAQ,gBAEpC;;AAGJ,OAAI,KAAK,MACP,SAAQ,IAAI,gCAAgB,IAAI,MAAM,CAAC,OAAO,MAAM,KAAK;AAE3D,QAAK,KAAK,kBAAkB,eAAe;AAC3C,OACE,eAAe,QAAQ,WAAW,eAAe,IACjD,eAAe,QAAQ,WAAW,WAAW,IAC7C,eAAe,QAAQ,WAAW,cAAc,IAChD,eAAe,QAAQ,WAAW,YAAY,IAC9C,eAAe,QAAQ,WAAW,cAAc,IAChD,eAAe,QAAQ,WAAW,cAAc,CAGhD,OAAM,KAAK,MACT,IAAI,gBAAgB,gBAAgB,EAAE,cAAc,KAAK,CAAC,CAC3D;IAEH;AAEF,SAAO,IAAI,SAAe,SAAS,WAAW;GAC5C,MAAM,yBAAyB;AAC7B,SAAK,IAAI,oBAAoB,QAAQ,iBAAiB;AACtD,aAAS;;AAEX,QAAK,IAAI,iBAAiB,QAAQ,iBAAiB;GACnD,MAAM,qBAAqB,MAAa;AACtC,SAAK,IAAI,oBAAoB,SAAS,kBAAkB;AACxD,WAAO,EAAE;;AAEX,QAAK,IAAI,iBAAiB,SAAS,kBAAkB;IACrD;;CAGJ,MAAa,UAAU;AACrB,OAAK,WAAW;AAChB,gBAAc,KAAK,cAAc;AACjC,OAAK,oBAAoB;AACzB,QAAM,KAAK,YAAY;AACvB,OAAK,IAAI,OAAO;;CAGlB,MAAa,SAAS,SAAiB;EACrC,MAAM,iBAAiB,IAAI,eACzB,gBAAgB,KAAK,QAAQ,OAAO,WACpC;GACE,WAAW,MAAM;GACjB,SAAS,QAAQ,UAAU,2CAA2C,KAAK,WAAW,aAAa;GACnG,MAAM,QAAQ,KAAK,QAAQ,SAAS,GAAG,KAAK,QAAQ,OAAO,QAAQ,MAAM;GACzE,IAAI,QAAQ,KAAK,QAAQ,SAAS,GAAG,KAAK,QAAQ,OAAO;GACzD,KAAK,eAAe,WAAW,UAAU,QAAQ;GAClD,CACF;EAED,MAAM,cAAc,iBAAiB,KAAK,IAAI,OAAO,EAAE,IAAK;EAC5D,IAAI,iBAAiB,MAAM,KAAK,QAAQ,eAAe;AACvD,eAAa,YAAY;EACzB,MAAM,UACJ,eAAe,QAAQ,uBACvB,eAAe,QAAQ;AACzB,MAAI,SAAS;GACX,MAAM,QAAQ,QAAQ,MAAM,kBAAkB,CAAE;GAChD,MAAM,aAAa,eAAe,MAAM;AACxC,cAAW,QAAQ,gBAAgB,sBACjC,KAAK,SACL,OACA,WACD;AACD,oBAAiB,MAAM,KAAK,QAAQ,WAAW;aACtC,eAAe,QAAQ,WAAW,eAAe,CAC1D,OAAM,IAAI,MAAM,0BAA0B,eAAe,QAAQ;AAEnE,MAAI,UAAU,GAAG;GAEf,MAAM,gBAAgB,OACpB,eAAe,QAAQ,QAAQ,MAAM,iBAAiB,CAAE,GACzD;AACD,QAAK,gBAAgB,iBACb;AACJ,SAAK,SAAS,QAAQ;OAEvB,gBAAgB,KAAK,IACvB;;;CAGL,MAAa,aAAa;AACxB,QAAM,KAAK,SAAS,EAAE;;CAGxB,MAAa,QAAQ,SAAkD;AACrE,SAAO,MAAM,KAAK,KAAK,SAAS,KAAK;;CAEvC,MAAa,MAAM,SAAyC;AAC1D,QAAM,KAAK,KAAK,SAAS,MAAM;;CAEjC,KACE,SACA,eAAe,OACU;AACzB,OAAK,IAAI,KAAK,QAAQ,UAAU,CAAC;AACjC,OAAK,KAAK,mBAAmB,QAAQ;AACrC,MAAI,CAAC,aACH,QAAO,IAAI,SAAyB,YAAY;AAC9C,WAAQ,IAAI,gBAAgB,CAAC;IAC7B;AAEJ,SAAO,IAAI,SAAyB,YAAY;GAC9C,MAAM,oBAAoB,mBAAmC;AAC3D,QACE,eAAe,QAAQ,KAAK,MAAM,CAAC,MAAM,MAAM,CAAC,OAChD,QAAQ,QAAQ,KAAK,MAAM,CAAC,MAAM,MAAM,CAAC,GAEzC;AAEF,QAAI,eAAe,QAAQ,WAAW,eAAe,CACnD;AAEF,SAAK,IAAI,kBAAkB,iBAAiB;AAC5C,YAAQ,eAAe;;AAEzB,QAAK,GAAG,kBAAkB,iBAAiB;IAC3C;;;AAKN,IAAa,iBAAb,MAAa,uBAAuB,aAAkC;CACpE,OAAe,iBAAiC,IAAI,gBAAgB;CACpE,WAAkB;CAClB;CACA,MAAa,QAAQ;CACrB,UAAiB;AACf,SAAO,QAAQ,QAAQ,eAAe,eAAe;;CAEvD,MAAa,QAAQ;CACrB,UAAiB;AACf,OAAK,WAAW;AAChB,SAAO,QAAQ,SAAS"}