{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import EventEmitter from \"node:events\";\nimport tls, { type TLSSocket } from \"node:tls\";\n\nimport waitFor from \"wait-for-async\";\n\nimport InboundCallSession from \"./call-session/inbound.js\";\nimport CallSession from \"./call-session/index.js\";\nimport OutboundCallSession from \"./call-session/outbound.js\";\nimport Codec from \"./codec.js\";\nimport {\n  InboundMessage,\n  OutboundMessage,\n  RequestMessage,\n  ResponseMessage,\n} from \"./sip-message/index.js\";\nimport type { SoftPhoneOptions } from \"./types.js\";\nimport { branch, generateAuthorization, localKey, uuid } from \"./utils.js\";\n\nclass Softphone extends EventEmitter {\n  public sipInfo: SoftPhoneOptions;\n  public client: TLSSocket;\n  public codec: Codec;\n\n  public fakeDomain = `${uuid()}.invalid`;\n  public fakeEmail = `${uuid()}@${this.fakeDomain}`;\n\n  private intervalHandle?: NodeJS.Timeout;\n  private connected = false;\n\n  public constructor(sipInfo: SoftPhoneOptions) {\n    super();\n    if (sipInfo.codec === undefined) {\n      sipInfo.codec = \"OPUS/16000\";\n    }\n    this.codec = new Codec(sipInfo.codec);\n    this.sipInfo = sipInfo;\n    if (this.sipInfo.domain === undefined) {\n      this.sipInfo.domain = \"sip.ringcentral.com\";\n    }\n    if (this.sipInfo.outboundProxy === undefined) {\n      this.sipInfo.outboundProxy = \"sip10.ringcentral.com:5096\";\n    }\n    const tokens = this.sipInfo.outboundProxy!.split(\":\");\n    this.client = tls.connect(\n      {\n        host: tokens[0],\n        port: parseInt(tokens[1], 10),\n        rejectUnauthorized: !this.sipInfo.ignoreTlsCertErrors,\n      },\n      () => {\n        this.connected = true;\n      },\n    );\n    const tlsWrite = this.client.write.bind(this.client);\n    this.client.write = (message) => {\n      this.emit(\"outboundMessage\", message.toString());\n      return tlsWrite(message);\n    };\n\n    let cache = \"\";\n    this.client.on(\"data\", (data) => {\n      cache += data.toString(\"utf-8\");\n      if (!cache.endsWith(\"\\r\\n\")) {\n        return; // haven't received a complete message yet\n      }\n      // received two empty body messages\n      const tempMessages = cache\n        .split(\"\\r\\nContent-Length: 0\\r\\n\\r\\n\")\n        .filter((message) => message.trim() !== \"\");\n      cache = \"\";\n      for (let i = 0; i < tempMessages.length; i++) {\n        if (!tempMessages[i].includes(\"Content-Length: \")) {\n          tempMessages[i] = `${tempMessages[i]}\\r\\nContent-Length: 0`;\n        }\n      }\n      for (const message of tempMessages) {\n        this.emit(\"message\", InboundMessage.fromString(message));\n      }\n    });\n  }\n\n  private instanceId = uuid();\n  private registerCallId = uuid();\n\n  public async register() {\n    if (!this.connected) {\n      await waitFor({\n        interval: 100,\n        times: 100,\n        condition: () => this.connected,\n      });\n      if (!this.connected) {\n        throw new Error(\"Failed to register: connect to TLS timeout\");\n      }\n    }\n    const sipRegister = async () => {\n      const fromTag = uuid();\n      const requestMessage = new RequestMessage(\n        `REGISTER sip:${this.sipInfo.domain} SIP/2.0`,\n        {\n          Via: `SIP/2.0/TLS ${this.client.localAddress}:${this.client.localPort};rport;branch=${branch()};alias`,\n          \"Max-Forwards\": \"70\",\n          From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${fromTag}`,\n          To: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>`,\n          \"Call-ID\": this.registerCallId,\n          Contact: `<sip:${this.sipInfo.username}@${this.client.localAddress}:${this.client.localPort};transport=TLS;ob>;reg-id=1;+sip.instance=\"<urn:uuid:${this.instanceId}>\"`,\n          Expires: 3600,\n          Allow:\n            \"PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS\",\n        },\n      );\n      const inboundMessage = await this.send(requestMessage, true);\n      if (inboundMessage.subject.startsWith(\"SIP/2.0 200 \")) {\n        // sometimes the server will return 200 OK directly\n        return;\n      }\n      if (!inboundMessage.subject.startsWith(\"SIP/2.0 401 \")) {\n        throw new Error(`Failed to register: ${inboundMessage.subject}`);\n      }\n      const wwwAuth = inboundMessage.getHeader(\"Www-Authenticate\")!;\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      const message = await this.send(newMessage, true);\n      if (!message.subject.startsWith(\"SIP/2.0 200 \")) {\n        throw new Error(`Failed to register: ${message.subject}`);\n      }\n    };\n    await sipRegister();\n    this.intervalHandle = setInterval(\n      () => {\n        sipRegister().catch((error) => {\n          this.emit(\"registrationError\", error);\n        });\n      },\n      30 * 1000, // refresh registration every 30 seconds\n    );\n    this.on(\"message\", (inboundMessage) => {\n      if (!inboundMessage.subject.startsWith(\"INVITE sip:\")) {\n        return;\n      }\n      const outboundMessage = new OutboundMessage(\"SIP/2.0 100 Trying\", {\n        Via: inboundMessage.headers.Via,\n        \"Call-ID\": inboundMessage.getHeader(\"Call-ID\"),\n        From: inboundMessage.headers.From,\n        To: inboundMessage.headers.To,\n        CSeq: inboundMessage.headers.CSeq,\n        \"Content-Length\": \"0\",\n      });\n      this.send(outboundMessage);\n      this.emit(\"invite\", inboundMessage);\n    });\n  }\n\n  public enableDebugMode(\n    options = {\n      inboundPrefix: \"Receiving...\\n\",\n      outboundPrefix: \"Sending...\\n\",\n    },\n  ) {\n    this.on(\"message\", (message) => {\n      console.log(\n        `${options.inboundPrefix}(${new Date()})\\n${message.toString()}`,\n      );\n    });\n    this.on(\"outboundMessage\", (message) => {\n      console.log(`${options.outboundPrefix}(${new Date()})\\n${message}`);\n    });\n  }\n\n  public revoke() {\n    clearInterval(this.intervalHandle);\n    this.removeAllListeners();\n    this.client.removeAllListeners();\n    this.client.destroy();\n  }\n\n  public send(\n    message: OutboundMessage,\n    waitForReply?: true,\n  ): Promise<InboundMessage>;\n  public send(\n    message: OutboundMessage,\n    waitForReply?: false,\n  ): Promise<undefined>;\n  public send(message: OutboundMessage, waitForReply = false) {\n    this.client.write(message.toString());\n    if (!waitForReply) {\n      return Promise.resolve(undefined);\n    }\n    return new Promise<InboundMessage>((resolve) => {\n      const messageListerner = (inboundMessage: InboundMessage) => {\n        // \"12563 INVITE\" vs \"12563 ACK\"\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(\"message\", messageListerner);\n        resolve(inboundMessage);\n      };\n      this.on(\"message\", messageListerner);\n    });\n  }\n\n  public async answer(inviteMessage: InboundMessage) {\n    const inboundCallSession = new InboundCallSession(this, inviteMessage);\n    await inboundCallSession.answer();\n    return inboundCallSession;\n  }\n\n  // decline an inbound call\n  public async decline(inviteMessage: InboundMessage) {\n    const newMessage = new ResponseMessage(inviteMessage, 603);\n    await this.send(newMessage);\n  }\n\n  public async call(callee: string) {\n    const { socket, port } = await CallSession.createBoundSocket();\n    const offerSDP = `\nv=0\no=- ${Date.now()} 0 IN IP4 ${this.client.localAddress}\ns=rc-softphone-ts\nc=IN IP4 ${this.client.localAddress}\nt=0 0\nm=audio ${port} RTP/SAVP ${this.codec.id} 101\na=rtpmap:${this.codec.id} ${this.codec.name}\na=rtpmap:101 telephone-event/8000\na=fmtp:101 0-15\na=sendrecv\na=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${localKey}\n  `.trim();\n    const inviteMessage = new RequestMessage(\n      `INVITE sip:${callee}@${this.sipInfo.domain} SIP/2.0`,\n      {\n        Via: `SIP/2.0/TLS ${this.client.localAddress}:${this.client.localPort};rport;branch=${branch()};alias`,\n        \"Max-Forwards\": 70,\n        From: `<sip:${this.sipInfo.username}@${this.sipInfo.domain}>;tag=${uuid()}`,\n        To: `<sip:${callee}@${this.sipInfo.domain}>`,\n        Contact: ` <sip:${this.sipInfo.username}@${this.client.localAddress}:${this.client.localPort};transport=TLS;ob>`,\n        \"Call-ID\": uuid(),\n        Route: `<sip:${this.sipInfo.outboundProxy};transport=tls;lr>`,\n        Allow: `PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS`,\n        Supported: `replaces, 100rel, timer, norefersub`,\n        \"Session-Expires\": 1800,\n        \"Min-SE\": 90,\n        \"Content-Type\": \"application/sdp\",\n      },\n      offerSDP,\n    );\n    const inboundMessage = await this.send(inviteMessage, true);\n    const proxyAuthenticate = inboundMessage.getHeader(\"Proxy-Authenticate\")!;\n    const nonce = proxyAuthenticate.match(/, nonce=\"(.+?)\"/)![1];\n    const newMessage = inviteMessage.fork();\n    newMessage.headers[\"Proxy-Authorization\"] = generateAuthorization(\n      this.sipInfo,\n      nonce,\n      \"INVITE\",\n    );\n    const progressMessage = await this.send(newMessage, true);\n    const outboundCallSession = new OutboundCallSession(\n      this,\n      progressMessage,\n      socket,\n    );\n    outboundCallSession.sdp = offerSDP;\n    return outboundCallSession;\n  }\n}\n\nexport default Softphone;\n"],"mappings":"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"}