diff --git a/src/Grammar.d.ts b/src/Grammar.d.ts index 6f69b8c0..eab44295 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 d2fc29a4..1ac477d5 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 eee4e5c2..d1997448 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" \r\n\r\n'; const ua = new UA(config as ConstructorParameters[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');