229 lines
6.0 KiB
Markdown
229 lines
6.0 KiB
Markdown
|
|
# ts/sip — SIP Protocol Library
|
||
|
|
|
||
|
|
A zero-dependency SIP (Session Initiation Protocol) library for Deno / Node.
|
||
|
|
Provides parsing, construction, mutation, and dialog management for SIP
|
||
|
|
messages, plus helpers for SDP bodies and URI rewriting.
|
||
|
|
|
||
|
|
## Modules
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `message.ts` | `SipMessage` class — parse, inspect, mutate, serialize |
|
||
|
|
| `dialog.ts` | `SipDialog` class — track dialog state, build in-dialog requests |
|
||
|
|
| `helpers.ts` | ID generators, codec registry, SDP builder/parser |
|
||
|
|
| `rewrite.ts` | SIP URI and SDP body rewriting |
|
||
|
|
| `types.ts` | Shared types (`IEndpoint`) |
|
||
|
|
| `index.ts` | Barrel re-export |
|
||
|
|
|
||
|
|
## Quick Start
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import {
|
||
|
|
SipMessage,
|
||
|
|
SipDialog,
|
||
|
|
buildSdp,
|
||
|
|
parseSdpEndpoint,
|
||
|
|
rewriteSipUri,
|
||
|
|
rewriteSdp,
|
||
|
|
generateCallId,
|
||
|
|
generateTag,
|
||
|
|
generateBranch,
|
||
|
|
} from './sip/index.ts';
|
||
|
|
```
|
||
|
|
|
||
|
|
## SipMessage
|
||
|
|
|
||
|
|
### Parsing
|
||
|
|
|
||
|
|
```ts
|
||
|
|
import { Buffer } from 'node:buffer';
|
||
|
|
|
||
|
|
const raw = Buffer.from(
|
||
|
|
'INVITE sip:user@example.com SIP/2.0\r\n' +
|
||
|
|
'Via: SIP/2.0/UDP 10.0.0.1:5060;branch=z9hG4bK776\r\n' +
|
||
|
|
'From: <sip:alice@example.com>;tag=abc\r\n' +
|
||
|
|
'To: <sip:bob@example.com>\r\n' +
|
||
|
|
'Call-ID: a84b4c76e66710@10.0.0.1\r\n' +
|
||
|
|
'CSeq: 1 INVITE\r\n' +
|
||
|
|
'Content-Length: 0\r\n\r\n'
|
||
|
|
);
|
||
|
|
|
||
|
|
const msg = SipMessage.parse(raw);
|
||
|
|
// msg.method → "INVITE"
|
||
|
|
// msg.isRequest → true
|
||
|
|
// msg.callId → "a84b4c76e66710@10.0.0.1"
|
||
|
|
// msg.cseqMethod → "INVITE"
|
||
|
|
// msg.isDialogEstablishing → true
|
||
|
|
```
|
||
|
|
|
||
|
|
### Fluent mutation
|
||
|
|
|
||
|
|
All setter methods return `this` for chaining:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const buf = SipMessage.parse(raw)!
|
||
|
|
.setHeader('Contact', '<sip:proxy@192.168.1.1:5070>')
|
||
|
|
.prependHeader('Record-Route', '<sip:192.168.1.1:5070;lr>')
|
||
|
|
.updateContentLength()
|
||
|
|
.serialize();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Building requests from scratch
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const invite = SipMessage.createRequest('INVITE', 'sip:+4930123@voip.example.com', {
|
||
|
|
via: { host: '192.168.5.66', port: 5070 },
|
||
|
|
from: { uri: 'sip:alice@example.com', displayName: 'Alice' },
|
||
|
|
to: { uri: 'sip:+4930123@voip.example.com' },
|
||
|
|
contact: '<sip:192.168.5.66:5070>',
|
||
|
|
body: sdpBody,
|
||
|
|
contentType: 'application/sdp',
|
||
|
|
});
|
||
|
|
// Call-ID, From tag, Via branch are auto-generated if not provided.
|
||
|
|
```
|
||
|
|
|
||
|
|
### Building responses
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const ok = SipMessage.createResponse(200, 'OK', incomingInvite, {
|
||
|
|
toTag: generateTag(),
|
||
|
|
contact: '<sip:192.168.5.66:5070>',
|
||
|
|
body: answerSdp,
|
||
|
|
contentType: 'application/sdp',
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Inspectors
|
||
|
|
|
||
|
|
| Property | Type | Description |
|
||
|
|
|----------|------|-------------|
|
||
|
|
| `isRequest` | `boolean` | True for requests (INVITE, BYE, ...) |
|
||
|
|
| `isResponse` | `boolean` | True for responses (SIP/2.0 200 OK, ...) |
|
||
|
|
| `method` | `string \| null` | Request method or null |
|
||
|
|
| `statusCode` | `number \| null` | Response status code or null |
|
||
|
|
| `callId` | `string` | Call-ID header value |
|
||
|
|
| `cseqMethod` | `string \| null` | Method from CSeq header |
|
||
|
|
| `requestUri` | `string \| null` | Request-URI (second token of start line) |
|
||
|
|
| `isDialogEstablishing` | `boolean` | INVITE, SUBSCRIBE, REFER, NOTIFY, UPDATE |
|
||
|
|
| `hasSdpBody` | `boolean` | Body present with Content-Type: application/sdp |
|
||
|
|
|
||
|
|
### Static helpers
|
||
|
|
|
||
|
|
```ts
|
||
|
|
SipMessage.extractTag('<sip:alice@x.com>;tag=abc') // → "abc"
|
||
|
|
SipMessage.extractUri('"Alice" <sip:alice@x.com>') // → "sip:alice@x.com"
|
||
|
|
```
|
||
|
|
|
||
|
|
## SipDialog
|
||
|
|
|
||
|
|
Tracks dialog state per RFC 3261 §12. A dialog is created from a
|
||
|
|
dialog-establishing request and updated as responses arrive.
|
||
|
|
|
||
|
|
### UAC (caller) side
|
||
|
|
|
||
|
|
```ts
|
||
|
|
// 1. Build and send INVITE
|
||
|
|
const invite = SipMessage.createRequest('INVITE', destUri, { ... });
|
||
|
|
const dialog = SipDialog.fromUacInvite(invite, '192.168.5.66', 5070);
|
||
|
|
|
||
|
|
// 2. Process responses as they arrive
|
||
|
|
dialog.processResponse(trying100); // state stays 'early'
|
||
|
|
dialog.processResponse(ringing180); // state stays 'early', remoteTag learned
|
||
|
|
dialog.processResponse(ok200); // state → 'confirmed'
|
||
|
|
|
||
|
|
// 3. ACK the 200
|
||
|
|
const ack = dialog.createAck();
|
||
|
|
|
||
|
|
// 4. In-dialog requests
|
||
|
|
const bye = dialog.createRequest('BYE');
|
||
|
|
dialog.terminate();
|
||
|
|
```
|
||
|
|
|
||
|
|
### UAS (callee) side
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const dialog = SipDialog.fromUasInvite(incomingInvite, generateTag(), localHost, localPort);
|
||
|
|
```
|
||
|
|
|
||
|
|
### CANCEL (before answer)
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const cancel = dialog.createCancel(originalInvite);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Dialog states
|
||
|
|
|
||
|
|
`'early'` → `'confirmed'` → `'terminated'`
|
||
|
|
|
||
|
|
## Helpers
|
||
|
|
|
||
|
|
### ID generation
|
||
|
|
|
||
|
|
```ts
|
||
|
|
generateCallId() // → "a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5"
|
||
|
|
generateCallId('example.com') // → "a3f8b2c1...@example.com"
|
||
|
|
generateTag() // → "1a2b3c4d5e6f7a8b"
|
||
|
|
generateBranch() // → "z9hG4bK-1a2b3c4d5e6f7a8b"
|
||
|
|
```
|
||
|
|
|
||
|
|
### SDP builder
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const sdp = buildSdp({
|
||
|
|
ip: '192.168.5.66',
|
||
|
|
port: 20000,
|
||
|
|
payloadTypes: [9, 0, 8, 101], // G.722, PCMU, PCMA, telephone-event
|
||
|
|
direction: 'sendrecv',
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### SDP parser
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const ep = parseSdpEndpoint(sdpBody);
|
||
|
|
// → { address: '10.0.0.1', port: 20000 } or null
|
||
|
|
```
|
||
|
|
|
||
|
|
### Codec names
|
||
|
|
|
||
|
|
```ts
|
||
|
|
codecName(9) // → "G722/8000"
|
||
|
|
codecName(0) // → "PCMU/8000"
|
||
|
|
codecName(101) // → "telephone-event/8000"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Rewriting
|
||
|
|
|
||
|
|
### SIP URI
|
||
|
|
|
||
|
|
Replaces the host:port in all `sip:` / `sips:` URIs found in a header value:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
rewriteSipUri('<sip:user@10.0.0.1:5060>', '203.0.113.1', 5070)
|
||
|
|
// → '<sip:user@203.0.113.1:5070>'
|
||
|
|
```
|
||
|
|
|
||
|
|
### SDP body
|
||
|
|
|
||
|
|
Rewrites the connection address and audio media port, returning the original
|
||
|
|
endpoint that was replaced:
|
||
|
|
|
||
|
|
```ts
|
||
|
|
const { body, original } = rewriteSdp(sdpBody, '203.0.113.1', 20000);
|
||
|
|
// original → { address: '10.0.0.1', port: 8000 }
|
||
|
|
```
|
||
|
|
|
||
|
|
## Architecture Notes
|
||
|
|
|
||
|
|
This library is intentionally low-level — it operates on individual messages
|
||
|
|
and dialogs rather than providing a full SIP stack with transport and
|
||
|
|
transaction layers. This makes it suitable for building:
|
||
|
|
|
||
|
|
- **SIP proxies** — parse, rewrite headers/SDP, serialize, forward
|
||
|
|
- **B2BUA (back-to-back user agents)** — manage two dialogs, bridge media
|
||
|
|
- **SIP testing tools** — craft and send arbitrary messages
|
||
|
|
- **Protocol analyzers** — parse and inspect SIP traffic
|
||
|
|
|
||
|
|
The library does not manage sockets, timers, or retransmissions — those
|
||
|
|
concerns belong to the application layer.
|