Files
siprouter/ts/sip/readme.md

229 lines
6.0 KiB
Markdown
Raw Permalink Normal View History

# 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.