@bitcall/webrtc-sip-gateway 0.3.0 → 0.3.3
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/README.md +17 -0
- package/lib/firewall.js +8 -1
- package/lib/system.js +43 -2
- package/package.json +1 -1
- package/src/index.js +397 -187
- package/templates/coturn-service.yml.template +39 -0
package/README.md
CHANGED
|
@@ -2,12 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
Linux-only CLI to install and operate the Bitcall WebRTC-to-SIP gateway.
|
|
4
4
|
|
|
5
|
+
Latest updates:
|
|
6
|
+
- Gateway container now renders Kamailio `#!substdef` defaults from `.env` at
|
|
7
|
+
startup, so advertise/origin/SIP transport values are runtime-accurate.
|
|
8
|
+
- `init` and `reconfigure` stop an existing stack before preflight checks so
|
|
9
|
+
the gateway's own `:5060` listener does not trigger false port conflicts.
|
|
10
|
+
- `update` now syncs `BITCALL_GATEWAY_IMAGE` to the CLI target image tag
|
|
11
|
+
before pulling and restarting.
|
|
12
|
+
- Docker image includes `sngrep` and `tcpdump` for SIP troubleshooting.
|
|
13
|
+
- `sip-trace` opens a live SIP message viewer using `sngrep` in the container.
|
|
14
|
+
- `TURN_MODE=coturn` now generates a compose stack with a dedicated coturn
|
|
15
|
+
container.
|
|
16
|
+
|
|
5
17
|
## Install
|
|
6
18
|
|
|
7
19
|
```bash
|
|
8
20
|
sudo npm i -g @bitcall/webrtc-sip-gateway
|
|
9
21
|
```
|
|
10
22
|
|
|
23
|
+
Host requirement:
|
|
24
|
+
- Use Docker Engine with `docker compose` plugin.
|
|
25
|
+
- Snap `docker-compose` is not supported (cannot access `/opt/bitcall-gateway`).
|
|
26
|
+
|
|
11
27
|
## Main workflow
|
|
12
28
|
|
|
13
29
|
```bash
|
|
@@ -50,6 +66,7 @@ console output concise and writes command details to
|
|
|
50
66
|
- `sudo bitcall-gateway reconfigure`
|
|
51
67
|
- `sudo bitcall-gateway status`
|
|
52
68
|
- `sudo bitcall-gateway logs [-f] [service]`
|
|
69
|
+
- `sudo bitcall-gateway sip-trace`
|
|
53
70
|
- `sudo bitcall-gateway cert status`
|
|
54
71
|
- `sudo bitcall-gateway cert renew`
|
|
55
72
|
- `sudo bitcall-gateway cert install --cert /path/cert.pem --key /path/key.pem`
|
package/lib/firewall.js
CHANGED
|
@@ -107,11 +107,18 @@ function buildNftRuleset(options = {}) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
function persistIp6tables(d) {
|
|
110
|
+
const aptEnv = {
|
|
111
|
+
...process.env,
|
|
112
|
+
DEBIAN_FRONTEND: "noninteractive",
|
|
113
|
+
NEEDRESTART_MODE: "a",
|
|
114
|
+
};
|
|
115
|
+
|
|
110
116
|
if (!d.commandExists("netfilter-persistent")) {
|
|
111
|
-
d.run("apt-get", ["update"], { stdio: "inherit" });
|
|
117
|
+
d.run("apt-get", ["update"], { stdio: "inherit", env: aptEnv });
|
|
112
118
|
d.run("apt-get", ["install", "-y", "netfilter-persistent", "iptables-persistent"], {
|
|
113
119
|
check: false,
|
|
114
120
|
stdio: "inherit",
|
|
121
|
+
env: aptEnv,
|
|
115
122
|
});
|
|
116
123
|
}
|
|
117
124
|
|
package/lib/system.js
CHANGED
|
@@ -129,8 +129,17 @@ function portInUse(port) {
|
|
|
129
129
|
function ensureDockerInstalled(options = {}) {
|
|
130
130
|
const exec = pickExec(options);
|
|
131
131
|
const shell = pickShell(options);
|
|
132
|
+
const dockerPath = commandExists("docker")
|
|
133
|
+
? output("sh", ["-lc", "command -v docker || true"]).trim()
|
|
134
|
+
: "";
|
|
132
135
|
|
|
133
136
|
if (commandExists("docker") && run("docker", ["info"], { check: false }).status === 0) {
|
|
137
|
+
if (dockerPath.startsWith("/snap/")) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
"Detected Docker installed via snap. This setup is not supported for Bitcall gateway. " +
|
|
140
|
+
"Install Docker Engine (docker-ce) with compose plugin (`docker compose`) and retry."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
134
143
|
return;
|
|
135
144
|
}
|
|
136
145
|
|
|
@@ -144,13 +153,45 @@ function ensureDockerInstalled(options = {}) {
|
|
|
144
153
|
|
|
145
154
|
function ensureComposePlugin(options = {}) {
|
|
146
155
|
const exec = pickExec(options);
|
|
156
|
+
const dockerComposePath = commandExists("docker-compose")
|
|
157
|
+
? output("sh", ["-lc", "command -v docker-compose || true"]).trim()
|
|
158
|
+
: "";
|
|
159
|
+
const snapDockerCompose = dockerComposePath.startsWith("/snap/");
|
|
147
160
|
|
|
148
161
|
if (run("docker", ["compose", "version"], { check: false }).status === 0) {
|
|
149
162
|
return;
|
|
150
163
|
}
|
|
151
164
|
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
if (commandExists("docker-compose") && run("docker-compose", ["version"], { check: false }).status === 0) {
|
|
166
|
+
if (snapDockerCompose) {
|
|
167
|
+
try {
|
|
168
|
+
exec("apt-get", ["update"]);
|
|
169
|
+
exec("apt-get", ["install", "-y", "docker-compose-plugin"]);
|
|
170
|
+
} catch (_error) {
|
|
171
|
+
// Fall through to explicit compatibility error below.
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (run("docker", ["compose", "version"], { check: false }).status === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
throw new Error(
|
|
179
|
+
"Detected snap docker-compose only. It cannot access /opt/bitcall-gateway due snap confinement. " +
|
|
180
|
+
"Install Docker's compose plugin (`docker compose`) and retry."
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
exec("apt-get", ["update"]);
|
|
188
|
+
exec("apt-get", ["install", "-y", "docker-compose-plugin"]);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
if (commandExists("docker-compose") && run("docker-compose", ["version"], { check: false }).status === 0) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
154
195
|
}
|
|
155
196
|
|
|
156
197
|
module.exports = {
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
const fs = require("fs");
|
|
5
5
|
const path = require("path");
|
|
6
|
-
const { Command } = require("commander");
|
|
7
6
|
|
|
8
7
|
const {
|
|
9
8
|
GATEWAY_DIR,
|
|
@@ -41,14 +40,65 @@ const {
|
|
|
41
40
|
} = require("../lib/firewall");
|
|
42
41
|
|
|
43
42
|
const PACKAGE_VERSION = require("../package.json").version;
|
|
43
|
+
// -- ANSI color helpers (zero dependencies) --
|
|
44
|
+
const _c = {
|
|
45
|
+
reset: "\x1b[0m",
|
|
46
|
+
bold: "\x1b[1m",
|
|
47
|
+
dim: "\x1b[2m",
|
|
48
|
+
red: "\x1b[31m",
|
|
49
|
+
green: "\x1b[32m",
|
|
50
|
+
yellow: "\x1b[33m",
|
|
51
|
+
cyan: "\x1b[36m",
|
|
52
|
+
white: "\x1b[37m",
|
|
53
|
+
};
|
|
54
|
+
const _color = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
55
|
+
function clr(style, text) {
|
|
56
|
+
return _color ? `${style}${text}${_c.reset}` : text;
|
|
57
|
+
}
|
|
58
|
+
|
|
44
59
|
const INSTALL_LOG_PATH = "/var/log/bitcall-gateway-install.log";
|
|
45
60
|
|
|
46
61
|
function printBanner() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
62
|
+
const v = `v${PACKAGE_VERSION}`;
|
|
63
|
+
|
|
64
|
+
if (!_color) {
|
|
65
|
+
console.log("\n BITCALL\n WebRTC → SIP Gateway " + v);
|
|
66
|
+
console.log(" one-line deploy · any provider\n");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Block-letter BITCALL — each line is one string, do not modify
|
|
71
|
+
const art = [
|
|
72
|
+
"██████ ██ ████████ ██████ █████ ██ ██",
|
|
73
|
+
"██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
74
|
+
"██████ ██ ██ ██ ███████ ██ ██",
|
|
75
|
+
"██ ██ ██ ██ ██ ██ ██ ██ ██",
|
|
76
|
+
"██████ ██ ██ ██████ ██ ██ ███████ ███████",
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const W = 55; // inner box width
|
|
80
|
+
const top = ` ${_c.cyan}╔${"═".repeat(W)}╗${_c.reset}`;
|
|
81
|
+
const bot = ` ${_c.cyan}╚${"═".repeat(W)}╝${_c.reset}`;
|
|
82
|
+
const blank = ` ${_c.cyan}║${_c.reset}${" ".repeat(W)}${_c.cyan}║${_c.reset}`;
|
|
83
|
+
|
|
84
|
+
function row(content, pad) {
|
|
85
|
+
const visible = content.replace(/\x1b\[[0-9;]*m/g, "");
|
|
86
|
+
const fill = Math.max(0, W - (pad || 0) - visible.length);
|
|
87
|
+
return ` ${_c.cyan}║${_c.reset}${"".padEnd(pad || 0)}${content}${" ".repeat(fill)}${_c.cyan}║${_c.reset}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log("");
|
|
91
|
+
console.log(top);
|
|
92
|
+
console.log(blank);
|
|
93
|
+
for (const line of art) {
|
|
94
|
+
console.log(row(`${_c.bold}${_c.white}${line}${_c.reset}`, 3));
|
|
95
|
+
}
|
|
96
|
+
console.log(blank);
|
|
97
|
+
console.log(row(`${_c.dim}WebRTC → SIP Gateway${_c.reset}`, 3));
|
|
98
|
+
console.log(row(`${_c.dim}one-line deploy · any provider${_c.reset} ${_c.dim}${v}${_c.reset}`, 3));
|
|
99
|
+
console.log(blank);
|
|
100
|
+
console.log(bot);
|
|
101
|
+
console.log("");
|
|
52
102
|
}
|
|
53
103
|
|
|
54
104
|
function createInstallContext(options = {}) {
|
|
@@ -104,15 +154,15 @@ function createInstallContext(options = {}) {
|
|
|
104
154
|
|
|
105
155
|
async function step(label, fn) {
|
|
106
156
|
state.index += 1;
|
|
107
|
-
console.log(`[${state.index}/${steps.length}] ${label}...`);
|
|
157
|
+
console.log(` ${clr(_c.dim, `[${state.index}/${steps.length}]`)} ${clr(_c.bold, label)}...`);
|
|
108
158
|
const started = Date.now();
|
|
109
159
|
try {
|
|
110
160
|
const value = await fn();
|
|
111
161
|
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
|
112
|
-
console.log(
|
|
162
|
+
console.log(` ${clr(_c.green, "✓")} ${clr(_c.bold, label)} ${clr(_c.dim, `(${elapsed}s)`)}\n`);
|
|
113
163
|
return value;
|
|
114
164
|
} catch (error) {
|
|
115
|
-
console.error(
|
|
165
|
+
console.error(` ${clr(_c.red, "✗")} ${clr(_c.bold + _c.red, label + " failed:")} ${error.message}`);
|
|
116
166
|
if (!verbose) {
|
|
117
167
|
console.error(`Install log: ${INSTALL_LOG_PATH}`);
|
|
118
168
|
}
|
|
@@ -135,6 +185,13 @@ function detectComposeCommand(exec = run) {
|
|
|
135
185
|
return { command: "docker", prefixArgs: ["compose"] };
|
|
136
186
|
}
|
|
137
187
|
if (exec("docker-compose", ["version"], { check: false }).status === 0) {
|
|
188
|
+
const composePath = output("sh", ["-lc", "command -v docker-compose || true"]).trim();
|
|
189
|
+
if (composePath.startsWith("/snap/")) {
|
|
190
|
+
throw new Error(
|
|
191
|
+
"Detected snap docker-compose only. Install Docker compose plugin (`docker compose`) " +
|
|
192
|
+
"because snap docker-compose cannot access /opt/bitcall-gateway."
|
|
193
|
+
);
|
|
194
|
+
}
|
|
138
195
|
return { command: "docker-compose", prefixArgs: [] };
|
|
139
196
|
}
|
|
140
197
|
throw new Error("docker compose not found. Install Docker compose plugin.");
|
|
@@ -142,7 +199,7 @@ function detectComposeCommand(exec = run) {
|
|
|
142
199
|
|
|
143
200
|
function runCompose(args, options = {}, exec = run) {
|
|
144
201
|
const compose = detectComposeCommand(exec);
|
|
145
|
-
return exec(compose.command, [...compose.prefixArgs, ...args], {
|
|
202
|
+
return exec(compose.command, [...compose.prefixArgs, "-f", COMPOSE_PATH, ...args], {
|
|
146
203
|
cwd: GATEWAY_DIR,
|
|
147
204
|
...options,
|
|
148
205
|
});
|
|
@@ -602,32 +659,27 @@ async function runPreflight(ctx) {
|
|
|
602
659
|
};
|
|
603
660
|
}
|
|
604
661
|
|
|
605
|
-
function printSummary(config,
|
|
662
|
+
function printSummary(config, notes) {
|
|
606
663
|
const allowedCount = countAllowedDomains(config.allowedDomains);
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
console.log("
|
|
614
|
-
console.log(
|
|
615
|
-
console.log(
|
|
616
|
-
console.log(
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
console.log(
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (securityNotes.length > 0) {
|
|
628
|
-
console.log("\nInfo:");
|
|
629
|
-
for (const note of securityNotes) {
|
|
630
|
-
console.log(` ℹ ${note}`);
|
|
664
|
+
console.log(`\n${clr(_c.bold + _c.cyan, " ┌─ Configuration ──────────────────────────────┐")}`);
|
|
665
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Domain:")} ${config.domain}`);
|
|
666
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Environment:")} ${config.bitcallEnv}`);
|
|
667
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Routing:")} ${config.routingMode}`);
|
|
668
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Allowlist:")} ${allowedCount > 0 ? config.allowedDomains : "(any)"}`);
|
|
669
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Origin:")} ${config.webphoneOrigin === "*" ? "(any)" : config.webphoneOrigin}`);
|
|
670
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "SIP source IPs:")} ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
|
|
671
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "TLS:")} ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
|
|
672
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "TURN:")} ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
|
|
673
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Media:")} ${config.mediaIpv4Only === "1" ? "IPv4-only (IPv6 media blocked)" : "dual-stack"}`);
|
|
674
|
+
if (config.configureUfw) {
|
|
675
|
+
console.log(`${clr(_c.cyan, " │")} ${clr(_c.dim, "Firewall:")} UFW enabled`);
|
|
676
|
+
}
|
|
677
|
+
console.log(`${clr(_c.bold + _c.cyan, " └─────────────────────────────────────────────┘")}`);
|
|
678
|
+
|
|
679
|
+
if (notes.length > 0) {
|
|
680
|
+
console.log("");
|
|
681
|
+
for (const note of notes) {
|
|
682
|
+
console.log(` ${clr(_c.dim, "ℹ")} ${clr(_c.dim, note)}`);
|
|
631
683
|
}
|
|
632
684
|
}
|
|
633
685
|
}
|
|
@@ -950,9 +1002,13 @@ function renderEnvContent(config, tlsCert, tlsKey) {
|
|
|
950
1002
|
return content + extra;
|
|
951
1003
|
}
|
|
952
1004
|
|
|
953
|
-
function writeComposeTemplate() {
|
|
954
|
-
|
|
955
|
-
|
|
1005
|
+
function writeComposeTemplate(config = {}) {
|
|
1006
|
+
let compose = readTemplate("docker-compose.yml.template").trimEnd();
|
|
1007
|
+
if (config.turnMode === "coturn") {
|
|
1008
|
+
const coturn = readTemplate("coturn-service.yml.template").trimEnd();
|
|
1009
|
+
compose = `${compose}\n\n${coturn}`;
|
|
1010
|
+
}
|
|
1011
|
+
writeFileWithMode(COMPOSE_PATH, `${compose}\n`, 0o644);
|
|
956
1012
|
}
|
|
957
1013
|
|
|
958
1014
|
function provisionCustomCert(config) {
|
|
@@ -1011,187 +1067,300 @@ function installGatewayService(exec = run) {
|
|
|
1011
1067
|
exec("systemctl", ["enable", "--now", SERVICE_NAME]);
|
|
1012
1068
|
}
|
|
1013
1069
|
|
|
1070
|
+
function backupExistingGatewayConfig() {
|
|
1071
|
+
if (!fs.existsSync(ENV_PATH) || !fs.existsSync(COMPOSE_PATH)) {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
envContent: fs.readFileSync(ENV_PATH, "utf8"),
|
|
1076
|
+
composeContent: fs.readFileSync(COMPOSE_PATH, "utf8"),
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function restoreGatewayConfigFromBackup(backup, contextLabel = "Operation") {
|
|
1081
|
+
if (!backup || backup.envContent == null || backup.composeContent == null) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
try {
|
|
1085
|
+
ensureInstallLayout();
|
|
1086
|
+
writeFileWithMode(ENV_PATH, backup.envContent, 0o600);
|
|
1087
|
+
writeFileWithMode(COMPOSE_PATH, backup.composeContent, 0o644);
|
|
1088
|
+
runCompose(["up", "-d", "--remove-orphans"], { check: false, stdio: "pipe" });
|
|
1089
|
+
console.error(`${contextLabel} failed. Restored previous gateway config and attempted restart.`);
|
|
1090
|
+
} catch (restoreError) {
|
|
1091
|
+
console.error(
|
|
1092
|
+
`${contextLabel} failed and automatic restore also failed: ${restoreError.message}`
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1014
1097
|
async function initCommand(initOptions = {}) {
|
|
1015
1098
|
printBanner();
|
|
1016
|
-
const
|
|
1099
|
+
const previousConfig = backupExistingGatewayConfig();
|
|
1017
1100
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1101
|
+
// If re-running init on an existing installation, stop the current
|
|
1102
|
+
// gateway so our own ports don't fail the preflight check.
|
|
1103
|
+
if (previousConfig) {
|
|
1104
|
+
console.log("Existing gateway detected. Stopping for re-initialization...\n");
|
|
1105
|
+
try {
|
|
1106
|
+
runCompose(["down"], { stdio: "pipe" });
|
|
1107
|
+
} catch (_e) {
|
|
1108
|
+
// Ignore — gateway may already be stopped or config may be corrupted
|
|
1109
|
+
}
|
|
1026
1110
|
}
|
|
1027
1111
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1112
|
+
const ctx = createInstallContext(initOptions);
|
|
1113
|
+
try {
|
|
1114
|
+
let preflight;
|
|
1115
|
+
await ctx.step("Checks", async () => {
|
|
1116
|
+
preflight = await runPreflight(ctx);
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
let existingEnv = {};
|
|
1120
|
+
if (fs.existsSync(ENV_PATH)) {
|
|
1121
|
+
existingEnv = loadEnvFile(ENV_PATH);
|
|
1122
|
+
}
|
|
1032
1123
|
|
|
1033
|
-
|
|
1124
|
+
let config;
|
|
1125
|
+
await ctx.step("Config", async () => {
|
|
1126
|
+
config = await runWizard(existingEnv, preflight, initOptions);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
ensureInstallLayout();
|
|
1034
1130
|
|
|
1035
|
-
|
|
1036
|
-
|
|
1131
|
+
let tlsCertPath;
|
|
1132
|
+
let tlsKeyPath;
|
|
1037
1133
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1134
|
+
await ctx.step("Firewall", async () => {
|
|
1135
|
+
if (config.configureUfw) {
|
|
1136
|
+
const ufwReady = await ensureUfwAvailable(ctx);
|
|
1137
|
+
if (ufwReady) {
|
|
1138
|
+
configureFirewall(config, ctx.exec);
|
|
1139
|
+
} else {
|
|
1140
|
+
console.log("Skipping UFW configuration by request.");
|
|
1141
|
+
config.configureUfw = false;
|
|
1142
|
+
printRequiredPorts(config);
|
|
1143
|
+
}
|
|
1043
1144
|
} else {
|
|
1044
|
-
console.log("Skipping UFW configuration by request.");
|
|
1045
|
-
config.configureUfw = false;
|
|
1046
1145
|
printRequiredPorts(config);
|
|
1047
1146
|
}
|
|
1048
|
-
} else {
|
|
1049
|
-
printRequiredPorts(config);
|
|
1050
|
-
}
|
|
1051
1147
|
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1148
|
+
if (config.mediaIpv4Only === "1") {
|
|
1149
|
+
try {
|
|
1150
|
+
const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
|
|
1151
|
+
console.log(`Applied IPv6 media block rules (${applied.backend}).`);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
console.error(`IPv6 media block setup failed: ${error.message}`);
|
|
1154
|
+
const proceed = await confirmContinueWithoutMediaBlock();
|
|
1155
|
+
if (!proceed) {
|
|
1156
|
+
throw new Error(
|
|
1157
|
+
"Initialization stopped because media IPv4-only firewall rules were not applied."
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
config.mediaIpv4Only = "0";
|
|
1161
|
+
console.log("Continuing without IPv6 media block rules.");
|
|
1061
1162
|
}
|
|
1062
|
-
config.mediaIpv4Only = "0";
|
|
1063
|
-
console.log("Continuing without IPv6 media block rules.");
|
|
1064
1163
|
}
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1164
|
+
});
|
|
1067
1165
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1166
|
+
await ctx.step("TLS", async () => {
|
|
1167
|
+
if (config.tlsMode === "custom") {
|
|
1168
|
+
const custom = provisionCustomCert(config);
|
|
1169
|
+
tlsCertPath = custom.certPath;
|
|
1170
|
+
tlsKeyPath = custom.keyPath;
|
|
1171
|
+
} else if (config.tlsMode === "dev-self-signed") {
|
|
1172
|
+
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1173
|
+
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1174
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
1175
|
+
console.log("WARNING: Self-signed certificates do not work with browsers.");
|
|
1176
|
+
console.log("WebSocket connections will be rejected. Use --dev for local testing only.");
|
|
1177
|
+
console.log("For browser access, re-run with Let's Encrypt: sudo bitcall-gateway init");
|
|
1178
|
+
} else {
|
|
1179
|
+
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1180
|
+
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
1181
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
|
|
1182
|
+
}
|
|
1085
1183
|
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1184
|
+
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
1185
|
+
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
1186
|
+
writeComposeTemplate(config);
|
|
1187
|
+
});
|
|
1090
1188
|
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1189
|
+
await ctx.step("Start", async () => {
|
|
1190
|
+
startGatewayStack(ctx.exec);
|
|
1191
|
+
if (config.tlsMode === "letsencrypt") {
|
|
1192
|
+
runLetsEncrypt(config, ctx.exec);
|
|
1193
|
+
}
|
|
1194
|
+
installGatewayService(ctx.exec);
|
|
1195
|
+
});
|
|
1098
1196
|
|
|
1099
|
-
|
|
1197
|
+
await ctx.step("Done", async () => {});
|
|
1100
1198
|
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1199
|
+
console.log("");
|
|
1200
|
+
console.log(clr(_c.bold + _c.green, " ╔══════════════════════════════════════╗"));
|
|
1201
|
+
console.log(
|
|
1202
|
+
clr(_c.bold + _c.green, " ║") +
|
|
1203
|
+
" Gateway is " +
|
|
1204
|
+
clr(_c.bold + _c.green, "READY") +
|
|
1205
|
+
" " +
|
|
1206
|
+
clr(_c.bold + _c.green, "║")
|
|
1207
|
+
);
|
|
1208
|
+
console.log(clr(_c.bold + _c.green, " ╚══════════════════════════════════════╝"));
|
|
1209
|
+
console.log("");
|
|
1210
|
+
console.log(` ${clr(_c.bold, "WSS")} ${clr(_c.cyan, `wss://${config.domain}`)}`);
|
|
1211
|
+
if (config.turnMode === "coturn") {
|
|
1212
|
+
console.log(` ${clr(_c.bold, "TURN")} ${clr(_c.cyan, `https://${config.domain}/turn-credentials`)}`);
|
|
1213
|
+
console.log(` ${clr(_c.bold, "STUN")} ${clr(_c.cyan, `stun:${config.domain}:${config.turnUdpPort}`)}`);
|
|
1214
|
+
console.log(` ${clr(_c.bold, "TURN")} ${clr(_c.cyan, `turn:${config.domain}:${config.turnUdpPort}`)}`);
|
|
1215
|
+
console.log(
|
|
1216
|
+
` ${clr(_c.bold, "TURNS")} ${clr(_c.cyan, `turns:${config.domain}:${config.turnsTcpPort}`)}`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
if (!ctx.verbose) {
|
|
1220
|
+
console.log(` ${clr(_c.dim, `Log: ${ctx.logPath}`)}`);
|
|
1221
|
+
}
|
|
1222
|
+
console.log("");
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
if (previousConfig) {
|
|
1225
|
+
restoreGatewayConfigFromBackup(previousConfig, "Initialization");
|
|
1226
|
+
}
|
|
1227
|
+
throw error;
|
|
1108
1228
|
}
|
|
1109
1229
|
}
|
|
1110
1230
|
|
|
1111
1231
|
async function reconfigureCommand(options) {
|
|
1112
1232
|
ensureInitialized();
|
|
1113
1233
|
printBanner();
|
|
1114
|
-
const
|
|
1234
|
+
const previousConfig = backupExistingGatewayConfig();
|
|
1115
1235
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1236
|
+
// Stop the running gateway so ports are free for preflight.
|
|
1237
|
+
if (previousConfig) {
|
|
1238
|
+
console.log("Stopping current gateway for reconfiguration...\n");
|
|
1239
|
+
try {
|
|
1240
|
+
runCompose(["down"], { stdio: "pipe" });
|
|
1241
|
+
} catch (_e) {
|
|
1242
|
+
// Ignore — gateway may already be stopped or config may be corrupted
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1120
1245
|
|
|
1121
|
-
const
|
|
1246
|
+
const ctx = createInstallContext(options);
|
|
1247
|
+
try {
|
|
1248
|
+
let preflight;
|
|
1249
|
+
await ctx.step("Checks", async () => {
|
|
1250
|
+
preflight = await runPreflight(ctx);
|
|
1251
|
+
});
|
|
1122
1252
|
|
|
1123
|
-
|
|
1124
|
-
await ctx.step("Config", async () => {
|
|
1125
|
-
config = await runWizard(existingEnv, preflight, options);
|
|
1126
|
-
});
|
|
1253
|
+
const existingEnv = fs.existsSync(ENV_PATH) ? loadEnvFile(ENV_PATH) : {};
|
|
1127
1254
|
|
|
1128
|
-
|
|
1255
|
+
let config;
|
|
1256
|
+
await ctx.step("Config", async () => {
|
|
1257
|
+
config = await runWizard(existingEnv, preflight, options);
|
|
1258
|
+
});
|
|
1129
1259
|
|
|
1130
|
-
|
|
1131
|
-
let tlsKeyPath;
|
|
1260
|
+
ensureInstallLayout();
|
|
1132
1261
|
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1262
|
+
let tlsCertPath;
|
|
1263
|
+
let tlsKeyPath;
|
|
1264
|
+
|
|
1265
|
+
await ctx.step("Firewall", async () => {
|
|
1266
|
+
if (config.configureUfw) {
|
|
1267
|
+
const ufwReady = await ensureUfwAvailable(ctx);
|
|
1268
|
+
if (ufwReady) {
|
|
1269
|
+
configureFirewall(config, ctx.exec);
|
|
1270
|
+
} else {
|
|
1271
|
+
config.configureUfw = false;
|
|
1272
|
+
printRequiredPorts(config);
|
|
1273
|
+
}
|
|
1138
1274
|
} else {
|
|
1139
|
-
config.configureUfw = false;
|
|
1140
1275
|
printRequiredPorts(config);
|
|
1141
1276
|
}
|
|
1142
|
-
} else {
|
|
1143
|
-
printRequiredPorts(config);
|
|
1144
|
-
}
|
|
1145
1277
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1278
|
+
if (config.mediaIpv4Only === "1") {
|
|
1279
|
+
try {
|
|
1280
|
+
const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
|
|
1281
|
+
console.log(`Applied IPv6 media block rules (${applied.backend}).`);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
console.error(`IPv6 media block setup failed: ${error.message}`);
|
|
1284
|
+
config.mediaIpv4Only = "0";
|
|
1285
|
+
}
|
|
1153
1286
|
}
|
|
1154
|
-
}
|
|
1155
|
-
});
|
|
1287
|
+
});
|
|
1156
1288
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
} else {
|
|
1167
|
-
// Keep existing LE cert if domain matches, otherwise re-provision.
|
|
1168
|
-
const envMap = loadEnvFile(ENV_PATH);
|
|
1169
|
-
if (envMap.TLS_MODE === "letsencrypt" && envMap.TLS_CERT && fs.existsSync(envMap.TLS_CERT)) {
|
|
1170
|
-
tlsCertPath = envMap.TLS_CERT;
|
|
1171
|
-
tlsKeyPath = envMap.TLS_KEY;
|
|
1289
|
+
await ctx.step("TLS", async () => {
|
|
1290
|
+
if (config.tlsMode === "custom") {
|
|
1291
|
+
const custom = provisionCustomCert(config);
|
|
1292
|
+
tlsCertPath = custom.certPath;
|
|
1293
|
+
tlsKeyPath = custom.keyPath;
|
|
1294
|
+
} else if (config.tlsMode === "dev-self-signed") {
|
|
1295
|
+
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1296
|
+
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1297
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
1172
1298
|
} else {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1299
|
+
// Keep existing LE cert only when domain is unchanged and cert files still exist.
|
|
1300
|
+
const envMap = loadEnvFile(ENV_PATH);
|
|
1301
|
+
const sameDomain =
|
|
1302
|
+
(envMap.DOMAIN || "").trim().toLowerCase() === (config.domain || "").trim().toLowerCase();
|
|
1303
|
+
if (
|
|
1304
|
+
envMap.TLS_MODE === "letsencrypt" &&
|
|
1305
|
+
sameDomain &&
|
|
1306
|
+
envMap.TLS_CERT &&
|
|
1307
|
+
envMap.TLS_KEY &&
|
|
1308
|
+
fs.existsSync(envMap.TLS_CERT) &&
|
|
1309
|
+
fs.existsSync(envMap.TLS_KEY)
|
|
1310
|
+
) {
|
|
1311
|
+
tlsCertPath = envMap.TLS_CERT;
|
|
1312
|
+
tlsKeyPath = envMap.TLS_KEY;
|
|
1313
|
+
} else {
|
|
1314
|
+
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1315
|
+
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
1316
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
|
|
1317
|
+
}
|
|
1176
1318
|
}
|
|
1177
|
-
}
|
|
1178
1319
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1320
|
+
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
1321
|
+
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
1322
|
+
writeComposeTemplate(config);
|
|
1323
|
+
});
|
|
1183
1324
|
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1325
|
+
await ctx.step("Start", async () => {
|
|
1326
|
+
startGatewayStack(ctx.exec);
|
|
1327
|
+
if (config.tlsMode === "letsencrypt" && (!tlsCertPath || !tlsCertPath.includes("letsencrypt"))) {
|
|
1328
|
+
runLetsEncrypt(config, ctx.exec);
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1190
1331
|
|
|
1191
|
-
|
|
1332
|
+
await ctx.step("Done", async () => {});
|
|
1192
1333
|
|
|
1193
|
-
|
|
1194
|
-
|
|
1334
|
+
console.log("");
|
|
1335
|
+
console.log(clr(_c.bold + _c.green, " ╔══════════════════════════════════════╗"));
|
|
1336
|
+
console.log(
|
|
1337
|
+
clr(_c.bold + _c.green, " ║") +
|
|
1338
|
+
" Gateway is " +
|
|
1339
|
+
clr(_c.bold + _c.green, "READY") +
|
|
1340
|
+
" " +
|
|
1341
|
+
clr(_c.bold + _c.green, "║")
|
|
1342
|
+
);
|
|
1343
|
+
console.log(clr(_c.bold + _c.green, " ╚══════════════════════════════════════╝"));
|
|
1344
|
+
console.log("");
|
|
1345
|
+
console.log(` ${clr(_c.bold, "WSS")} ${clr(_c.cyan, `wss://${config.domain}`)}`);
|
|
1346
|
+
if (config.turnMode === "coturn") {
|
|
1347
|
+
console.log(` ${clr(_c.bold, "TURN")} ${clr(_c.cyan, `https://${config.domain}/turn-credentials`)}`);
|
|
1348
|
+
console.log(` ${clr(_c.bold, "STUN")} ${clr(_c.cyan, `stun:${config.domain}:${config.turnUdpPort}`)}`);
|
|
1349
|
+
console.log(` ${clr(_c.bold, "TURN")} ${clr(_c.cyan, `turn:${config.domain}:${config.turnUdpPort}`)}`);
|
|
1350
|
+
console.log(
|
|
1351
|
+
` ${clr(_c.bold, "TURNS")} ${clr(_c.cyan, `turns:${config.domain}:${config.turnsTcpPort}`)}`
|
|
1352
|
+
);
|
|
1353
|
+
}
|
|
1354
|
+
if (!ctx.verbose) {
|
|
1355
|
+
console.log(` ${clr(_c.dim, `Log: ${ctx.logPath}`)}`);
|
|
1356
|
+
}
|
|
1357
|
+
console.log("");
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (previousConfig) {
|
|
1360
|
+
restoreGatewayConfigFromBackup(previousConfig, "Reconfigure");
|
|
1361
|
+
}
|
|
1362
|
+
throw error;
|
|
1363
|
+
}
|
|
1195
1364
|
}
|
|
1196
1365
|
|
|
1197
1366
|
function runSystemctl(args, fallbackComposeArgs) {
|
|
@@ -1253,8 +1422,36 @@ function logsCommand(service, options) {
|
|
|
1253
1422
|
runCompose(args, { stdio: "inherit" });
|
|
1254
1423
|
}
|
|
1255
1424
|
|
|
1425
|
+
function sipTraceCommand() {
|
|
1426
|
+
ensureInitialized();
|
|
1427
|
+
const compose = detectComposeCommand();
|
|
1428
|
+
const containerName = "bitcall-gateway";
|
|
1429
|
+
|
|
1430
|
+
// Check if sngrep is available in the container
|
|
1431
|
+
const check = run(
|
|
1432
|
+
compose.command,
|
|
1433
|
+
[...compose.prefixArgs, "exec", "-T", containerName, "which", "sngrep"],
|
|
1434
|
+
{ cwd: GATEWAY_DIR, check: false, stdio: "pipe" }
|
|
1435
|
+
);
|
|
1436
|
+
|
|
1437
|
+
if (check.status !== 0) {
|
|
1438
|
+
console.error("sngrep is not available in this gateway image.");
|
|
1439
|
+
console.error("Update to the latest image: sudo bitcall-gateway update");
|
|
1440
|
+
process.exit(1);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Run sngrep interactively inside the container
|
|
1444
|
+
const result = run(
|
|
1445
|
+
compose.command,
|
|
1446
|
+
[...compose.prefixArgs, "exec", containerName, "sngrep"],
|
|
1447
|
+
{ cwd: GATEWAY_DIR, stdio: "inherit" }
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
process.exit(result.status || 0);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1256
1453
|
function formatMark(state) {
|
|
1257
|
-
return state ? "✓" : "✗";
|
|
1454
|
+
return state ? clr(_c.green, "✓") : clr(_c.red, "✗");
|
|
1258
1455
|
}
|
|
1259
1456
|
|
|
1260
1457
|
function statusCommand() {
|
|
@@ -1271,6 +1468,12 @@ function statusCommand() {
|
|
|
1271
1468
|
"-lc",
|
|
1272
1469
|
"docker ps --filter name=^bitcall-gateway$ --format '{{.Status}}'",
|
|
1273
1470
|
]);
|
|
1471
|
+
const containerUp = Boolean(containerStatus);
|
|
1472
|
+
const coturnStatus = output("sh", [
|
|
1473
|
+
"-lc",
|
|
1474
|
+
"docker ps --filter name=^bitcall-coturn$ --format '{{.Status}}'",
|
|
1475
|
+
]);
|
|
1476
|
+
const coturnUp = Boolean(coturnStatus);
|
|
1274
1477
|
|
|
1275
1478
|
const p80 = portInUse(Number.parseInt(envMap.ACME_LISTEN_PORT || "80", 10));
|
|
1276
1479
|
const p443 = portInUse(Number.parseInt(envMap.WSS_LISTEN_PORT || "443", 10));
|
|
@@ -1292,10 +1495,13 @@ function statusCommand() {
|
|
|
1292
1495
|
|
|
1293
1496
|
const mediaStatus = isMediaIpv4OnlyRulesPresent();
|
|
1294
1497
|
|
|
1295
|
-
console.log("Bitcall Gateway Status\n
|
|
1498
|
+
console.log(`\n${clr(_c.bold + _c.cyan, " Bitcall Gateway Status")}\n`);
|
|
1296
1499
|
console.log(`Gateway service: ${formatMark(active)} ${active ? "running" : "stopped"}`);
|
|
1297
1500
|
console.log(`Auto-start: ${formatMark(enabled)} ${enabled ? "enabled" : "disabled"}`);
|
|
1298
|
-
console.log(`Container: ${containerStatus || "not running"}`);
|
|
1501
|
+
console.log(`Container: ${formatMark(containerUp)} ${containerStatus || "not running"}`);
|
|
1502
|
+
if (envMap.TURN_MODE === "coturn") {
|
|
1503
|
+
console.log(`Coturn container: ${formatMark(coturnUp)} ${coturnStatus || "not running"}`);
|
|
1504
|
+
}
|
|
1299
1505
|
console.log("");
|
|
1300
1506
|
console.log(`Port ${envMap.ACME_LISTEN_PORT || "80"}: ${formatMark(p80.inUse)} listening`);
|
|
1301
1507
|
console.log(`Port ${envMap.WSS_LISTEN_PORT || "443"}: ${formatMark(p443.inUse)} listening`);
|
|
@@ -1410,19 +1616,18 @@ function updateCommand() {
|
|
|
1410
1616
|
const targetImage = DEFAULT_GATEWAY_IMAGE;
|
|
1411
1617
|
|
|
1412
1618
|
if (currentImage === targetImage) {
|
|
1413
|
-
console.log(
|
|
1619
|
+
console.log(`${clr(_c.green, "✓")} Image is current: ${clr(_c.dim, targetImage)}`);
|
|
1414
1620
|
console.log("Checking for newer image layers...");
|
|
1415
1621
|
} else {
|
|
1416
|
-
console.log(
|
|
1622
|
+
console.log(`${clr(_c.yellow, "→")} Updating: ${clr(_c.dim, currentImage || "(none)")} → ${clr(_c.bold, targetImage)}`);
|
|
1417
1623
|
updateGatewayEnv({ BITCALL_GATEWAY_IMAGE: targetImage });
|
|
1418
1624
|
}
|
|
1419
1625
|
|
|
1420
1626
|
runCompose(["pull"], { stdio: "inherit" });
|
|
1421
1627
|
runSystemctl(["reload", SERVICE_NAME], ["up", "-d", "--remove-orphans"]);
|
|
1422
1628
|
|
|
1423
|
-
console.log(`\
|
|
1424
|
-
console.log("To update the CLI
|
|
1425
|
-
console.log(" sudo npm i -g @bitcall/webrtc-sip-gateway@latest");
|
|
1629
|
+
console.log(`\n${clr(_c.green, "✓")} Gateway updated to ${clr(_c.bold, PACKAGE_VERSION)}.`);
|
|
1630
|
+
console.log(clr(_c.dim, " To update the CLI: sudo npm i -g @bitcall/webrtc-sip-gateway@latest"));
|
|
1426
1631
|
}
|
|
1427
1632
|
|
|
1428
1633
|
function mediaStatusCommand() {
|
|
@@ -1531,12 +1736,12 @@ async function uninstallCommand(options) {
|
|
|
1531
1736
|
fs.rmSync(GATEWAY_DIR, { recursive: true, force: true });
|
|
1532
1737
|
}
|
|
1533
1738
|
|
|
1534
|
-
console.log("Gateway uninstalled.");
|
|
1739
|
+
console.log(`\n${clr(_c.green, "✓")} ${clr(_c.bold, "Gateway uninstalled.")}`);
|
|
1535
1740
|
const domain = uninstallEnv.DOMAIN || "";
|
|
1536
|
-
console.log("
|
|
1741
|
+
console.log(`\n${clr(_c.dim, "To remove the CLI:")}`);
|
|
1537
1742
|
console.log(" sudo npm uninstall -g @bitcall/webrtc-sip-gateway");
|
|
1538
1743
|
if (domain && uninstallEnv.TLS_MODE === "letsencrypt") {
|
|
1539
|
-
console.log("
|
|
1744
|
+
console.log(`\n${clr(_c.dim, "To remove Let's Encrypt certificate:")}`);
|
|
1540
1745
|
console.log(` sudo certbot delete --cert-name ${domain}`);
|
|
1541
1746
|
}
|
|
1542
1747
|
}
|
|
@@ -1552,6 +1757,7 @@ function configCommand() {
|
|
|
1552
1757
|
}
|
|
1553
1758
|
|
|
1554
1759
|
function buildProgram() {
|
|
1760
|
+
const { Command } = require("commander");
|
|
1555
1761
|
const program = new Command();
|
|
1556
1762
|
|
|
1557
1763
|
program
|
|
@@ -1582,6 +1788,10 @@ function buildProgram() {
|
|
|
1582
1788
|
.option("--no-follow", "Print and exit")
|
|
1583
1789
|
.option("--tail <lines>", "Number of lines", "200")
|
|
1584
1790
|
.action(logsCommand);
|
|
1791
|
+
program
|
|
1792
|
+
.command("sip-trace")
|
|
1793
|
+
.description("Live SIP message viewer (sngrep)")
|
|
1794
|
+
.action(sipTraceCommand);
|
|
1585
1795
|
|
|
1586
1796
|
const cert = program.command("cert").description("Certificate operations");
|
|
1587
1797
|
cert.command("status").description("Show current certificate info").action(certStatusCommand);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
coturn:
|
|
2
|
+
image: coturn/coturn:latest
|
|
3
|
+
container_name: bitcall-coturn
|
|
4
|
+
network_mode: host
|
|
5
|
+
read_only: true
|
|
6
|
+
tmpfs:
|
|
7
|
+
- /tmp:rw,nosuid,nodev
|
|
8
|
+
security_opt:
|
|
9
|
+
- no-new-privileges:true
|
|
10
|
+
volumes:
|
|
11
|
+
- ${TLS_CERT}:/etc/ssl/cert.pem:ro
|
|
12
|
+
- ${TLS_KEY}:/etc/ssl/key.pem:ro
|
|
13
|
+
command: >
|
|
14
|
+
-n
|
|
15
|
+
--realm=${DOMAIN}
|
|
16
|
+
--external-ip=${PUBLIC_IP}
|
|
17
|
+
--listening-port=${TURN_UDP_PORT:-3478}
|
|
18
|
+
--tls-listening-port=${TURNS_TCP_PORT:-5349}
|
|
19
|
+
--cert=/etc/ssl/cert.pem
|
|
20
|
+
--pkey=/etc/ssl/key.pem
|
|
21
|
+
--use-auth-secret
|
|
22
|
+
--static-auth-secret=${TURN_SECRET}
|
|
23
|
+
--min-port=${TURN_RELAY_MIN_PORT:-49152}
|
|
24
|
+
--max-port=${TURN_RELAY_MAX_PORT:-49252}
|
|
25
|
+
--no-cli
|
|
26
|
+
--denied-peer-ip=0.0.0.0-0.255.255.255
|
|
27
|
+
--denied-peer-ip=10.0.0.0-10.255.255.255
|
|
28
|
+
--denied-peer-ip=127.0.0.0-127.255.255.255
|
|
29
|
+
--denied-peer-ip=172.16.0.0-172.31.255.255
|
|
30
|
+
--denied-peer-ip=192.168.0.0-192.168.255.255
|
|
31
|
+
--channel-lifetime=600
|
|
32
|
+
--permission-lifetime=300
|
|
33
|
+
--stale-nonce=600
|
|
34
|
+
restart: always
|
|
35
|
+
logging:
|
|
36
|
+
driver: json-file
|
|
37
|
+
options:
|
|
38
|
+
max-size: "10m"
|
|
39
|
+
max-file: "3"
|