Commit 74bede6 for jssip.net
commit 74bede62efb1ec1c750e2c72afb3b176e1e82d17
Author: José Luis Millán <jmillan@aliax.net>
Date: Wed Oct 21 11:03:11 2026 +0200
TS: Parser (#989)
diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts
index 6f69b8c..eab4429 100644
--- a/src/Grammar.d.ts
+++ b/src/Grammar.d.ts
@@ -1,4 +1,5 @@
import NameAddrHeader = require('./NameAddrHeader');
+import URI = require('./URI');
/**
* The result of `parse()`, whose shape depends on the start rule used.
@@ -75,6 +76,32 @@ export type EventHeader = {
*/
export type ReferToHeader = NameAddrHeader;
+/**
+ * Result of the `Request_Response` start rule when it matches a `Status_Line`.
+ */
+export type StatusLine = {
+ status_code: number;
+ reason_phrase: string;
+};
+
+/**
+ * Result of the `Request_Response` start rule when it matches a `Request_Line`.
+ *
+ * NOTE: `uri` is only set when the Request-URI is a SIP URI. `Request_URI` also
+ * accepts an `absoluteURI`, which the grammar does not turn into a `URI`.
+ */
+export type RequestLine = {
+ method: string;
+ uri?: URI;
+};
+
+/**
+ * Result of the `Request_Response` start rule. Which of the two it is can be
+ * told apart with `'status_code' in parsed`, as the grammar only creates the
+ * properties the matched rule sets.
+ */
+export type RequestResponse = StatusLine | RequestLine;
+
export function parse(input: string, startRule?: string): Grammar;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/Parser.js b/src/Parser.ts
similarity index 69%
rename from src/Parser.js
rename to src/Parser.ts
index d2fc29a..1ac477d 100644
--- a/src/Parser.js
+++ b/src/Parser.ts
@@ -1,35 +1,41 @@
-const { Logger } = require('./Logger');
-const Grammar = require('./Grammar');
-const { IncomingMessageBuilder } = require('./IncomingMessageBuilder');
+import { Logger } from './Logger';
+import * as Grammar from './Grammar';
+import { IncomingMessageBuilder } from './IncomingMessageBuilder';
+import type { IncomingRequest, IncomingResponse, UALike } from './SIPMessage';
const logger = new Logger('Parser');
/**
* Parse SIP Message
*/
-exports.parseMessage = (data, ua) => {
- let bodyStart;
+export const parseMessage = (
+ data: string,
+ ua: UALike
+): IncomingRequest | IncomingResponse | undefined => {
+ let bodyStart: number;
let headerEnd = data.indexOf('\r\n');
if (headerEnd === -1) {
logger.warn('parseMessage() | no CRLF found, not a SIP message');
- return;
+ return undefined;
}
// Parse first line. Check if it is a Request or a Reply.
const firstLine = data.substring(0, headerEnd);
- const parsed = Grammar.parse(firstLine, 'Request_Response');
+ const parsed: Grammar.RequestResponse | -1 = Grammar.parse(
+ firstLine,
+ 'Request_Response'
+ );
if (parsed === -1) {
logger.warn(
`parseMessage() | error parsing first line of SIP message: "${firstLine}"`
);
- return;
+ return undefined;
}
- const isResponse = Boolean(parsed.status_code);
const builder = new IncomingMessageBuilder();
let headerStart = headerEnd + 2;
@@ -37,6 +43,7 @@ exports.parseMessage = (data, ua) => {
/* Loop over every line in data. Detect the end of each header and parse
* it or simply add to the headers collection.
*/
+
while (true) {
headerEnd = getHeader(data, headerStart);
@@ -49,7 +56,7 @@ exports.parseMessage = (data, ua) => {
else if (headerEnd === -1) {
logger.warn('parseMessage() | malformed message');
- return;
+ return undefined;
}
const result = parseHeader(builder, data, headerStart, headerEnd);
@@ -57,7 +64,7 @@ exports.parseMessage = (data, ua) => {
if (result !== true) {
logger.warn('parseMessage() |', result.error);
- return;
+ return undefined;
}
headerStart = headerEnd + 2;
@@ -72,27 +79,38 @@ exports.parseMessage = (data, ua) => {
if (builder.hasHeader('content-length')) {
const contentLength = builder.getHeader('content-length');
- body = data.substr(bodyStart, contentLength);
+ body = data.substr(bodyStart, Number(contentLength));
} else {
body = data.substring(bodyStart);
}
- if (isResponse) {
+ if ('status_code' in parsed) {
return builder.buildResponse(
data,
parsed.status_code,
parsed.reason_phrase,
body
);
- } else {
- return builder.buildRequest(ua, data, parsed.method, parsed.uri, body);
}
+
+ /* RFC 3261 8.2.2.1. The grammar accepts any absoluteURI as Request-URI but
+ * only turns a SIP one into a `URI`, so anything else cannot be handled.
+ */
+ if (!parsed.uri) {
+ logger.warn(
+ `parseMessage() | unsupported Request-URI scheme in "${firstLine}"`
+ );
+
+ return undefined;
+ }
+
+ return builder.buildRequest(ua, data, parsed.method, parsed.uri, body);
};
/**
* Extract and parse every header of a SIP message.
*/
-function getHeader(data, headerStart) {
+function getHeader(data: string, headerStart: number): number {
// 'start' position of the header.
let start = headerStart;
// 'end' position of the header.
@@ -128,7 +146,12 @@ function getHeader(data, headerStart) {
return end;
}
-function parseHeader(builder, data, headerStart, headerEnd) {
+function parseHeader(
+ builder: IncomingMessageBuilder,
+ data: string,
+ headerStart: number,
+ headerEnd: number
+): true | { error: string } {
const hcolonIndex = data.indexOf(':', headerStart);
const headerName = data.substring(headerStart, hcolonIndex).trim();
const headerValue = data.substring(hcolonIndex + 1, headerEnd).trim();
diff --git a/src/test/test-parser.ts b/src/test/test-parser.ts
index eee4e5c..d199744 100644
--- a/src/test/test-parser.ts
+++ b/src/test/test-parser.ts
@@ -4,8 +4,8 @@ import * as consts from './include/consts';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const JsSIP = require('../JsSIP.js');
const { URI, NameAddrHeader, Grammar, WebSocketInterface, UA } = JsSIP;
-// eslint-disable-next-line @typescript-eslint/no-require-imports
-const Parser = require('../Parser.js');
+
+import * as Parser from '../Parser';
describe('parser', () => {
test('parse URI', () => {
@@ -429,14 +429,18 @@ P-Preferred-Identity: "Cullen Jennings" <sip:fluffy@cisco.com>\r\n\r\n';
const ua = new UA(config as ConstructorParameters<typeof UA>[0]);
const message = Parser.parseMessage(data, ua);
+ if (!message) {
+ throw new Error('parseMessage() did not return a message');
+ }
+
expect(message.hasHeader('P-Preferred-Identity')).toBe(true);
const pai = message.getHeader('P-Preferred-Identity');
const nameAddress = NameAddrHeader.parse(pai);
expect(nameAddress instanceof NameAddrHeader).toBeTruthy();
- expect(nameAddress!.uri.user).toBe('fluffy');
- expect(nameAddress!.uri.host).toBe('cisco.com');
+ expect(nameAddress.uri.user).toBe('fluffy');
+ expect(nameAddress.uri.host).toBe('cisco.com');
expect(message.hasHeader('Privacy')).toBe(true);
expect(message.getHeader('Privacy')).toBe('id');