sip.js
Version:
A SIP library for JavaScript
798 lines (797 loc) • 40.7 kB
JavaScript
import { C } from "../messages/methods/constants.js";
import { OutgoingRequestMessage } from "../messages/outgoing-request-message.js";
import { constructOutgoingResponse } from "../messages/outgoing-response.js";
import { InviteServerTransaction } from "../transactions/invite-server-transaction.js";
import { NonInviteClientTransaction } from "../transactions/non-invite-client-transaction.js";
import { TransactionState } from "../transactions/transaction-state.js";
import { InviteUserAgentClient } from "../user-agents/invite-user-agent-client.js";
import { InviteUserAgentServer } from "../user-agents/invite-user-agent-server.js";
import { MessageUserAgentClient } from "../user-agents/message-user-agent-client.js";
import { MessageUserAgentServer } from "../user-agents/message-user-agent-server.js";
import { NotifyUserAgentServer } from "../user-agents/notify-user-agent-server.js";
import { PublishUserAgentClient } from "../user-agents/publish-user-agent-client.js";
import { ReferUserAgentServer } from "../user-agents/refer-user-agent-server.js";
import { RegisterUserAgentClient } from "../user-agents/register-user-agent-client.js";
import { RegisterUserAgentServer } from "../user-agents/register-user-agent-server.js";
import { SubscribeUserAgentClient } from "../user-agents/subscribe-user-agent-client.js";
import { SubscribeUserAgentServer } from "../user-agents/subscribe-user-agent-server.js";
import { UserAgentClient } from "../user-agents/user-agent-client.js";
import { AllowedMethods } from "./allowed-methods.js";
/**
* This is ported from UA.C.ACCEPTED_BODY_TYPES.
* FIXME: TODO: Should be configurable/variable.
*/
const acceptedBodyTypes = ["application/sdp", "application/dtmf-relay"];
/**
* User Agent Core.
* @remarks
* Core designates the functions specific to a particular type
* of SIP entity, i.e., specific to either a stateful or stateless
* proxy, a user agent or registrar. All cores, except those for
* the stateless proxy, are transaction users.
* https://tools.ietf.org/html/rfc3261#section-6
*
* UAC Core: The set of processing functions required of a UAC that
* reside above the transaction and transport layers.
* https://tools.ietf.org/html/rfc3261#section-6
*
* UAS Core: The set of processing functions required at a UAS that
* resides above the transaction and transport layers.
* https://tools.ietf.org/html/rfc3261#section-6
* @public
*/
export class UserAgentCore {
/**
* Constructor.
* @param configuration - Configuration.
* @param delegate - Delegate.
*/
constructor(configuration, delegate = {}) {
/** UACs. */
this.userAgentClients = new Map();
/** UASs. */
this.userAgentServers = new Map();
this.configuration = configuration;
this.delegate = delegate;
this.dialogs = new Map();
this.subscribers = new Map();
this.logger = configuration.loggerFactory.getLogger("sip.user-agent-core");
}
/** Destructor. */
dispose() {
this.reset();
}
/** Reset. */
reset() {
this.dialogs.forEach((dialog) => dialog.dispose());
this.dialogs.clear();
this.subscribers.forEach((subscriber) => subscriber.dispose());
this.subscribers.clear();
this.userAgentClients.forEach((uac) => uac.dispose());
this.userAgentClients.clear();
this.userAgentServers.forEach((uac) => uac.dispose());
this.userAgentServers.clear();
}
/** Logger factory. */
get loggerFactory() {
return this.configuration.loggerFactory;
}
/** Transport. */
get transport() {
const transport = this.configuration.transportAccessor();
if (!transport) {
throw new Error("Transport undefined.");
}
return transport;
}
/**
* Send INVITE.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
invite(request, delegate) {
return new InviteUserAgentClient(this, request, delegate);
}
/**
* Send MESSAGE.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
message(request, delegate) {
return new MessageUserAgentClient(this, request, delegate);
}
/**
* Send PUBLISH.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
publish(request, delegate) {
return new PublishUserAgentClient(this, request, delegate);
}
/**
* Send REGISTER.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
register(request, delegate) {
return new RegisterUserAgentClient(this, request, delegate);
}
/**
* Send SUBSCRIBE.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
subscribe(request, delegate) {
return new SubscribeUserAgentClient(this, request, delegate);
}
/**
* Send a request.
* @param request - Outgoing request.
* @param delegate - Request delegate.
*/
request(request, delegate) {
return new UserAgentClient(NonInviteClientTransaction, this, request, delegate);
}
/**
* Outgoing request message factory function.
* @param method - Method.
* @param requestURI - Request-URI.
* @param fromURI - From URI.
* @param toURI - To URI.
* @param options - Request options.
* @param extraHeaders - Extra headers to add.
* @param body - Message body.
*/
makeOutgoingRequestMessage(method, requestURI, fromURI, toURI, options, extraHeaders, body) {
// default values from user agent configuration
const callIdPrefix = this.configuration.sipjsId;
const fromDisplayName = this.configuration.displayName;
const forceRport = this.configuration.viaForceRport;
const hackViaTcp = this.configuration.hackViaTcp;
const optionTags = this.configuration.supportedOptionTags.slice();
if (method === C.REGISTER) {
optionTags.push("path", "gruu");
}
if (method === C.INVITE && (this.configuration.contact.pubGruu || this.configuration.contact.tempGruu)) {
optionTags.push("gruu");
}
const routeSet = this.configuration.routeSet;
const userAgentString = this.configuration.userAgentHeaderFieldValue;
const viaHost = this.configuration.viaHost;
const defaultOptions = {
callIdPrefix,
forceRport,
fromDisplayName,
hackViaTcp,
optionTags,
routeSet,
userAgentString,
viaHost
};
// merge provided options with default options
const requestOptions = Object.assign(Object.assign({}, defaultOptions), options);
return new OutgoingRequestMessage(method, requestURI, fromURI, toURI, requestOptions, extraHeaders, body);
}
/**
* Handle an incoming request message from the transport.
* @param message - Incoming request message from transport layer.
*/
receiveIncomingRequestFromTransport(message) {
this.receiveRequestFromTransport(message);
}
/**
* Handle an incoming response message from the transport.
* @param message - Incoming response message from transport layer.
*/
receiveIncomingResponseFromTransport(message) {
this.receiveResponseFromTransport(message);
}
/**
* A stateless UAS is a UAS that does not maintain transaction state.
* It replies to requests normally, but discards any state that would
* ordinarily be retained by a UAS after a response has been sent. If a
* stateless UAS receives a retransmission of a request, it regenerates
* the response and re-sends it, just as if it were replying to the first
* instance of the request. A UAS cannot be stateless unless the request
* processing for that method would always result in the same response
* if the requests are identical. This rules out stateless registrars,
* for example. Stateless UASs do not use a transaction layer; they
* receive requests directly from the transport layer and send responses
* directly to the transport layer.
* https://tools.ietf.org/html/rfc3261#section-8.2.7
* @param message - Incoming request message to reply to.
* @param statusCode - Status code to reply with.
*/
replyStateless(message, options) {
const userAgent = this.configuration.userAgentHeaderFieldValue;
const supported = this.configuration.supportedOptionTagsResponse;
options = Object.assign(Object.assign({}, options), { userAgent, supported });
const response = constructOutgoingResponse(message, options);
this.transport.send(response.message).catch((error) => {
// If the transport rejects, it SHOULD reject with a TransportError.
// But the transport may be external code, so we are careful...
if (error instanceof Error) {
this.logger.error(error.message);
}
this.logger.error(`Transport error occurred sending stateless reply to ${message.method} request.`);
// TODO: Currently there is no hook to provide notification that a transport error occurred
// and throwing would result in an uncaught error (in promise), so we silently eat the error.
// Furthermore, silently eating stateless reply transport errors is arguably what we want to do here.
});
return response;
}
/**
* In Section 18.2.1, replace the last paragraph with:
*
* Next, the server transport attempts to match the request to a
* server transaction. It does so using the matching rules described
* in Section 17.2.3. If a matching server transaction is found, the
* request is passed to that transaction for processing. If no match
* is found, the request is passed to the core, which may decide to
* construct a new server transaction for that request.
* https://tools.ietf.org/html/rfc6026#section-8.10
* @param message - Incoming request message from transport layer.
*/
receiveRequestFromTransport(message) {
// When a request is received from the network by the server, it has to
// be matched to an existing transaction. This is accomplished in the
// following manner.
//
// The branch parameter in the topmost Via header field of the request
// is examined. If it is present and begins with the magic cookie
// "z9hG4bK", the request was generated by a client transaction
// compliant to this specification. Therefore, the branch parameter
// will be unique across all transactions sent by that client. The
// request matches a transaction if:
//
// 1. the branch parameter in the request is equal to the one in the
// top Via header field of the request that created the
// transaction, and
//
// 2. the sent-by value in the top Via of the request is equal to the
// one in the request that created the transaction, and
//
// 3. the method of the request matches the one that created the
// transaction, except for ACK, where the method of the request
// that created the transaction is INVITE.
//
// This matching rule applies to both INVITE and non-INVITE transactions
// alike.
//
// The sent-by value is used as part of the matching process because
// there could be accidental or malicious duplication of branch
// parameters from different clients.
// https://tools.ietf.org/html/rfc3261#section-17.2.3
const transactionId = message.viaBranch; // FIXME: Currently only using rule 1...
const uas = this.userAgentServers.get(transactionId);
// When receiving an ACK that matches an existing INVITE server
// transaction and that does not contain a branch parameter containing
// the magic cookie defined in RFC 3261, the matching transaction MUST
// be checked to see if it is in the "Accepted" state. If it is, then
// the ACK must be passed directly to the transaction user instead of
// being absorbed by the transaction state machine. This is necessary
// as requests from RFC 2543 clients will not include a unique branch
// parameter, and the mechanisms for calculating the transaction ID from
// such a request will be the same for both INVITE and ACKs.
// https://tools.ietf.org/html/rfc6026#section-6
// Any ACKs received from the network while in the "Accepted" state MUST be
// passed directly to the TU and not absorbed.
// https://tools.ietf.org/html/rfc6026#section-7.1
if (message.method === C.ACK) {
if (uas && uas.transaction.state === TransactionState.Accepted) {
if (uas instanceof InviteUserAgentServer) {
// These are ACKs matching an INVITE server transaction.
// These should never happen with RFC 3261 compliant user agents
// (would be a broken ACK to negative final response or something)
// but is apparently how RFC 2543 user agents do things.
// We are not currently supporting this case.
// NOTE: Not backwards compatible with RFC 2543 (no support for strict-routing).
this.logger.warn(`Discarding out of dialog ACK after 2xx response sent on transaction ${transactionId}.`);
return;
}
}
}
// The CANCEL method requests that the TU at the server side cancel a
// pending transaction. The TU determines the transaction to be
// cancelled by taking the CANCEL request, and then assuming that the
// request method is anything but CANCEL or ACK and applying the
// transaction matching procedures of Section 17.2.3. The matching
// transaction is the one to be cancelled.
// https://tools.ietf.org/html/rfc3261#section-9.2
if (message.method === C.CANCEL) {
if (uas) {
// Regardless of the method of the original request, as long as the
// CANCEL matched an existing transaction, the UAS answers the CANCEL
// request itself with a 200 (OK) response.
// https://tools.ietf.org/html/rfc3261#section-9.2
this.replyStateless(message, { statusCode: 200 });
// If the transaction for the original request still exists, the behavior
// of the UAS on receiving a CANCEL request depends on whether it has already
// sent a final response for the original request. If it has, the CANCEL
// request has no effect on the processing of the original request, no
// effect on any session state, and no effect on the responses generated
// for the original request. If the UAS has not issued a final response
// for the original request, its behavior depends on the method of the
// original request. If the original request was an INVITE, the UAS
// SHOULD immediately respond to the INVITE with a 487 (Request
// Terminated).
// https://tools.ietf.org/html/rfc3261#section-9.2
if (uas.transaction instanceof InviteServerTransaction &&
uas.transaction.state === TransactionState.Proceeding) {
if (uas instanceof InviteUserAgentServer) {
uas.receiveCancel(message);
}
// A CANCEL request has no impact on the processing of
// transactions with any other method defined in this specification.
// https://tools.ietf.org/html/rfc3261#section-9.2
}
}
else {
// If the UAS did not find a matching transaction for the CANCEL
// according to the procedure above, it SHOULD respond to the CANCEL
// with a 481 (Call Leg/Transaction Does Not Exist).
// https://tools.ietf.org/html/rfc3261#section-9.2
this.replyStateless(message, { statusCode: 481 });
}
return;
}
// If a matching server transaction is found, the request is passed to that
// transaction for processing.
// https://tools.ietf.org/html/rfc6026#section-8.10
if (uas) {
uas.transaction.receiveRequest(message);
return;
}
// If no match is found, the request is passed to the core, which may decide to
// construct a new server transaction for that request.
// https://tools.ietf.org/html/rfc6026#section-8.10
this.receiveRequest(message);
return;
}
/**
* UAC and UAS procedures depend strongly on two factors. First, based
* on whether the request or response is inside or outside of a dialog,
* and second, based on the method of a request. Dialogs are discussed
* thoroughly in Section 12; they represent a peer-to-peer relationship
* between user agents and are established by specific SIP methods, such
* as INVITE.
* @param message - Incoming request message.
*/
receiveRequest(message) {
// 8.2 UAS Behavior
// UASs SHOULD process the requests in the order of the steps that
// follow in this section (that is, starting with authentication, then
// inspecting the method, the header fields, and so on throughout the
// remainder of this section).
// https://tools.ietf.org/html/rfc3261#section-8.2
// 8.2.1 Method Inspection
// Once a request is authenticated (or authentication is skipped), the
// UAS MUST inspect the method of the request. If the UAS recognizes
// but does not support the method of a request, it MUST generate a 405
// (Method Not Allowed) response. Procedures for generating responses
// are described in Section 8.2.6. The UAS MUST also add an Allow
// header field to the 405 (Method Not Allowed) response. The Allow
// header field MUST list the set of methods supported by the UAS
// generating the message.
// https://tools.ietf.org/html/rfc3261#section-8.2.1
if (!AllowedMethods.includes(message.method)) {
const allowHeader = "Allow: " + AllowedMethods.toString();
this.replyStateless(message, {
statusCode: 405,
extraHeaders: [allowHeader]
});
return;
}
// 8.2.2 Header Inspection
// https://tools.ietf.org/html/rfc3261#section-8.2.2
if (!message.ruri) {
// FIXME: A request message should always have an ruri
throw new Error("Request-URI undefined.");
}
// 8.2.2.1 To and Request-URI
// If the Request-URI uses a scheme not supported by the UAS, it SHOULD
// reject the request with a 416 (Unsupported URI Scheme) response.
// https://tools.ietf.org/html/rfc3261#section-8.2.2.1
if (message.ruri.scheme !== "sip") {
this.replyStateless(message, { statusCode: 416 });
return;
}
// 8.2.2.1 To and Request-URI
// If the Request-URI does not identify an address that the
// UAS is willing to accept requests for, it SHOULD reject
// the request with a 404 (Not Found) response.
// https://tools.ietf.org/html/rfc3261#section-8.2.2.1
const ruri = message.ruri;
const ruriMatches = (uri) => {
return !!uri && uri.user === ruri.user;
};
if (!ruriMatches(this.configuration.aor) &&
!(ruriMatches(this.configuration.contact.uri) ||
ruriMatches(this.configuration.contact.pubGruu) ||
ruriMatches(this.configuration.contact.tempGruu))) {
this.logger.warn("Request-URI does not point to us.");
if (message.method !== C.ACK) {
this.replyStateless(message, { statusCode: 404 });
}
return;
}
// 8.2.2.1 To and Request-URI
// Other potential sources of received Request-URIs include
// the Contact header fields of requests and responses sent by the UA
// that establish or refresh dialogs.
// https://tools.ietf.org/html/rfc3261#section-8.2.2.1
if (message.method === C.INVITE) {
if (!message.hasHeader("Contact")) {
this.replyStateless(message, {
statusCode: 400,
reasonPhrase: "Missing Contact Header"
});
return;
}
}
// 8.2.2.2 Merged Requests
// If the request has no tag in the To header field, the UAS core MUST
// check the request against ongoing transactions. If the From tag,
// Call-ID, and CSeq exactly match those associated with an ongoing
// transaction, but the request does not match that transaction (based
// on the matching rules in Section 17.2.3), the UAS core SHOULD
// generate a 482 (Loop Detected) response and pass it to the server
// transaction.
//
// The same request has arrived at the UAS more than once, following
// different paths, most likely due to forking. The UAS processes
// the first such request received and responds with a 482 (Loop
// Detected) to the rest of them.
// https://tools.ietf.org/html/rfc3261#section-8.2.2.2
if (!message.toTag) {
const transactionId = message.viaBranch;
if (!this.userAgentServers.has(transactionId)) {
const mergedRequest = Array.from(this.userAgentServers.values()).some((uas) => uas.transaction.request.fromTag === message.fromTag &&
uas.transaction.request.callId === message.callId &&
uas.transaction.request.cseq === message.cseq);
if (mergedRequest) {
this.replyStateless(message, { statusCode: 482 });
return;
}
}
}
// 8.2.2.3 Require
// https://tools.ietf.org/html/rfc3261#section-8.2.2.3
// TODO
// 8.2.3 Content Processing
// https://tools.ietf.org/html/rfc3261#section-8.2.3
// TODO
// 8.2.4 Applying Extensions
// https://tools.ietf.org/html/rfc3261#section-8.2.4
// TODO
// 8.2.5 Processing the Request
// Assuming all of the checks in the previous subsections are passed,
// the UAS processing becomes method-specific.
// https://tools.ietf.org/html/rfc3261#section-8.2.5
// The UAS will receive the request from the transaction layer. If the
// request has a tag in the To header field, the UAS core computes the
// dialog identifier corresponding to the request and compares it with
// existing dialogs. If there is a match, this is a mid-dialog request.
// In that case, the UAS first applies the same processing rules for
// requests outside of a dialog, discussed in Section 8.2.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
if (message.toTag) {
this.receiveInsideDialogRequest(message);
}
else {
this.receiveOutsideDialogRequest(message);
}
return;
}
/**
* Once a dialog has been established between two UAs, either of them
* MAY initiate new transactions as needed within the dialog. The UA
* sending the request will take the UAC role for the transaction. The
* UA receiving the request will take the UAS role. Note that these may
* be different roles than the UAs held during the transaction that
* established the dialog.
* https://tools.ietf.org/html/rfc3261#section-12.2
* @param message - Incoming request message.
*/
receiveInsideDialogRequest(message) {
// NOTIFY requests are matched to such SUBSCRIBE requests if they
// contain the same "Call-ID", a "To" header field "tag" parameter that
// matches the "From" header field "tag" parameter of the SUBSCRIBE
// request, and the same "Event" header field. Rules for comparisons of
// the "Event" header fields are described in Section 8.2.1.
// https://tools.ietf.org/html/rfc6665#section-4.4.1
if (message.method === C.NOTIFY) {
const event = message.parseHeader("Event");
if (!event || !event.event) {
this.replyStateless(message, { statusCode: 489 });
return;
}
// FIXME: Subscriber id should also matching on event id.
const subscriberId = message.callId + message.toTag + event.event;
const subscriber = this.subscribers.get(subscriberId);
if (subscriber) {
const uas = new NotifyUserAgentServer(this, message);
subscriber.onNotify(uas);
return;
}
}
// Requests sent within a dialog, as any other requests, are atomic. If
// a particular request is accepted by the UAS, all the state changes
// associated with it are performed. If the request is rejected, none
// of the state changes are performed.
//
// Note that some requests, such as INVITEs, affect several pieces of
// state.
//
// The UAS will receive the request from the transaction layer. If the
// request has a tag in the To header field, the UAS core computes the
// dialog identifier corresponding to the request and compares it with
// existing dialogs. If there is a match, this is a mid-dialog request.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
const dialogId = message.callId + message.toTag + message.fromTag;
const dialog = this.dialogs.get(dialogId);
if (dialog) {
// [Sip-implementors] Reg. SIP reinvite, UPDATE and OPTIONS
// You got the question right.
//
// And you got the right answer too. :-)
//
// Thanks,
// Paul
//
// Robert Sparks wrote:
// > So I've lost track of the question during the musing.
// >
// > I _think_ the fundamental question being asked is this:
// >
// > Is an endpoint required to reject (with a 481) an OPTIONS request that
// > arrives with at to-tag but does not match any existing dialog state.
// > (Assuming some earlier requirement hasn't forced another error code). Or
// > is it OK if it just sends
// > a 200 OK anyhow.
// >
// > My take on the collection of specs is that its _not_ ok for it to send
// > the 200 OK anyhow and that it is required to send
// > the 481. I base this primarily on these sentences from 11.2 in 3261:
// >
// > The response to an OPTIONS is constructed using the standard rules
// > for a SIP response as discussed in Section 8.2.6. The response code
// > chosen MUST be the same that would have been chosen had the request
// > been an INVITE.
// >
// > Did I miss the point of the question?
// >
// > On May 15, 2008, at 12:48 PM, Paul Kyzivat wrote:
// >
// >> [Including Robert in hopes of getting his insight on this.]
// https://lists.cs.columbia.edu/pipermail/sip-implementors/2008-May/019178.html
//
// Requests that do not change in any way the state of a dialog may be
// received within a dialog (for example, an OPTIONS request). They are
// processed as if they had been received outside the dialog.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
if (message.method === C.OPTIONS) {
const allowHeader = "Allow: " + AllowedMethods.toString();
const acceptHeader = "Accept: " + acceptedBodyTypes.toString();
this.replyStateless(message, {
statusCode: 200,
extraHeaders: [allowHeader, acceptHeader]
});
return;
}
// Pass the incoming request to the dialog for further handling.
dialog.receiveRequest(message);
return;
}
// The most important behaviors of a stateless UAS are the following:
// ...
// o A stateless UAS MUST ignore ACK requests.
// ...
// https://tools.ietf.org/html/rfc3261#section-8.2.7
if (message.method === C.ACK) {
// If a final response to an INVITE was sent statelessly,
// the corresponding ACK:
// - will not match an existing transaction
// - may have tag in the To header field
// - not not match any existing dialogs
// Absorb unmatched ACKs.
return;
}
// If the request has a tag in the To header field, but the dialog
// identifier does not match any existing dialogs, the UAS may have
// crashed and restarted, or it may have received a request for a
// different (possibly failed) UAS (the UASs can construct the To tags
// so that a UAS can identify that the tag was for a UAS for which it is
// providing recovery). Another possibility is that the incoming
// request has been simply mis-routed. Based on the To tag, the UAS MAY
// either accept or reject the request. Accepting the request for
// acceptable To tags provides robustness, so that dialogs can persist
// even through crashes. UAs wishing to support this capability must
// take into consideration some issues such as choosing monotonically
// increasing CSeq sequence numbers even across reboots, reconstructing
// the route set, and accepting out-of-range RTP timestamps and sequence
// numbers.
//
// If the UAS wishes to reject the request because it does not wish to
// recreate the dialog, it MUST respond to the request with a 481
// (Call/Transaction Does Not Exist) status code and pass that to the
// server transaction.
// https://tools.ietf.org/html/rfc3261#section-12.2.2
this.replyStateless(message, { statusCode: 481 });
return;
}
/**
* Assuming all of the checks in the previous subsections are passed,
* the UAS processing becomes method-specific.
* https://tools.ietf.org/html/rfc3261#section-8.2.5
* @param message - Incoming request message.
*/
receiveOutsideDialogRequest(message) {
switch (message.method) {
case C.ACK:
// Absorb stray out of dialog ACKs
break;
case C.BYE:
// If the BYE does not match an existing dialog, the UAS core SHOULD
// generate a 481 (Call/Transaction Does Not Exist) response and pass
// that to the server transaction. This rule means that a BYE sent
// without tags by a UAC will be rejected.
// https://tools.ietf.org/html/rfc3261#section-15.1.2
this.replyStateless(message, { statusCode: 481 });
break;
case C.CANCEL:
throw new Error(`Unexpected out of dialog request method ${message.method}.`);
break;
case C.INFO:
// Use of the INFO method does not constitute a separate dialog usage.
// INFO messages are always part of, and share the fate of, an invite
// dialog usage [RFC5057]. INFO messages cannot be sent as part of
// other dialog usages, or outside an existing dialog.
// https://tools.ietf.org/html/rfc6086#section-1
this.replyStateless(message, { statusCode: 405 }); // Should never happen
break;
case C.INVITE:
// https://tools.ietf.org/html/rfc3261#section-13.3.1
{
const uas = new InviteUserAgentServer(this, message);
this.delegate.onInvite ? this.delegate.onInvite(uas) : uas.reject();
}
break;
case C.MESSAGE:
// MESSAGE requests are discouraged inside a dialog. Implementations
// are restricted from creating a usage for the purpose of carrying a
// sequence of MESSAGE requests (though some implementations use it that
// way, against the standard recommendation).
// https://tools.ietf.org/html/rfc5057#section-5.3
{
const uas = new MessageUserAgentServer(this, message);
this.delegate.onMessage ? this.delegate.onMessage(uas) : uas.accept();
}
break;
case C.NOTIFY:
// Obsoleted by: RFC 6665
// If any non-SUBSCRIBE mechanisms are defined to create subscriptions,
// it is the responsibility of the parties defining those mechanisms to
// ensure that correlation of a NOTIFY message to the corresponding
// subscription is possible. Designers of such mechanisms are also
// warned to make a distinction between sending a NOTIFY message to a
// subscriber who is aware of the subscription, and sending a NOTIFY
// message to an unsuspecting node. The latter behavior is invalid, and
// MUST receive a "481 Subscription does not exist" response (unless
// some other 400- or 500-class error code is more applicable), as
// described in section 3.2.4. In other words, knowledge of a
// subscription must exist in both the subscriber and the notifier to be
// valid, even if installed via a non-SUBSCRIBE mechanism.
// https://tools.ietf.org/html/rfc3265#section-3.2
//
// NOTIFY requests are sent to inform subscribers of changes in state to
// which the subscriber has a subscription. Subscriptions are created
// using the SUBSCRIBE method. In legacy implementations, it is
// possible that other means of subscription creation have been used.
// However, this specification does not allow the creation of
// subscriptions except through SUBSCRIBE requests and (for backwards-
// compatibility) REFER requests [RFC3515].
// https://tools.ietf.org/html/rfc6665#section-3.2
{
const uas = new NotifyUserAgentServer(this, message);
this.delegate.onNotify ? this.delegate.onNotify(uas) : uas.reject({ statusCode: 405 });
}
break;
case C.OPTIONS:
// https://tools.ietf.org/html/rfc3261#section-11.2
{
const allowHeader = "Allow: " + AllowedMethods.toString();
const acceptHeader = "Accept: " + acceptedBodyTypes.toString();
this.replyStateless(message, {
statusCode: 200,
extraHeaders: [allowHeader, acceptHeader]
});
}
break;
case C.REFER:
// https://tools.ietf.org/html/rfc3515#section-2.4.2
{
const uas = new ReferUserAgentServer(this, message);
this.delegate.onRefer ? this.delegate.onRefer(uas) : uas.reject({ statusCode: 405 });
}
break;
case C.REGISTER:
// https://tools.ietf.org/html/rfc3261#section-10.3
{
const uas = new RegisterUserAgentServer(this, message);
this.delegate.onRegister ? this.delegate.onRegister(uas) : uas.reject({ statusCode: 405 });
}
break;
case C.SUBSCRIBE:
// https://tools.ietf.org/html/rfc6665#section-4.2
{
const uas = new SubscribeUserAgentServer(this, message);
this.delegate.onSubscribe ? this.delegate.onSubscribe(uas) : uas.reject({ statusCode: 480 });
}
break;
default:
throw new Error(`Unexpected out of dialog request method ${message.method}.`);
}
return;
}
/**
* Responses are first processed by the transport layer and then passed
* up to the transaction layer. The transaction layer performs its
* processing and then passes the response up to the TU. The majority
* of response processing in the TU is method specific. However, there
* are some general behaviors independent of the method.
* https://tools.ietf.org/html/rfc3261#section-8.1.3
* @param message - Incoming response message from transport layer.
*/
receiveResponseFromTransport(message) {
// 8.1.3.1 Transaction Layer Errors
// https://tools.ietf.org/html/rfc3261#section-8.1.3.1
// Handled by transaction layer callbacks.
// 8.1.3.2 Unrecognized Responses
// https://tools.ietf.org/html/rfc3261#section-8.1.3.1
// TODO
// 8.1.3.3 Vias
// https://tools.ietf.org/html/rfc3261#section-8.1.3.3
if (message.getHeaders("via").length > 1) {
this.logger.warn("More than one Via header field present in the response, dropping");
return;
}
// 8.1.3.4 Processing 3xx Responses
// https://tools.ietf.org/html/rfc3261#section-8.1.3.4
// TODO
// 8.1.3.5 Processing 4xx Responses
// https://tools.ietf.org/html/rfc3261#section-8.1.3.5
// TODO
// When the transport layer in the client receives a response, it has to
// determine which client transaction will handle the response, so that
// the processing of Sections 17.1.1 and 17.1.2 can take place. The
// branch parameter in the top Via header field is used for this
// purpose. A response matches a client transaction under two
// conditions:
//
// 1. If the response has the same value of the branch parameter in
// the top Via header field as the branch parameter in the top
// Via header field of the request that created the transaction.
//
// 2. If the method parameter in the CSeq header field matches the
// method of the request that created the transaction. The
// method is needed since a CANCEL request constitutes a
// different transaction, but shares the same value of the branch
// parameter.
// https://tools.ietf.org/html/rfc3261#section-17.1.3
const userAgentClientId = message.viaBranch + message.method;
const userAgentClient = this.userAgentClients.get(userAgentClientId);
// The client transport uses the matching procedures of Section
// 17.1.3 to attempt to match the response to an existing
// transaction. If there is a match, the response MUST be passed to
// that transaction. Otherwise, any element other than a stateless
// proxy MUST silently discard the response.
// https://tools.ietf.org/html/rfc6026#section-8.9
if (userAgentClient) {
userAgentClient.transaction.receiveResponse(message);
}
else {
this.logger.warn(`Discarding unmatched ${message.statusCode} response to ${message.method} ${userAgentClientId}.`);
}
}
}