@routr/connect 2.15.3 → 2.15.4

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/utils.d.ts CHANGED
@@ -22,3 +22,42 @@ export declare const hasXConnectObjectHeader: (req: MessageRequest) => string;
22
22
  export declare const getVerifierImpl: () => {
23
23
  verify: (token: string) => Promise<import("@routr/common/dist/errors").ServiceUnavailableError | V.VerifyResponse>;
24
24
  };
25
+ /**
26
+ * Returns the headers that must be removed from a request before it is forwarded.
27
+ *
28
+ * Requests from authenticated Agents and Peers are trusted and keep their headers.
29
+ * Requests from the PSTN, and requests authorized only by a Connect Token, are not:
30
+ * the platform headers in `headers` are removed so a caller can't spoof them.
31
+ * A client using a verified Connect Token keeps its X-App-Ref.
32
+ *
33
+ * @param {RoutingDirection} direction - The routing direction of the request
34
+ * @param {boolean} withConnectToken - Whether the request carried a verified X-Connect-Token
35
+ * @param {string[]} headers - The headers to strip from untrusted requests
36
+ * @return {string[]} The headers to remove
37
+ */
38
+ export declare const getUntrustedHeadersToStrip: (direction: RoutingDirection, withConnectToken: boolean, headers: string[]) => string[];
39
+ export interface EdgePortTrust {
40
+ refs: string[];
41
+ addresses: string[];
42
+ }
43
+ /**
44
+ * Returns the address the request came from: the top Via's "received" value,
45
+ * which the SIP stack sets when the packet's source differs from the Via host
46
+ * (RFC 3261 §18.2.1), or the Via host otherwise.
47
+ *
48
+ * @param {MessageRequest} request - The request
49
+ * @return {string | undefined} The source address
50
+ */
51
+ export declare const getSourceAddress: (request: MessageRequest) => string | undefined;
52
+ /**
53
+ * Tells whether a request carrying X-Edgeport-Ref was forwarded by a trusted peer
54
+ * EdgePort. Only such a request may take the last-message fast path, which skips
55
+ * routing and access checks. The header value alone proves nothing (anyone can
56
+ * set it), so the request's source address must be in `trust.addresses`; when
57
+ * `trust.refs` is not empty, the header value must also be one of them.
58
+ *
59
+ * @param {MessageRequest} request - The request
60
+ * @param {EdgePortTrust} trust - The trusted peer EdgePorts
61
+ * @return {boolean} true if the request comes from a trusted peer EdgePort
62
+ */
63
+ 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.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
@@ -223,3 +223,72 @@ const getVerifierImpl = () => {
223
223
  return null;
224
224
  };
225
225
  exports.getVerifierImpl = getVerifierImpl;
226
+ /**
227
+ * Returns the headers that must be removed from a request before it is forwarded.
228
+ *
229
+ * Requests from authenticated Agents and Peers are trusted and keep their headers.
230
+ * Requests from the PSTN, and requests authorized only by a Connect Token, are not:
231
+ * the platform headers in `headers` are removed so a caller can't spoof them.
232
+ * A client using a verified Connect Token keeps its X-App-Ref.
233
+ *
234
+ * @param {RoutingDirection} direction - The routing direction of the request
235
+ * @param {boolean} withConnectToken - Whether the request carried a verified X-Connect-Token
236
+ * @param {string[]} headers - The headers to strip from untrusted requests
237
+ * @return {string[]} The headers to remove
238
+ */
239
+ const getUntrustedHeadersToStrip = (direction, withConnectToken, headers) => {
240
+ if (withConnectToken) {
241
+ return headers.filter((h) => h.toLowerCase() !== common_1.CommonTypes.ExtraHeader.APP_REF.toLowerCase());
242
+ }
243
+ if (direction === types_1.RoutingDirection.FROM_PSTN ||
244
+ direction === types_1.RoutingDirection.UNKNOWN) {
245
+ return headers;
246
+ }
247
+ return [];
248
+ };
249
+ exports.getUntrustedHeadersToStrip = getUntrustedHeadersToStrip;
250
+ /**
251
+ * Returns the address the request came from: the top Via's "received" value,
252
+ * which the SIP stack sets when the packet's source differs from the Via host
253
+ * (RFC 3261 §18.2.1), or the Via host otherwise.
254
+ *
255
+ * @param {MessageRequest} request - The request
256
+ * @return {string | undefined} The source address
257
+ */
258
+ const getSourceAddress = (request) => {
259
+ var _a, _b;
260
+ const via = (_b = (_a = request.message) === null || _a === void 0 ? void 0 : _a.via) === null || _b === void 0 ? void 0 : _b[0];
261
+ return (via === null || via === void 0 ? void 0 : via.received) || (via === null || via === void 0 ? void 0 : via.host);
262
+ };
263
+ exports.getSourceAddress = getSourceAddress;
264
+ /**
265
+ * Tells whether a request carrying X-Edgeport-Ref was forwarded by a trusted peer
266
+ * EdgePort. Only such a request may take the last-message fast path, which skips
267
+ * routing and access checks. The header value alone proves nothing (anyone can
268
+ * set it), so the request's source address must be in `trust.addresses`; when
269
+ * `trust.refs` is not empty, the header value must also be one of them.
270
+ *
271
+ * @param {MessageRequest} request - The request
272
+ * @param {EdgePortTrust} trust - The trusted peer EdgePorts
273
+ * @return {boolean} true if the request comes from a trusted peer EdgePort
274
+ */
275
+ const isFromTrustedEdgePort = (request, trust) => {
276
+ const ref = processor_1.Extensions.getHeaderValue(request, common_1.CommonTypes.ExtraHeader.EDGEPORT_REF);
277
+ if (!ref || trust.addresses.length === 0)
278
+ return false;
279
+ if (trust.refs.length > 0 && !trust.refs.includes(ref))
280
+ return false;
281
+ const addr = (0, exports.getSourceAddress)(request);
282
+ if (!addr)
283
+ return false;
284
+ return trust.addresses.some((net) => {
285
+ try {
286
+ return common_1.IpUtils.hasIp(net, addr);
287
+ }
288
+ catch (e) {
289
+ // Invalid net, or a source that is not an IP address
290
+ return false;
291
+ }
292
+ });
293
+ };
294
+ 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.15.4",
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.15.4",
24
+ "@routr/location": "^2.15.4",
25
+ "@routr/processor": "^2.15.4",
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": "3d4a72191d5ac315429570e562d20fb5807cc942"
45
45
  }