@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 +9 -3
- package/dist/envs.d.ts +4 -0
- package/dist/envs.js +37 -1
- package/dist/handlers/request.d.ts +2 -1
- package/dist/handlers/request.js +27 -4
- package/dist/router.js +11 -0
- package/dist/service.d.ts +15 -0
- package/dist/service.js +74 -41
- package/dist/utils.d.ts +52 -0
- package/dist/utils.js +84 -1
- package/package.json +5 -5
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
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
4
|
+
import { EdgePortTrust } from "../utils";
|
|
5
|
+
export declare const handleRequest: (location: ILocationService, apiClient?: CC.APIClient, edgePortTrust?: EdgePortTrust) => (request: MessageRequest, res: Response) => Promise<void>;
|
package/dist/handlers/request.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
61
|
-
|
|
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.
|
|
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.
|
|
24
|
-
"@routr/location": "^2.
|
|
25
|
-
"@routr/processor": "^2.
|
|
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": "
|
|
44
|
+
"gitHead": "db8a2eeb4b3f57a7f6b632d8d3378ffe53fc6cfe"
|
|
45
45
|
}
|