@routr/connect 2.15.3 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/access.js CHANGED
@@ -31,6 +31,7 @@ exports.checkAccessFromPSTN = exports.checkAgentOrPeerAccess = exports.checkAcce
31
31
  const logger_1 = require("@fonoster/logger");
32
32
  const common_1 = require("@routr/common");
33
33
  const types_1 = require("./types");
34
+ const utils_1 = require("./utils");
34
35
  const logger = (0, logger_1.getLogger)({ service: "connect", filePath: __filename });
35
36
  const checkAccess = (accessRequest) => __awaiter(void 0, void 0, void 0, function* () {
36
37
  const { apiClient, request, caller, callee, routingDirection } = accessRequest;
@@ -80,11 +81,16 @@ const checkAccessFromPSTN = (apiClient, request, callee) => __awaiter(void 0, vo
80
81
  return common_1.CommonResponse.createForbiddenResponse();
81
82
  }
82
83
  // Verify that the IP is allowlist which means getting the access control list for the trunk
84
+ // The source is the top Via's "received" value (set by the SIP stack when the
85
+ // packet came from another address), not request.sender.host, which is copied
86
+ // from the Via host and can be forged.
83
87
  if (trunk.accessControlList) {
84
88
  try {
85
- const allow = trunk.accessControlList.allow.filter((net) => {
86
- return common_1.IpUtils.hasIp(net, request.sender.host);
87
- })[0];
89
+ const source = (0, utils_1.getSourceAddress)(request);
90
+ const allow = source &&
91
+ trunk.accessControlList.allow.filter((net) => {
92
+ return common_1.IpUtils.hasIp(net, source);
93
+ })[0];
88
94
  if (!allow) {
89
95
  return common_1.CommonResponse.createUnauthorizedResponseWithoutChallenge();
90
96
  }
package/dist/envs.d.ts CHANGED
@@ -4,3 +4,7 @@ export declare const API_ADDR: string;
4
4
  export declare const CONNECT_VERIFIER_ADDR: string;
5
5
  export declare const CONNECT_VERIFIER_PUBLIC_KEY_PATH: string;
6
6
  export declare const CONNECT_VERIFIER_OPTIONS: any;
7
+ export declare const DEFAULT_UNTRUSTED_HEADERS_TO_STRIP: string[];
8
+ export declare const UNTRUSTED_HEADERS_TO_STRIP: string[];
9
+ export declare const TRUSTED_EDGEPORTS: string[];
10
+ export declare const TRUSTED_EDGEPORT_ADDRESSES: string[];
package/dist/envs.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.CONNECT_VERIFIER_OPTIONS = exports.CONNECT_VERIFIER_PUBLIC_KEY_PATH = exports.CONNECT_VERIFIER_ADDR = exports.API_ADDR = exports.LOCATION_ADDR = exports.BIND_ADDR = void 0;
4
+ exports.TRUSTED_EDGEPORT_ADDRESSES = exports.TRUSTED_EDGEPORTS = exports.UNTRUSTED_HEADERS_TO_STRIP = exports.DEFAULT_UNTRUSTED_HEADERS_TO_STRIP = exports.CONNECT_VERIFIER_OPTIONS = exports.CONNECT_VERIFIER_PUBLIC_KEY_PATH = exports.CONNECT_VERIFIER_ADDR = exports.API_ADDR = exports.LOCATION_ADDR = exports.BIND_ADDR = void 0;
5
5
  /*
6
6
  * Copyright (C) 2026 by Fonoster Inc (https://fonoster.com)
7
7
  * http://github.com/fonoster/routr
@@ -30,3 +30,39 @@ exports.CONNECT_VERIFIER_PUBLIC_KEY_PATH = process.env.CONNECT_VERIFIER_PUBLIC_K
30
30
  exports.CONNECT_VERIFIER_OPTIONS = process.env.CONNECT_VERIFIER_OPTIONS
31
31
  ? JSON.parse(process.env.CONNECT_VERIFIER_OPTIONS)
32
32
  : { expiresIn: "1h", algorithm: ["RS256"] };
33
+ // Platform headers a caller must never be able to set. They are removed from
34
+ // requests that come from untrusted sources (e.g., the PSTN or a client using a
35
+ // Connect Token) before Routr adds its own. Override with a comma-separated list;
36
+ // set it to an empty string to disable stripping.
37
+ exports.DEFAULT_UNTRUSTED_HEADERS_TO_STRIP = [
38
+ "X-App-Ref",
39
+ "X-Call-Ref",
40
+ "X-Call-Direction",
41
+ "X-DOD-Number",
42
+ "X-DOD-Privacy",
43
+ "X-Access-Key-Id",
44
+ "X-Is-Api-Originated-Type",
45
+ "X-Edgeport-Ref",
46
+ "X-Gateway-Auth",
47
+ "X-Request-Uri",
48
+ "X-Session-Count"
49
+ ];
50
+ exports.UNTRUSTED_HEADERS_TO_STRIP = process.env.UNTRUSTED_HEADERS_TO_STRIP !== undefined
51
+ ? process.env.UNTRUSTED_HEADERS_TO_STRIP.split(",")
52
+ .map((h) => h.trim())
53
+ .filter((h) => h.length > 0)
54
+ : exports.DEFAULT_UNTRUSTED_HEADERS_TO_STRIP;
55
+ const parseList = (value) => (value !== null && value !== void 0 ? value : "")
56
+ .split(",")
57
+ .map((v) => v.trim())
58
+ .filter((v) => v.length > 0);
59
+ // Peer EdgePorts allowed to forward requests with X-Edgeport-Ref. Such a request
60
+ // skips routing and access checks (its route is rebuilt from the message), so it
61
+ // is only honored when the packet comes from an address in
62
+ // TRUSTED_EDGEPORT_ADDRESSES (IPs or CIDRs, matched against the top Via's
63
+ // "received" value, or its host when "received" is absent). TRUSTED_EDGEPORTS
64
+ // optionally narrows which X-Edgeport-Ref values are accepted. The defaults trust
65
+ // no peer EdgePort, which is right for single-EdgePort deployments: the header is
66
+ // removed and the request is routed normally.
67
+ exports.TRUSTED_EDGEPORTS = parseList(process.env.TRUSTED_EDGEPORTS);
68
+ exports.TRUSTED_EDGEPORT_ADDRESSES = parseList(process.env.TRUSTED_EDGEPORT_ADDRESSES);
@@ -1,4 +1,5 @@
1
1
  import { MessageRequest, Response } from "@routr/processor";
2
2
  import { ILocationService } from "@routr/location";
3
3
  import { CommonConnect as CC } from "@routr/common";
4
- export declare const handleRequest: (location: ILocationService, apiClient?: CC.APIClient) => (request: MessageRequest, res: Response) => Promise<void>;
4
+ import { EdgePortTrust } from "../utils";
5
+ export declare const handleRequest: (location: ILocationService, apiClient?: CC.APIClient, edgePortTrust?: EdgePortTrust) => (request: MessageRequest, res: Response) => Promise<void>;
@@ -70,12 +70,33 @@ const location_1 = require("@routr/location");
70
70
  const common_1 = require("@routr/common");
71
71
  const logger_1 = require("@fonoster/logger");
72
72
  const types_1 = require("./../types");
73
+ const utils_1 = require("../utils");
74
+ const envs_1 = require("../envs");
73
75
  const logger = (0, logger_1.getLogger)({ service: "connect", filePath: __filename });
74
76
  const enforceE164 = processor_1.Alterations.enforceE164(common_1.Environment.ENFORCE_E164, common_1.Environment.ENFORCE_E164_WITH_MOBILE_PREFIX);
75
- const handleRequest = (location, apiClient) => (request, res) => __awaiter(void 0, void 0, void 0, function* () {
77
+ const defaultEdgePortTrust = {
78
+ refs: envs_1.TRUSTED_EDGEPORTS,
79
+ addresses: envs_1.TRUSTED_EDGEPORT_ADDRESSES
80
+ };
81
+ const handleRequest = (location, apiClient, edgePortTrust = defaultEdgePortTrust) => (request, res) => __awaiter(void 0, void 0, void 0, function* () {
82
+ var _a;
76
83
  try {
77
- const req = common_1.Environment.ENFORCE_E164 ? enforceE164(request) : request;
84
+ let req = common_1.Environment.ENFORCE_E164 ? enforceE164(request) : request;
78
85
  let route;
86
+ // X-Edgeport-Ref marks a request forwarded by a peer EdgePort, whose route
87
+ // is rebuilt from the message without routing or access checks. Honor it
88
+ // only from a trusted peer EdgePort. Anyone else could use it to relay calls
89
+ // through Routr (toll fraud), so remove it and route the request normally.
90
+ const fromTrustedEdgePort = (0, utils_1.isFromTrustedEdgePort)(req, edgePortTrust);
91
+ if (!fromTrustedEdgePort &&
92
+ processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.EDGEPORT_REF)) {
93
+ logger.warn("ignoring X-Edgeport-Ref from an untrusted source", {
94
+ ref: req.ref,
95
+ edgePortRef: req.edgePortRef,
96
+ via: (_a = req.message.via) === null || _a === void 0 ? void 0 : _a[0]
97
+ });
98
+ req = processor_1.Alterations.removeXEdgePortRef(req);
99
+ }
79
100
  // A CANCEL must follow the exact path its INVITE took (RFC 3261 §9.1) — it is
80
101
  // never a new call to route. edgeport already forwards it correctly, straight
81
102
  // from the INVITE's own client transaction (GRPCSipListener#sendCancel), which
@@ -87,8 +108,7 @@ const handleRequest = (location, apiClient) => (request, res) => __awaiter(void
87
108
  // reaches edgeport's already-correct CANCEL handling. The result: the CANCEL is
88
109
  // silently dropped, the call keeps ringing, and if the callee answers a moment
89
110
  // later they connect into a line the caller has already abandoned.
90
- if (processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.EDGEPORT_REF) ||
91
- req.method === common_1.CommonTypes.Method.CANCEL) {
111
+ if (fromTrustedEdgePort || req.method === common_1.CommonTypes.Method.CANCEL) {
92
112
  route = location_1.Helper.createRouteFromLastMessage(req);
93
113
  }
94
114
  else {
@@ -105,6 +125,9 @@ const handleRequest = (location, apiClient) => (request, res) => __awaiter(void
105
125
  else if (!routerResult.route) {
106
126
  return res.sendTemporarilyUnavailable();
107
127
  }
128
+ // Remove platform headers a caller from an untrusted source (e.g., the
129
+ // PSTN) could have spoofed. Routr re-adds its own values afterwards.
130
+ req = processor_1.Alterations.removeHeaders((0, utils_1.getUntrustedHeadersToStrip)(direction, !!(0, utils_1.hasXConnectObjectHeader)(req), envs_1.UNTRUSTED_HEADERS_TO_STRIP))(req);
108
131
  const p = {
109
132
  name: common_1.CommonTypes.ExtraHeader.CALL_DIRECTION,
110
133
  value: direction,
package/dist/router.js CHANGED
@@ -280,6 +280,7 @@ function agentToPeer(location, callee, req) {
280
280
  // eslint-disable-next-line require-jsdoc
281
281
  function peerToPSTN(apiClient, req) {
282
282
  return __awaiter(this, void 0, void 0, function* () {
283
+ var _a;
283
284
  const numberTel = processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.DOD_NUMBER);
284
285
  const privacy = processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.DOD_PRIVACY);
285
286
  if (!numberTel) {
@@ -289,6 +290,16 @@ function peerToPSTN(apiClient, req) {
289
290
  if (!number) {
290
291
  return common_1.CommonResponse.createNotFoundResponse(`No Number found for tel:${numberTel}`);
291
292
  }
293
+ // A Number bound to an access key (e.g., a Fonoster workspace) may only be
294
+ // used by a request that carries the same X-Access-Key-Id
295
+ const numberAccessKeyId = (_a = number.extended) === null || _a === void 0 ? void 0 : _a.accessKeyId;
296
+ if (numberAccessKeyId &&
297
+ numberAccessKeyId !== processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.ACCESS_KEY_ID)) {
298
+ logger.verbose("rejected peer-to-pstn request: access key mismatch", {
299
+ numberRef: number.ref
300
+ });
301
+ return common_1.CommonResponse.createForbiddenResponse();
302
+ }
292
303
  if (!number.trunk) {
293
304
  // TODO: Create custom error
294
305
  throw new Error(`no trunk associated with Number ref: ${number.ref}`);
package/dist/service.d.ts CHANGED
@@ -1,2 +1,17 @@
1
1
  import { ConnectProcessorConfig } from "./types";
2
+ import { MessageRequest } from "@routr/common";
3
+ import { Response } from "@routr/processor";
4
+ import { CommonConnect as CC } from "@routr/common";
5
+ import { ILocationService } from "@routr/location";
6
+ /**
7
+ * Builds the connect processor's message handler. Exported so the dispatch can be
8
+ * unit tested without a gRPC server.
9
+ *
10
+ * @param {object} deps - the API client and location service the handlers use
11
+ * @return {Function} the handler to pass to Processor#listen
12
+ */
13
+ export declare const createDispatcher: (deps: {
14
+ apiClient: CC.APIClient;
15
+ location: ILocationService;
16
+ }) => (req: MessageRequest, res: Response) => Promise<void>;
2
17
  export default function connectProcessor(config: ConnectProcessorConfig): void;
package/dist/service.js CHANGED
@@ -42,6 +42,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
42
42
  });
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.createDispatcher = void 0;
45
46
  exports.default = connectProcessor;
46
47
  const common_1 = require("@routr/common");
47
48
  const location_1 = require("@routr/location");
@@ -52,50 +53,82 @@ const logger_1 = require("@fonoster/logger");
52
53
  const request_1 = require("./handlers/request");
53
54
  const registry_1 = require("./handlers/registry");
54
55
  const register_1 = require("./handlers/register");
56
+ const utils_1 = require("./utils");
55
57
  const logger = (0, logger_1.getLogger)({ service: "connect", filePath: __filename });
58
+ /**
59
+ * Forwards a request that belongs to an existing dialog along its route set: strip
60
+ * our own Route, add our Via, and let the EdgePort send it to the next hop or the
61
+ * Request-URI. No routing and no access checks, the dialog was authorized when it
62
+ * was set up. BYE and ACK always took this path; since RFC 3261 section 12.2 makes
63
+ * any request with a To tag an in-dialog request, re-INVITE, UPDATE, INFO, REFER and
64
+ * the NOTIFY that follows a REFER take it too.
65
+ *
66
+ * @param {MessageRequest} req - the in-dialog request
67
+ * @param {Response} res - the response object used to send it on
68
+ * @return {void}
69
+ */
70
+ const forwardInDialog = (req, res) => res.send((0, function_1.pipe)(req, processor_1.Alterations.decreaseMaxForwards, processor_1.Alterations.addSelfViaUsingExternalAddrs, processor_1.Alterations.removeSelfRoutes));
71
+ /**
72
+ * Builds the connect processor's message handler. Exported so the dispatch can be
73
+ * unit tested without a gRPC server.
74
+ *
75
+ * @param {object} deps - the API client and location service the handlers use
76
+ * @return {Function} the handler to pass to Processor#listen
77
+ */
78
+ const createDispatcher = (deps) => (req, res) => __awaiter(void 0, void 0, void 0, function* () {
79
+ const { apiClient, location } = deps;
80
+ logger.verbose("connect processor received new request", {
81
+ ref: req.ref,
82
+ method: req.method,
83
+ type: req.message.messageType === common_2.CommonTypes.MessageType.RESPONSE
84
+ ? "(response)"
85
+ : "(request)",
86
+ edgePort: req.edgePortRef
87
+ });
88
+ logger.silly(JSON.stringify(req, null, " "));
89
+ // If it is a response simply forwards to uac
90
+ if (processor_1.Helper.isTypeResponse(req)) {
91
+ // Remove the proxy and overwrite the contact with the sender info
92
+ return res.send(
93
+ // NOTE: We should consider making the overwriteContactWithSenderInfo an Agent/Peer level alteration
94
+ // pipe(req, A.overwriteContactWithSenderInfo, A.removeTopVia)
95
+ (0, function_1.pipe)(req, processor_1.Alterations.removeTopVia));
96
+ }
97
+ // REGISTER never carries a To tag and CANCEL keeps its own path (it follows
98
+ // the INVITE it cancels); everything else with a To tag is in a dialog.
99
+ if (req.method !== common_1.Method.REGISTER &&
100
+ req.method !== common_1.Method.CANCEL &&
101
+ (0, utils_1.isInDialog)(req)) {
102
+ return forwardInDialog(req, res);
103
+ }
104
+ switch (req.method) {
105
+ case common_1.Method.PUBLISH:
106
+ case common_1.Method.NOTIFY:
107
+ case common_1.Method.SUBSCRIBE:
108
+ case common_1.Method.MESSAGE:
109
+ res.sendMethodNotAllowed();
110
+ break;
111
+ case common_1.Method.REGISTER:
112
+ if (processor_1.Extensions.getHeaderValue(req, common_2.CommonTypes.ExtraHeader.GATEWAY_AUTH)) {
113
+ (0, registry_1.handleRegistry)(req, res);
114
+ }
115
+ else {
116
+ return (0, register_1.handleRegister)(apiClient, location)(req, res);
117
+ }
118
+ break;
119
+ case common_1.Method.BYE:
120
+ case common_1.Method.ACK:
121
+ forwardInDialog(req, res);
122
+ break;
123
+ default:
124
+ return (0, request_1.handleRequest)(location, apiClient)(req, res);
125
+ }
126
+ });
127
+ exports.createDispatcher = createDispatcher;
56
128
  // eslint-disable-next-line require-jsdoc
57
129
  function connectProcessor(config) {
58
130
  const { bindAddr, locationAddr } = config;
59
131
  const location = new location_1.LocationClient({ addr: locationAddr });
60
- new processor_1.default({ bindAddr, name: "connect" }).listen((req, res) => __awaiter(this, void 0, void 0, function* () {
61
- logger.verbose("connect processor received new request", {
62
- ref: req.ref,
63
- method: req.method,
64
- type: req.message.messageType === common_2.CommonTypes.MessageType.RESPONSE
65
- ? "(response)"
66
- : "(request)",
67
- edgePort: req.edgePortRef
68
- });
69
- logger.silly(JSON.stringify(req, null, " "));
70
- // If it is a response simply forwards to uac
71
- if (processor_1.Helper.isTypeResponse(req)) {
72
- // Remove the proxy and overwrite the contact with the sender info
73
- return res.send(
74
- // NOTE: We should consider making the overwriteContactWithSenderInfo an Agent/Peer level alteration
75
- // pipe(req, A.overwriteContactWithSenderInfo, A.removeTopVia)
76
- (0, function_1.pipe)(req, processor_1.Alterations.removeTopVia));
77
- }
78
- switch (req.method) {
79
- case common_1.Method.PUBLISH:
80
- case common_1.Method.NOTIFY:
81
- case common_1.Method.SUBSCRIBE:
82
- case common_1.Method.MESSAGE:
83
- res.sendMethodNotAllowed();
84
- break;
85
- case common_1.Method.REGISTER:
86
- if (processor_1.Extensions.getHeaderValue(req, common_2.CommonTypes.ExtraHeader.GATEWAY_AUTH)) {
87
- (0, registry_1.handleRegistry)(req, res);
88
- }
89
- else {
90
- (0, register_1.handleRegister)(common_2.CommonConnect.apiClient({ apiAddr: config.apiAddr }), location)(req, res);
91
- }
92
- break;
93
- case common_1.Method.BYE:
94
- case common_1.Method.ACK:
95
- res.send((0, function_1.pipe)(req, processor_1.Alterations.decreaseMaxForwards, processor_1.Alterations.addSelfViaUsingExternalAddrs, processor_1.Alterations.removeSelfRoutes));
96
- break;
97
- default:
98
- (0, request_1.handleRequest)(location, common_2.CommonConnect.apiClient({ apiAddr: config.apiAddr }))(req, res);
99
- }
100
- }));
132
+ const apiClient = common_2.CommonConnect.apiClient({ apiAddr: config.apiAddr });
133
+ new processor_1.default({ bindAddr, name: "connect" }).listen((0, exports.createDispatcher)({ apiClient, location }));
101
134
  }
package/dist/utils.d.ts CHANGED
@@ -18,7 +18,59 @@ export declare const getSipUri: (uri: {
18
18
  user?: string;
19
19
  host: string;
20
20
  }) => string;
21
+ /**
22
+ * Whether a request belongs to an existing dialog: RFC 3261 section 12.2, a request
23
+ * inside a dialog carries the To tag the dialog was established with. Such a request is
24
+ * forwarded along the dialog's route set and is never routed as a new call, which
25
+ * would send a hold or transfer re-INVITE to the PSTN trunk and answer the provider's
26
+ * re-INVITE with 500 (Routr v1 fixed the same bug in issue #58). The EdgePort refuses
27
+ * an in-dialog INVITE or REFER for a dialog it did not proxy, so forwarding here does
28
+ * not bypass access checks for a forged To tag.
29
+ *
30
+ * @param {MessageRequest} req - The request
31
+ * @return {boolean} true when the To header carries a tag
32
+ */
33
+ export declare const isInDialog: (req: MessageRequest) => boolean;
21
34
  export declare const hasXConnectObjectHeader: (req: MessageRequest) => string;
22
35
  export declare const getVerifierImpl: () => {
23
36
  verify: (token: string) => Promise<import("@routr/common/dist/errors").ServiceUnavailableError | V.VerifyResponse>;
24
37
  };
38
+ /**
39
+ * Returns the headers that must be removed from a request before it is forwarded.
40
+ *
41
+ * Requests from authenticated Agents and Peers are trusted and keep their headers.
42
+ * Requests from the PSTN, and requests authorized only by a Connect Token, are not:
43
+ * the platform headers in `headers` are removed so a caller can't spoof them.
44
+ * A client using a verified Connect Token keeps its X-App-Ref.
45
+ *
46
+ * @param {RoutingDirection} direction - The routing direction of the request
47
+ * @param {boolean} withConnectToken - Whether the request carried a verified X-Connect-Token
48
+ * @param {string[]} headers - The headers to strip from untrusted requests
49
+ * @return {string[]} The headers to remove
50
+ */
51
+ export declare const getUntrustedHeadersToStrip: (direction: RoutingDirection, withConnectToken: boolean, headers: string[]) => string[];
52
+ export interface EdgePortTrust {
53
+ refs: string[];
54
+ addresses: string[];
55
+ }
56
+ /**
57
+ * Returns the address the request came from: the top Via's "received" value,
58
+ * which the SIP stack sets when the packet's source differs from the Via host
59
+ * (RFC 3261 §18.2.1), or the Via host otherwise.
60
+ *
61
+ * @param {MessageRequest} request - The request
62
+ * @return {string | undefined} The source address
63
+ */
64
+ export declare const getSourceAddress: (request: MessageRequest) => string | undefined;
65
+ /**
66
+ * Tells whether a request carrying X-Edgeport-Ref was forwarded by a trusted peer
67
+ * EdgePort. Only such a request may take the last-message fast path, which skips
68
+ * routing and access checks. The header value alone proves nothing (anyone can
69
+ * set it), so the request's source address must be in `trust.addresses`; when
70
+ * `trust.refs` is not empty, the header value must also be one of them.
71
+ *
72
+ * @param {MessageRequest} request - The request
73
+ * @param {EdgePortTrust} trust - The trusted peer EdgePorts
74
+ * @return {boolean} true if the request comes from a trusted peer EdgePort
75
+ */
76
+ export declare const isFromTrustedEdgePort: (request: MessageRequest, trust: EdgePortTrust) => boolean;
package/dist/utils.js CHANGED
@@ -45,7 +45,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
45
45
  return (mod && mod.__esModule) ? mod : { "default": mod };
46
46
  };
47
47
  Object.defineProperty(exports, "__esModule", { value: true });
48
- exports.getVerifierImpl = exports.hasXConnectObjectHeader = exports.getSipUri = exports.getTrunkURI = exports.createTrunkAuthentication = exports.createRemotePartyId = exports.createPAssertedIdentity = exports.getRoutingDirection = exports.findResource = exports.findNumberByTelUrl = exports.findDomain = exports.isKind = void 0;
48
+ exports.isFromTrustedEdgePort = exports.getSourceAddress = exports.getUntrustedHeadersToStrip = exports.getVerifierImpl = exports.hasXConnectObjectHeader = exports.isInDialog = exports.getSipUri = exports.getTrunkURI = exports.createTrunkAuthentication = exports.createRemotePartyId = exports.createPAssertedIdentity = exports.getRoutingDirection = exports.findResource = exports.findNumberByTelUrl = exports.findDomain = exports.isKind = void 0;
49
49
  /*
50
50
  * Copyright (C) 2026 by Fonoster Inc (https://fonoster.com)
51
51
  * http://github.com/fonoster/routr
@@ -206,6 +206,20 @@ const getTrunkURI = (trunk) => {
206
206
  exports.getTrunkURI = getTrunkURI;
207
207
  const getSipUri = (uri) => uri.user ? `sip:${uri.user}@${uri.host}` : `sip:${uri.host}`;
208
208
  exports.getSipUri = getSipUri;
209
+ /**
210
+ * Whether a request belongs to an existing dialog: RFC 3261 section 12.2, a request
211
+ * inside a dialog carries the To tag the dialog was established with. Such a request is
212
+ * forwarded along the dialog's route set and is never routed as a new call, which
213
+ * would send a hold or transfer re-INVITE to the PSTN trunk and answer the provider's
214
+ * re-INVITE with 500 (Routr v1 fixed the same bug in issue #58). The EdgePort refuses
215
+ * an in-dialog INVITE or REFER for a dialog it did not proxy, so forwarding here does
216
+ * not bypass access checks for a forged To tag.
217
+ *
218
+ * @param {MessageRequest} req - The request
219
+ * @return {boolean} true when the To header carries a tag
220
+ */
221
+ const isInDialog = (req) => { var _a; return !!((_a = req.message.to) === null || _a === void 0 ? void 0 : _a.tag); };
222
+ exports.isInDialog = isInDialog;
209
223
  const hasXConnectObjectHeader = (req) => processor_1.Extensions.getHeaderValue(req, common_1.CommonTypes.ExtraHeader.CONNECT_TOKEN);
210
224
  exports.hasXConnectObjectHeader = hasXConnectObjectHeader;
211
225
  const getVerifierImpl = () => {
@@ -223,3 +237,72 @@ const getVerifierImpl = () => {
223
237
  return null;
224
238
  };
225
239
  exports.getVerifierImpl = getVerifierImpl;
240
+ /**
241
+ * Returns the headers that must be removed from a request before it is forwarded.
242
+ *
243
+ * Requests from authenticated Agents and Peers are trusted and keep their headers.
244
+ * Requests from the PSTN, and requests authorized only by a Connect Token, are not:
245
+ * the platform headers in `headers` are removed so a caller can't spoof them.
246
+ * A client using a verified Connect Token keeps its X-App-Ref.
247
+ *
248
+ * @param {RoutingDirection} direction - The routing direction of the request
249
+ * @param {boolean} withConnectToken - Whether the request carried a verified X-Connect-Token
250
+ * @param {string[]} headers - The headers to strip from untrusted requests
251
+ * @return {string[]} The headers to remove
252
+ */
253
+ const getUntrustedHeadersToStrip = (direction, withConnectToken, headers) => {
254
+ if (withConnectToken) {
255
+ return headers.filter((h) => h.toLowerCase() !== common_1.CommonTypes.ExtraHeader.APP_REF.toLowerCase());
256
+ }
257
+ if (direction === types_1.RoutingDirection.FROM_PSTN ||
258
+ direction === types_1.RoutingDirection.UNKNOWN) {
259
+ return headers;
260
+ }
261
+ return [];
262
+ };
263
+ exports.getUntrustedHeadersToStrip = getUntrustedHeadersToStrip;
264
+ /**
265
+ * Returns the address the request came from: the top Via's "received" value,
266
+ * which the SIP stack sets when the packet's source differs from the Via host
267
+ * (RFC 3261 §18.2.1), or the Via host otherwise.
268
+ *
269
+ * @param {MessageRequest} request - The request
270
+ * @return {string | undefined} The source address
271
+ */
272
+ const getSourceAddress = (request) => {
273
+ var _a, _b;
274
+ const via = (_b = (_a = request.message) === null || _a === void 0 ? void 0 : _a.via) === null || _b === void 0 ? void 0 : _b[0];
275
+ return (via === null || via === void 0 ? void 0 : via.received) || (via === null || via === void 0 ? void 0 : via.host);
276
+ };
277
+ exports.getSourceAddress = getSourceAddress;
278
+ /**
279
+ * Tells whether a request carrying X-Edgeport-Ref was forwarded by a trusted peer
280
+ * EdgePort. Only such a request may take the last-message fast path, which skips
281
+ * routing and access checks. The header value alone proves nothing (anyone can
282
+ * set it), so the request's source address must be in `trust.addresses`; when
283
+ * `trust.refs` is not empty, the header value must also be one of them.
284
+ *
285
+ * @param {MessageRequest} request - The request
286
+ * @param {EdgePortTrust} trust - The trusted peer EdgePorts
287
+ * @return {boolean} true if the request comes from a trusted peer EdgePort
288
+ */
289
+ const isFromTrustedEdgePort = (request, trust) => {
290
+ const ref = processor_1.Extensions.getHeaderValue(request, common_1.CommonTypes.ExtraHeader.EDGEPORT_REF);
291
+ if (!ref || trust.addresses.length === 0)
292
+ return false;
293
+ if (trust.refs.length > 0 && !trust.refs.includes(ref))
294
+ return false;
295
+ const addr = (0, exports.getSourceAddress)(request);
296
+ if (!addr)
297
+ return false;
298
+ return trust.addresses.some((net) => {
299
+ try {
300
+ return common_1.IpUtils.hasIp(net, addr);
301
+ }
302
+ catch (e) {
303
+ // Invalid net, or a source that is not an IP address
304
+ return false;
305
+ }
306
+ });
307
+ };
308
+ exports.isFromTrustedEdgePort = isFromTrustedEdgePort;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@routr/connect",
3
- "version": "2.15.3",
3
+ "version": "2.16.0",
4
4
  "description": "Default processor",
5
5
  "author": "Pedro Sanders <psanders@fonoster.com>",
6
6
  "homepage": "https://github.com/fonoster/routr#readme",
@@ -20,9 +20,9 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@fonoster/logger": "0.6.0",
23
- "@routr/common": "^2.15.3",
24
- "@routr/location": "^2.15.3",
25
- "@routr/processor": "^2.15.3",
23
+ "@routr/common": "^2.16.0",
24
+ "@routr/location": "^2.16.0",
25
+ "@routr/processor": "^2.16.0",
26
26
  "jsonwebtoken": "^9.0.3"
27
27
  },
28
28
  "devDependencies": {
@@ -41,5 +41,5 @@
41
41
  "bugs": {
42
42
  "url": "https://github.com/fonoster/routr/issues"
43
43
  },
44
- "gitHead": "30e73f1331f115b2dd9d02cdfdcf931dbeede36b"
44
+ "gitHead": "db8a2eeb4b3f57a7f6b632d8d3378ffe53fc6cfe"
45
45
  }