Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add incoming update requests handling #1086

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/api/invitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,8 @@ export class Invitation extends Session {
onMessage: (messageRequest): void => this.onMessageRequest(messageRequest),
onNotify: (notifyRequest): void => this.onNotifyRequest(notifyRequest),
onPrack: (prackRequest): void => this.onPrackRequest(prackRequest),
onRefer: (referRequest): void => this.onReferRequest(referRequest)
onRefer: (referRequest): void => this.onReferRequest(referRequest),
onUpdate: (updateRequest): void => this.onUpdateRequest(updateRequest)
};
this._dialog = session;
this.stateTransition(SessionState.Established);
Expand Down
6 changes: 4 additions & 2 deletions src/api/inviter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ export class Inviter extends Session {
}
extraHeaders.push("Contact: " + contact);
extraHeaders.push(
"Allow: " + ["ACK", "CANCEL", "INVITE", "MESSAGE", "BYE", "OPTIONS", "INFO", "NOTIFY", "REFER"].toString()
"Allow: " +
["ACK", "CANCEL", "INVITE", "MESSAGE", "BYE", "OPTIONS", "INFO", "NOTIFY", "REFER", "UPDATE"].toString()
);
if (userAgent.configuration.sipExtension100rel === SIPExtension.Required) {
extraHeaders.push("Require: 100rel");
Expand Down Expand Up @@ -810,7 +811,8 @@ export class Inviter extends Session {
onMessage: (messageRequest): void => this.onMessageRequest(messageRequest),
onNotify: (notifyRequest): void => this.onNotifyRequest(notifyRequest),
onPrack: (prackRequest): void => this.onPrackRequest(prackRequest),
onRefer: (referRequest): void => this.onReferRequest(referRequest)
onRefer: (referRequest): void => this.onReferRequest(referRequest),
onUpdate: (updateRequest): void => this.onUpdateRequest(updateRequest)
};
this._dialog = session;

Expand Down
8 changes: 8 additions & 0 deletions src/api/session-delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ export interface SessionDelegate {
*/
onRefer?(referral: Referral): void;

/**
* Called upon receiving an incoming in dialog UPDATE request.
* @param request - The incoming update request.
* @param response - The response sent
* @param statusCode - The status code for the outgoing update response.
*/
onUpdate?(request: IncomingRequestMessage, response: string, statusCode: number): void;

/**
* Called upon creating a SessionDescriptionHandler.
*
Expand Down
75 changes: 75 additions & 0 deletions src/api/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { SessionOptions } from "./session-options.js";
import { SessionReferOptions } from "./session-refer-options.js";
import { SessionState } from "./session-state.js";
import { UserAgent } from "./user-agent.js";
import { IncomingUpdateRequest } from "../core/index.js";

/**
* A session provides real time communication between one or more participants.
Expand Down Expand Up @@ -1099,6 +1100,80 @@ export abstract class Session {
}
}

protected onUpdateRequest(request: IncomingUpdateRequest): void {
this.logger.log("Session.onUpdateRequest");

const extraHeaders = ["Contact: " + this._contact];

// Handle P-Asserted-Identity
if (request.message.hasHeader("P-Asserted-Identity")) {
const header = request.message.getHeader("P-Asserted-Identity");
if (!header) {
throw new Error("Header undefined.");
}
this._assertedIdentity = Grammar.nameAddrHeaderParse(header);
}

const body = getBody(request.message);
const hasOffer = body && body.contentDisposition === "session";

if (!hasOffer) {
const response = request.accept({ statusCode: 200, extraHeaders });
this.delegate?.onUpdate?.(request.message, response.message, 200);
return;
}

const options = {
sessionDescriptionHandlerOptions: this.sessionDescriptionHandlerOptionsReInvite,
sessionDescriptionHandlerModifiers: this.sessionDescriptionHandlerModifiersReInvite
};

this.generateResponseOfferAnswerInDialog(options)
.then((body) => {
const response = request.accept({ statusCode: 200, extraHeaders, body });
this.delegate?.onUpdate?.(request.message, response.message, 200);
})
.catch((error: Error) => {
this.logger.error(error.message);
this.logger.error("Failed to handle to UPDATE request");
if (!this.dialog) {
throw new Error("Dialog undefined.");
}
this.logger.error(this.dialog.signalingState);
// If we don't have a local/remote offer...
if (this.dialog.signalingState === SignalingState.Stable) {
const response = request.reject({ statusCode: 488 }); // Not Acceptable Here
this.delegate?.onUpdate?.(request.message, response.message, 488);
return;
}
// Otherwise rollback
this.rollbackOffer()
.then(() => {
const response = request.reject({ statusCode: 488 }); // Not Acceptable Here
this.delegate?.onUpdate?.(request.message, response.message, 488);
})
.catch((errorRollback: Error) => {
// No way to recover, so terminate session and mark as failed.
this.logger.error(errorRollback.message);
this.logger.error("Failed to rollback offer on UPDATE request");
const response = request.reject({ statusCode: 488 }); // Not Acceptable Here
// A BYE should only be sent if session is not already terminated.
// For example, a BYE may be sent/received while re-INVITE is outstanding.
// Note that the ACK was already sent by the transaction, so just need to send BYE.
if (this.state !== SessionState.Terminated) {
if (!this.dialog) {
throw new Error("Dialog undefined.");
}
const extraHeadersBye: Array<string> = [];
extraHeadersBye.push("Reason: " + this.getReasonHeaderValue(500, "Internal Server Error"));
this.dialog.bye(undefined, { extraHeaders: extraHeadersBye });
this.stateTransition(SessionState.Terminated);
}
this.delegate?.onUpdate?.(request.message, response.message, 488);
});
});
}

/**
* Generate an offer or answer for a response to an INVITE request.
* If a remote offer was provided in the request, set the remote
Expand Down
54 changes: 54 additions & 0 deletions src/core/dialogs/session-dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ReInviteUserAgentClient } from "../user-agents/re-invite-user-agent-cli
import { ReInviteUserAgentServer } from "../user-agents/re-invite-user-agent-server.js";
import { ReferUserAgentClient } from "../user-agents/refer-user-agent-client.js";
import { ReferUserAgentServer } from "../user-agents/refer-user-agent-server.js";
import { UpdateUserAgentServer } from "../user-agents/update-user-agent-server.js";
import { Dialog } from "./dialog.js";
import { DialogState } from "./dialog-state.js";

Expand All @@ -48,6 +49,7 @@ export class SessionDialog extends Dialog implements Session {

public reinviteUserAgentClient: ReInviteUserAgentClient | undefined;
public reinviteUserAgentServer: ReInviteUserAgentServer | undefined;
public updateUserAgentServer: UpdateUserAgentServer | undefined;

/** The state of the offer/answer exchange. */
private _signalingState: SignalingState = SignalingState.Initial;
Expand Down Expand Up @@ -615,6 +617,45 @@ export class SessionDialog extends Dialog implements Session {
this.dialogState.remoteTarget = contact.uri;
}

if (message.method === C.UPDATE) {
const retryAfter = Math.floor(Math.random() * 10) + 1;
const extraHeaders = [`Retry-After: ${retryAfter}`];

const body = getBody(message);
const hasOffer = body && body.contentDisposition === "session";

if (hasOffer && this._signalingState === SignalingState.HaveLocalOffer) {
// If an UPDATE is received that contains an offer, and the UAS has
// generated an offer (in an UPDATE, PRACK or INVITE) to which it has
// not yet received an answer, the UAS MUST reject the UPDATE with a 491
// response
// https://datatracker.ietf.org/doc/html/rfc3311#section-5.2
this.core.replyStateless(message, { statusCode: 491 });
return;
}

if (hasOffer && this._signalingState === SignalingState.HaveRemoteOffer) {
// Similarly, if an UPDATE is received that contains an
// offer, and the UAS has received an offer (in an UPDATE, PRACK, or
// INVITE) to which it has not yet generated an answer, the UAS MUST
// reject the UPDATE with a 500 response, and MUST include a Retry-After
// header field with a randomly chosen value between 0 and 10 seconds.
// https://datatracker.ietf.org/doc/html/rfc3311#section-5.2
this.core.replyStateless(message, { statusCode: 500, extraHeaders });
return;
}

if (this.updateUserAgentServer) {
// A UAS that receives an UPDATE before it has generated a final
// response to a previous UPDATE on the same dialog MUST return a 500
// response to the new UPDATE, and MUST include a Retry-After header
// field with a randomly chosen value between 0 and 10 seconds.
// https://datatracker.ietf.org/doc/html/rfc3311#section-5.2
this.core.replyStateless(message, { statusCode: 500, extraHeaders });
return;
}
}

// Switch on method and then delegate.
switch (message.method) {
case C.BYE:
Expand Down Expand Up @@ -692,6 +733,19 @@ export class SessionDialog extends Dialog implements Session {
this.delegate && this.delegate.onRefer ? this.delegate.onRefer(uas) : uas.reject();
}
break;
case C.UPDATE:
// However, unlike a re-INVITE, the UPDATE MUST be
// responded to promptly, and therefore the user cannot generally be
// prompted to approve the session changes. If the UAS cannot change
// the session parameters without prompting the user, it SHOULD reject
// the request with a 504 response.
// https://datatracker.ietf.org/doc/html/rfc3311#section-5.2
{
const uas = new UpdateUserAgentServer(this, message);
this.signalingStateTransition(message);
this.delegate && this.delegate.onUpdate ? this.delegate.onUpdate(uas) : uas.reject({ statusCode: 504 });
}
break;
default:
{
this.logger.log(`INVITE dialog ${this.id} received unimplemented ${message.method} request`);
Expand Down
1 change: 1 addition & 0 deletions src/core/messages/methods/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from "./publish.js";
export * from "./register.js";
export * from "./refer.js";
export * from "./subscribe.js";
export * from "./update.js";
22 changes: 22 additions & 0 deletions src/core/messages/methods/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
import { IncomingRequest } from "../incoming-request.js";
import { IncomingResponse } from "../incoming-response.js";
import { OutgoingRequest } from "../outgoing-request.js";

/**
* Incoming UPDATE request.
* @public
*/
export interface IncomingUpdateRequest extends IncomingRequest {}

/**
* Incoming UPDATE response.
* @public
*/
export interface IncomingUpdateResponse extends IncomingResponse {}

/**
* Outgoing UPDATE request.
* @public
*/
export interface OutgoingUpdateRequest extends OutgoingRequest {}
7 changes: 7 additions & 0 deletions src/core/session/session-delegate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IncomingUpdateRequest } from "../messages/methods/update.js";
import { IncomingAckRequest } from "../messages/methods/ack.js";
import { IncomingByeRequest } from "../messages/methods/bye.js";
import { IncomingInfoRequest } from "../messages/methods/info.js";
Expand Down Expand Up @@ -83,4 +84,10 @@ export interface SessionDelegate {
* @param request - Incoming REFER request.
*/
onRefer?(request: IncomingReferRequest): void;

/**
* Receive UPDATE request.
* @param request - Incoming UPDATE request.
*/
onUpdate?(request: IncomingUpdateRequest): void;
}
3 changes: 2 additions & 1 deletion src/core/user-agent-core/allowed-methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ export const AllowedMethods = [
C.PRACK, // FIXME: Only if 100rel Supported
C.REFER,
C.REGISTER,
C.SUBSCRIBE
C.SUBSCRIBE,
C.UPDATE
];
39 changes: 39 additions & 0 deletions src/core/user-agents/update-user-agent-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { SessionDialog } from "../dialogs/session-dialog.js";
import { IncomingRequestDelegate } from "../messages/incoming-request.js";
import { IncomingRequestMessage } from "../messages/incoming-request-message.js";
import { IncomingUpdateRequest } from "../messages/methods/update.js";
import { NonInviteServerTransaction } from "../transactions/non-invite-server-transaction.js";
import { UserAgentServer } from "./user-agent-server.js";
import { OutgoingResponse, ResponseOptions } from "../messages/outgoing-response.js";

/**
* UPDATE UAS.
* @public
*/
export class UpdateUserAgentServer extends UserAgentServer implements IncomingUpdateRequest {
private dialog: SessionDialog;

constructor(dialog: SessionDialog, message: IncomingRequestMessage, delegate?: IncomingRequestDelegate) {
super(NonInviteServerTransaction, dialog.userAgentCore, message, delegate);
dialog.updateUserAgentServer = this;
this.dialog = dialog;
}

public accept(options: ResponseOptions = { statusCode: 200 }): OutgoingResponse {
const response = super.accept(options);
this.dialog.updateUserAgentServer = undefined;

if (options.body) {
// Update dialog signaling state with offer/answer in body
this.dialog.signalingStateTransition(options.body);
}

return response;
}

public reject(options: ResponseOptions = { statusCode: 504 }): OutgoingResponse {
this.dialog.signalingStateRollback();
this.dialog.updateUserAgentServer = undefined;
return super.reject(options);
}
}
4 changes: 3 additions & 1 deletion src/grammar/pegjs/src/grammar.pegjs
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,10 @@ REFERm = "\x52\x45\x46\x45\x52" // REFER in caps

PUBLISHm = "\x50\x55\x42\x4c\x49\x53\x48" // PUBLISH in caps

UPDATEm = "\x55\x50\x44\x41\x54\x45" // UPDATE in caps

Method = ( INVITEm / ACKm / OPTIONSm / BYEm / CANCELm / REGISTERm
/ SUBSCRIBEm / PUBLISHm / NOTIFYm / REFERm / extension_method ){
/ SUBSCRIBEm / PUBLISHm / NOTIFYm / REFERm / UPDATEm / extension_method ){

options = options || { data: {}};
options.data.method = text();
Expand Down
Loading