UNPKG

sip.js

Version:

A SIP library for JavaScript

288 lines (287 loc) 14.6 kB
import { SubscriptionDialog } from "../dialogs/subscription-dialog.js"; import { SubscriptionState } from "../subscription/subscription.js"; import { Timers } from "../timers.js"; import { NonInviteClientTransaction } from "../transactions/non-invite-client-transaction.js"; import { UserAgentClient } from "./user-agent-client.js"; /** * SUBSCRIBE UAC. * @remarks * 4.1. Subscriber Behavior * https://tools.ietf.org/html/rfc6665#section-4.1 * * User agent client for installation of a single subscription per SUBSCRIBE request. * TODO: Support for installation of multiple subscriptions on forked SUBSCRIBE requests. * @public */ export class SubscribeUserAgentClient extends UserAgentClient { constructor(core, message, delegate) { // Get event from request message. const event = message.getHeader("Event"); if (!event) { throw new Error("Event undefined"); } // Get expires from request message. const expires = message.getHeader("Expires"); if (!expires) { throw new Error("Expires undefined"); } super(NonInviteClientTransaction, core, message, delegate); this.delegate = delegate; // FIXME: Subscriber id should also be matching on event id. this.subscriberId = message.callId + message.fromTag + event; this.subscriptionExpiresRequested = this.subscriptionExpires = Number(expires); this.subscriptionEvent = event; this.subscriptionState = SubscriptionState.NotifyWait; // Start waiting for a NOTIFY we can use to create a subscription. this.waitNotifyStart(); } /** * Destructor. * Note that Timer N may live on waiting for an initial NOTIFY and * the delegate may still receive that NOTIFY. If you don't want * that behavior then either clear the delegate so the delegate * doesn't get called (a 200 will be sent in response to the NOTIFY) * or call `waitNotifyStop` which will clear Timer N and remove this * UAC from the core (a 481 will be sent in response to the NOTIFY). */ dispose() { super.dispose(); } /** * Handle out of dialog NOTIFY associated with SUBSCRIBE request. * This is the first NOTIFY received after the SUBSCRIBE request. * @param uas - User agent server handling the subscription creating NOTIFY. */ onNotify(uas) { // 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 const event = uas.message.parseHeader("Event").event; if (!event || event !== this.subscriptionEvent) { this.logger.warn(`Failed to parse event.`); uas.reject({ statusCode: 489 }); return; } // NOTIFY requests MUST contain "Subscription-State" header fields that // indicate the status of the subscription. // https://tools.ietf.org/html/rfc6665#section-4.1.3 const subscriptionState = uas.message.parseHeader("Subscription-State"); if (!subscriptionState || !subscriptionState.state) { this.logger.warn("Failed to parse subscription state."); uas.reject({ statusCode: 489 }); return; } // Validate subscription state. const state = subscriptionState.state; switch (state) { case "pending": break; case "active": break; case "terminated": break; default: this.logger.warn(`Invalid subscription state ${state}`); uas.reject({ statusCode: 489 }); return; } // Dialogs usages are created upon completion of a NOTIFY transaction // for a new subscription, unless the NOTIFY request contains a // "Subscription-State" of "terminated." // https://tools.ietf.org/html/rfc6665#section-4.4.1 if (state !== "terminated") { // The Contact header field MUST be present and contain exactly one SIP // or SIPS URI in any request that can result in the establishment of a // dialog. // https://tools.ietf.org/html/rfc3261#section-8.1.1.8 const contact = uas.message.parseHeader("contact"); if (!contact) { this.logger.warn("Failed to parse contact."); uas.reject({ statusCode: 489 }); return; } } // In accordance with the rules for proxying non-INVITE requests as // defined in [RFC3261], successful SUBSCRIBE requests will receive only // one 200-class response; however, due to forking, the subscription may // have been accepted by multiple nodes. The subscriber MUST therefore // be prepared to receive NOTIFY requests with "From:" tags that differ // from the "To:" tag received in the SUBSCRIBE 200-class response. // // If multiple NOTIFY requests are received in different dialogs in // response to a single SUBSCRIBE request, each dialog represents a // different destination to which the SUBSCRIBE request was forked. // Subscriber handling in such situations varies by event package; see // Section 5.4.9 for details. // https://tools.ietf.org/html/rfc6665#section-4.1.4 // Each event package MUST specify whether forked SUBSCRIBE requests are // allowed to install multiple subscriptions. // // If such behavior is not allowed, the first potential dialog- // establishing message will create a dialog. All subsequent NOTIFY // requests that correspond to the SUBSCRIBE request (i.e., have // matching "To", "From", "Call-ID", and "Event" header fields, as well // as "From" header field "tag" parameter and "Event" header field "id" // parameter) but that do not match the dialog would be rejected with a // 481 response. Note that the 200-class response to the SUBSCRIBE // request can arrive after a matching NOTIFY request has been received; // such responses might not correlate to the same dialog established by // the NOTIFY request. Except as required to complete the SUBSCRIBE // transaction, such non-matching 200-class responses are ignored. // // If installing of multiple subscriptions by way of a single forked // SUBSCRIBE request is allowed, the subscriber establishes a new dialog // towards each notifier by returning a 200-class response to each // NOTIFY request. Each dialog is then handled as its own entity and is // refreshed independently of the other dialogs. // // In the case that multiple subscriptions are allowed, the event // package MUST specify whether merging of the notifications to form a // single state is required, and how such merging is to be performed. // Note that it is possible that some event packages may be defined in // such a way that each dialog is tied to a mutually exclusive state // that is unaffected by the other dialogs; this MUST be clearly stated // if it is the case. // https://tools.ietf.org/html/rfc6665#section-5.4.9 // *** NOTE: This implementation is only for event packages which // do not allow forked requests to install multiple subscriptions. // As such and in accordance with the specification, we stop waiting // and any future NOTIFY requests will be rejected with a 481. if (this.dialog) { throw new Error("Dialog already created. This implementation only supports install of single subscriptions."); } this.waitNotifyStop(); // Update expires. this.subscriptionExpires = subscriptionState.expires ? Math.min(this.subscriptionExpires, Math.max(subscriptionState.expires, 0)) : this.subscriptionExpires; // Update subscription state. switch (state) { case "pending": this.subscriptionState = SubscriptionState.Pending; break; case "active": this.subscriptionState = SubscriptionState.Active; break; case "terminated": this.subscriptionState = SubscriptionState.Terminated; break; default: throw new Error(`Unrecognized state ${state}.`); } // Dialogs usages are created upon completion of a NOTIFY transaction // for a new subscription, unless the NOTIFY request contains a // "Subscription-State" of "terminated." // https://tools.ietf.org/html/rfc6665#section-4.4.1 if (this.subscriptionState !== SubscriptionState.Terminated) { // Because the dialog usage is established by the NOTIFY request, the // route set at the subscriber is taken from the NOTIFY request itself, // as opposed to the route set present in the 200-class response to the // SUBSCRIBE request. // https://tools.ietf.org/html/rfc6665#section-4.4.1 const dialogState = SubscriptionDialog.initialDialogStateForSubscription(this.message, uas.message); // Subscription Initiated! :) this.dialog = new SubscriptionDialog(this.subscriptionEvent, this.subscriptionExpires, this.subscriptionState, this.core, dialogState); } // Delegate. if (this.delegate && this.delegate.onNotify) { const request = uas; const subscription = this.dialog; this.delegate.onNotify({ request, subscription }); } else { uas.accept(); } } waitNotifyStart() { if (!this.N) { // Add ourselves to the core's subscriber map. // This allows the core to route out of dialog NOTIFY messages to us. this.core.subscribers.set(this.subscriberId, this); this.N = setTimeout(() => this.timerN(), Timers.TIMER_N); } } waitNotifyStop() { if (this.N) { // Remove ourselves to the core's subscriber map. // Any future out of dialog NOTIFY messages will be rejected with a 481. this.core.subscribers.delete(this.subscriberId); clearTimeout(this.N); this.N = undefined; } } /** * Receive a response from the transaction layer. * @param message - Incoming response message. */ receiveResponse(message) { if (!this.authenticationGuard(message)) { return; } if (message.statusCode && message.statusCode >= 200 && message.statusCode < 300) { // The "Expires" header field in a 200-class response to SUBSCRIBE // request indicates the actual duration for which the subscription will // remain active (unless refreshed). The received value might be // smaller than the value indicated in the SUBSCRIBE request but cannot // be larger; see Section 4.2.1 for details. // https://tools.ietf.org/html/rfc6665#section-4.1.2.1 // The "Expires" values present in SUBSCRIBE 200-class responses behave // in the same way as they do in REGISTER responses: the server MAY // shorten the interval but MUST NOT lengthen it. // // If the duration specified in a SUBSCRIBE request is unacceptably // short, the notifier may be able to send a 423 response, as // described earlier in this section. // // 200-class responses to SUBSCRIBE requests will not generally contain // any useful information beyond subscription duration; their primary // purpose is to serve as a reliability mechanism. State information // will be communicated via a subsequent NOTIFY request from the // notifier. // https://tools.ietf.org/html/rfc6665#section-4.2.1.1 const expires = message.getHeader("Expires"); if (!expires) { this.logger.warn("Expires header missing in a 200-class response to SUBSCRIBE"); } else { const subscriptionExpiresReceived = Number(expires); if (subscriptionExpiresReceived > this.subscriptionExpiresRequested) { this.logger.warn("Expires header in a 200-class response to SUBSCRIBE with a higher value than the one in the request"); } if (subscriptionExpiresReceived < this.subscriptionExpires) { this.subscriptionExpires = subscriptionExpiresReceived; } } // If a NOTIFY arrived before 200-class response a dialog may have been created. // Updated the dialogs expiration only if this indicates earlier expiration. if (this.dialog) { if (this.dialog.subscriptionExpires > this.subscriptionExpires) { this.dialog.subscriptionExpires = this.subscriptionExpires; } } } if (message.statusCode && message.statusCode >= 300 && message.statusCode < 700) { this.waitNotifyStop(); // No NOTIFY will be sent after a negative final response. } super.receiveResponse(message); } /** * To ensure that subscribers do not wait indefinitely for a * subscription to be established, a subscriber starts a Timer N, set to * 64*T1, when it sends a SUBSCRIBE request. If this Timer N expires * prior to the receipt of a NOTIFY request, the subscriber considers * the subscription failed, and cleans up any state associated with the * subscription attempt. * https://tools.ietf.org/html/rfc6665#section-4.1.2.4 */ timerN() { this.logger.warn(`Timer N expired for SUBSCRIBE user agent client. Timed out waiting for NOTIFY.`); this.waitNotifyStop(); if (this.delegate && this.delegate.onNotifyTimeout) { this.delegate.onNotifyTimeout(); } } }