@bitcall/webrtc-sip-gateway 0.2.7 → 0.3.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/README.md +14 -4
- package/lib/constants.js +2 -1
- package/package.json +1 -1
- package/src/index.js +319 -163
package/README.md
CHANGED
|
@@ -5,13 +5,13 @@ Linux-only CLI to install and operate the Bitcall WebRTC-to-SIP gateway.
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
sudo npm i -g @bitcall/webrtc-sip-gateway
|
|
8
|
+
sudo npm i -g @bitcall/webrtc-sip-gateway
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Main workflow
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
sudo bitcall-gateway init
|
|
14
|
+
sudo bitcall-gateway init
|
|
15
15
|
sudo bitcall-gateway status
|
|
16
16
|
sudo bitcall-gateway logs -f
|
|
17
17
|
sudo bitcall-gateway media status
|
|
@@ -22,8 +22,13 @@ ports only. Host IPv6 remains enabled for signaling and non-media traffic.
|
|
|
22
22
|
Backend selection prefers nftables on non-UFW hosts and uses ip6tables when UFW
|
|
23
23
|
is active.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
Default `init` runs in production profile with universal routing:
|
|
26
|
+
- `BITCALL_ENV=production`
|
|
27
|
+
- `ROUTING_MODE=universal`
|
|
28
|
+
- provider allowlist/origin/source IPs are permissive by default
|
|
29
|
+
|
|
30
|
+
Use `sudo bitcall-gateway init --advanced` for full security/provider controls.
|
|
31
|
+
Use `sudo bitcall-gateway init --dev` for local testing only.
|
|
27
32
|
Use `--verbose` to stream apt/docker output during install. Default mode keeps
|
|
28
33
|
console output concise and writes command details to
|
|
29
34
|
`/var/log/bitcall-gateway-install.log`.
|
|
@@ -38,6 +43,11 @@ console output concise and writes command details to
|
|
|
38
43
|
- `sudo bitcall-gateway up`
|
|
39
44
|
- `sudo bitcall-gateway down`
|
|
40
45
|
- `sudo bitcall-gateway restart`
|
|
46
|
+
- `sudo bitcall-gateway pause`
|
|
47
|
+
- `sudo bitcall-gateway resume`
|
|
48
|
+
- `sudo bitcall-gateway enable`
|
|
49
|
+
- `sudo bitcall-gateway disable`
|
|
50
|
+
- `sudo bitcall-gateway reconfigure`
|
|
41
51
|
- `sudo bitcall-gateway status`
|
|
42
52
|
- `sudo bitcall-gateway logs [-f] [service]`
|
|
43
53
|
- `sudo bitcall-gateway cert status`
|
package/lib/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const path = require("path");
|
|
4
|
+
const PKG_VERSION = require("../package.json").version;
|
|
4
5
|
|
|
5
6
|
const GATEWAY_DIR = "/opt/bitcall-gateway";
|
|
6
7
|
const SERVICE_NAME = "bitcall-gateway";
|
|
@@ -14,7 +15,7 @@ module.exports = {
|
|
|
14
15
|
SSL_DIR: path.join(GATEWAY_DIR, "ssl"),
|
|
15
16
|
ENV_PATH: path.join(GATEWAY_DIR, ".env"),
|
|
16
17
|
COMPOSE_PATH: path.join(GATEWAY_DIR, "docker-compose.yml"),
|
|
17
|
-
DEFAULT_GATEWAY_IMAGE:
|
|
18
|
+
DEFAULT_GATEWAY_IMAGE: `ghcr.io/bitcallio/webrtc-sip-gateway:${PKG_VERSION}`,
|
|
18
19
|
DEFAULT_PROVIDER_HOST: "sip.example.com",
|
|
19
20
|
DEFAULT_WEBPHONE_ORIGIN: "*",
|
|
20
21
|
RENEW_HOOK_PATH: "/etc/letsencrypt/renewal-hooks/deploy/bitcall-gateway.sh",
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -40,7 +40,7 @@ const {
|
|
|
40
40
|
isMediaIpv4OnlyRulesPresent,
|
|
41
41
|
} = require("../lib/firewall");
|
|
42
42
|
|
|
43
|
-
const PACKAGE_VERSION = "
|
|
43
|
+
const PACKAGE_VERSION = require("../package.json").version;
|
|
44
44
|
const INSTALL_LOG_PATH = "/var/log/bitcall-gateway-install.log";
|
|
45
45
|
|
|
46
46
|
function printBanner() {
|
|
@@ -327,36 +327,35 @@ function isOriginWildcard(originPattern) {
|
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
function validateProductionConfig(config) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
330
|
+
// In production, validate that the config is internally consistent.
|
|
331
|
+
// Universal mode (open domains, open origin) is a valid production config.
|
|
332
|
+
|
|
333
|
+
// Single-provider mode requires a valid SIP_PROVIDER_URI
|
|
334
|
+
if (config.routingMode === "single-provider") {
|
|
335
|
+
if (!(config.sipProviderUri || "").trim()) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"Single-provider mode requires SIP_PROVIDER_URI. " +
|
|
338
|
+
"Re-run: bitcall-gateway init --advanced --production"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
336
341
|
}
|
|
337
342
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
const hasTurnToken = Boolean((config.turnApiToken || "").trim());
|
|
341
|
-
if (!hasStrictOrigin && !hasTurnToken) {
|
|
342
|
-
throw new Error(
|
|
343
|
-
"Production requires a strict WEBPHONE_ORIGIN_PATTERN or TURN_API_TOKEN. Re-run: bitcall-gateway init --advanced --production"
|
|
344
|
-
);
|
|
345
|
-
}
|
|
343
|
+
// If custom cert mode, paths are required (already validated elsewhere)
|
|
344
|
+
// No other hard requirements for production.
|
|
346
345
|
}
|
|
347
346
|
|
|
348
|
-
function
|
|
349
|
-
const
|
|
347
|
+
function buildSecurityNotes(config) {
|
|
348
|
+
const notes = [];
|
|
350
349
|
if (countAllowedDomains(config.allowedDomains) === 0) {
|
|
351
|
-
|
|
350
|
+
notes.push("SIP domains: unrestricted (universal mode)");
|
|
352
351
|
}
|
|
353
352
|
if (isOriginWildcard(toOriginPattern(config.webphoneOrigin))) {
|
|
354
|
-
|
|
353
|
+
notes.push("Webphone origin: unrestricted");
|
|
355
354
|
}
|
|
356
355
|
if (!(config.sipTrustedIps || "").trim()) {
|
|
357
|
-
|
|
356
|
+
notes.push("SIP source IPs: unrestricted");
|
|
358
357
|
}
|
|
359
|
-
return
|
|
358
|
+
return notes;
|
|
360
359
|
}
|
|
361
360
|
|
|
362
361
|
function buildQuickFlowDefaults(initProfile, existing = {}) {
|
|
@@ -371,12 +370,15 @@ function buildQuickFlowDefaults(initProfile, existing = {}) {
|
|
|
371
370
|
sipTrustedIps: existing.SIP_TRUSTED_IPS || "",
|
|
372
371
|
};
|
|
373
372
|
|
|
373
|
+
// Dev mode: explicitly open everything (ignore existing restrictions)
|
|
374
374
|
if (initProfile === "dev") {
|
|
375
375
|
defaults.allowedDomains = "";
|
|
376
376
|
defaults.webphoneOrigin = "*";
|
|
377
377
|
defaults.sipTrustedIps = "";
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// Production mode: use existing values or defaults (already universal)
|
|
381
|
+
// Do not force restrictions here.
|
|
380
382
|
return defaults;
|
|
381
383
|
}
|
|
382
384
|
|
|
@@ -554,7 +556,7 @@ function normalizeInitProfile(initOptions = {}, existing = {}) {
|
|
|
554
556
|
if (initOptions.dev) {
|
|
555
557
|
return "dev";
|
|
556
558
|
}
|
|
557
|
-
return existing.BITCALL_ENV || "
|
|
559
|
+
return existing.BITCALL_ENV || "production";
|
|
558
560
|
}
|
|
559
561
|
|
|
560
562
|
async function runPreflight(ctx) {
|
|
@@ -600,19 +602,20 @@ async function runPreflight(ctx) {
|
|
|
600
602
|
};
|
|
601
603
|
}
|
|
602
604
|
|
|
603
|
-
function printSummary(config,
|
|
605
|
+
function printSummary(config, securityNotes) {
|
|
604
606
|
const allowedCount = countAllowedDomains(config.allowedDomains);
|
|
605
|
-
const
|
|
607
|
+
const providerAllowlistSummary =
|
|
608
|
+
allowedCount > 0
|
|
609
|
+
? config.allowedDomains
|
|
610
|
+
: isSingleProviderConfigured(config)
|
|
611
|
+
? "(single-provider mode)"
|
|
612
|
+
: "(any)";
|
|
606
613
|
console.log("\nSummary:");
|
|
607
614
|
console.log(` Domain: ${config.domain}`);
|
|
608
615
|
console.log(` Environment: ${config.bitcallEnv}`);
|
|
609
616
|
console.log(` Routing: ${config.routingMode}`);
|
|
610
|
-
console.log(
|
|
611
|
-
|
|
612
|
-
);
|
|
613
|
-
console.log(
|
|
614
|
-
` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
|
|
615
|
-
);
|
|
617
|
+
console.log(` Provider allowlist: ${providerAllowlistSummary}`);
|
|
618
|
+
console.log(` Webphone origin: ${config.webphoneOrigin === "*" ? "(any)" : config.webphoneOrigin}`);
|
|
616
619
|
console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
|
|
617
620
|
console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
|
|
618
621
|
console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
|
|
@@ -621,14 +624,20 @@ function printSummary(config, devWarnings) {
|
|
|
621
624
|
);
|
|
622
625
|
console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
|
|
623
626
|
|
|
624
|
-
if (
|
|
625
|
-
console.log("\
|
|
626
|
-
for (const
|
|
627
|
-
console.log(`
|
|
627
|
+
if (securityNotes.length > 0) {
|
|
628
|
+
console.log("\nInfo:");
|
|
629
|
+
for (const note of securityNotes) {
|
|
630
|
+
console.log(` ℹ ${note}`);
|
|
628
631
|
}
|
|
629
632
|
}
|
|
630
633
|
}
|
|
631
634
|
|
|
635
|
+
function shouldRequireAllowlist(bitcallEnv, routingMode) {
|
|
636
|
+
void bitcallEnv;
|
|
637
|
+
void routingMode;
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
632
641
|
function parseProviderFromUri(uri = "") {
|
|
633
642
|
const clean = uri.replace(/^sip:/, "");
|
|
634
643
|
const [hostPort, transportPart] = clean.split(";");
|
|
@@ -649,12 +658,7 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
649
658
|
|
|
650
659
|
try {
|
|
651
660
|
const initProfile = normalizeInitProfile(initOptions, existing);
|
|
652
|
-
|
|
653
|
-
if (initOptions.production && !initOptions.advanced) {
|
|
654
|
-
advanced = true;
|
|
655
|
-
} else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
|
|
656
|
-
advanced = await prompt.askYesNo("Advanced setup", false);
|
|
657
|
-
}
|
|
661
|
+
const advanced = Boolean(initOptions.advanced);
|
|
658
662
|
|
|
659
663
|
const detectedIp = detectPublicIp();
|
|
660
664
|
const domainDefault = initOptions.domain || existing.DOMAIN || "";
|
|
@@ -667,49 +671,55 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
667
671
|
}
|
|
668
672
|
|
|
669
673
|
const resolved = resolveDomainIpv4(domain);
|
|
670
|
-
if (resolved.length > 0 && !resolved.includes(publicIp)) {
|
|
674
|
+
if (initProfile !== "dev" && resolved.length > 0 && !resolved.includes(publicIp)) {
|
|
671
675
|
console.log(`Warning: DNS for ${domain} resolves to ${resolved.join(", ")}, not ${publicIp}.`);
|
|
672
676
|
}
|
|
673
677
|
|
|
674
|
-
const
|
|
678
|
+
const detectedDeployMode =
|
|
675
679
|
(preflight.p80 && preflight.p80.inUse) || (preflight.p443 && preflight.p443.inUse)
|
|
676
680
|
? "reverse-proxy"
|
|
677
681
|
: "standalone";
|
|
678
682
|
|
|
679
|
-
|
|
683
|
+
const existingDeployMode =
|
|
684
|
+
existing.DEPLOY_MODE === "standalone" || existing.DEPLOY_MODE === "reverse-proxy"
|
|
685
|
+
? existing.DEPLOY_MODE
|
|
686
|
+
: "";
|
|
687
|
+
let deployMode = existingDeployMode || detectedDeployMode;
|
|
680
688
|
let tlsMode = "letsencrypt";
|
|
681
689
|
let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
|
|
682
690
|
let customCertPath = "";
|
|
683
691
|
let customKeyPath = "";
|
|
684
|
-
|
|
685
|
-
let
|
|
692
|
+
const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
|
|
693
|
+
let bitcallEnv = quickDefaults.bitcallEnv;
|
|
694
|
+
let routingMode = "universal";
|
|
686
695
|
const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
|
|
687
696
|
let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
|
|
688
|
-
let sipTransport =
|
|
689
|
-
let sipPort =
|
|
697
|
+
let sipTransport = "udp";
|
|
698
|
+
let sipPort = "5060";
|
|
690
699
|
let sipProviderUri = "";
|
|
691
|
-
let allowedDomains =
|
|
692
|
-
let sipTrustedIps =
|
|
700
|
+
let allowedDomains = "";
|
|
701
|
+
let sipTrustedIps = "";
|
|
693
702
|
let turnMode = "coturn";
|
|
694
703
|
let turnSecret = "";
|
|
695
704
|
let turnTtl = existing.TURN_TTL || "86400";
|
|
696
|
-
let turnApiToken =
|
|
705
|
+
let turnApiToken = "";
|
|
697
706
|
let turnExternalUrls = "";
|
|
698
707
|
let turnExternalUsername = "";
|
|
699
708
|
let turnExternalCredential = "";
|
|
700
|
-
let webphoneOrigin =
|
|
701
|
-
let configureUfw =
|
|
709
|
+
let webphoneOrigin = "*";
|
|
710
|
+
let configureUfw = true;
|
|
702
711
|
let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
712
|
+
let rtpMin = existing.RTPENGINE_MIN_PORT || "10000";
|
|
713
|
+
let rtpMax = existing.RTPENGINE_MAX_PORT || "20000";
|
|
714
|
+
let turnUdpPort = existing.TURN_UDP_PORT || "3478";
|
|
715
|
+
let turnsTcpPort = existing.TURNS_TCP_PORT || "5349";
|
|
716
|
+
let turnRelayMinPort = existing.TURN_RELAY_MIN_PORT || "49152";
|
|
717
|
+
let turnRelayMaxPort = existing.TURN_RELAY_MAX_PORT || "49252";
|
|
718
|
+
|
|
719
|
+
if (initProfile === "dev") {
|
|
720
|
+
tlsMode = "dev-self-signed";
|
|
721
|
+
acmeEmail = "";
|
|
722
|
+
turnMode = "coturn";
|
|
713
723
|
routingMode = quickDefaults.routingMode;
|
|
714
724
|
sipProviderUri = quickDefaults.sipProviderUri;
|
|
715
725
|
sipTransport = quickDefaults.sipTransport;
|
|
@@ -717,17 +727,13 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
717
727
|
allowedDomains = quickDefaults.allowedDomains;
|
|
718
728
|
webphoneOrigin = quickDefaults.webphoneOrigin;
|
|
719
729
|
sipTrustedIps = quickDefaults.sipTrustedIps;
|
|
730
|
+
configureUfw = true;
|
|
731
|
+
mediaIpv4Only = true;
|
|
720
732
|
} else {
|
|
721
|
-
deployMode = await prompt.askChoice(
|
|
722
|
-
"Deployment mode",
|
|
723
|
-
["standalone", "reverse-proxy"],
|
|
724
|
-
autoDeployMode === "reverse-proxy" ? 1 : 0
|
|
725
|
-
);
|
|
726
|
-
|
|
727
733
|
tlsMode = await prompt.askChoice(
|
|
728
734
|
"TLS certificate mode",
|
|
729
|
-
["letsencrypt", "custom"
|
|
730
|
-
0
|
|
735
|
+
["letsencrypt", "custom"],
|
|
736
|
+
existing.TLS_MODE === "custom" ? 1 : 0
|
|
731
737
|
);
|
|
732
738
|
|
|
733
739
|
if (tlsMode === "custom") {
|
|
@@ -737,100 +743,79 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
737
743
|
customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
|
|
738
744
|
required: true,
|
|
739
745
|
});
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (tlsMode === "letsencrypt") {
|
|
743
|
-
acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
|
|
744
|
-
} else {
|
|
745
746
|
acmeEmail = "";
|
|
747
|
+
} else {
|
|
748
|
+
acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
|
|
746
749
|
}
|
|
747
750
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
if (routingMode === "single-provider") {
|
|
763
|
-
sipProviderHost = await prompt.askText(
|
|
764
|
-
"SIP provider host",
|
|
765
|
-
existing.SIP_PROVIDER_URI
|
|
766
|
-
? existing.SIP_PROVIDER_URI.replace(/^sip:/, "").split(":")[0]
|
|
767
|
-
: DEFAULT_PROVIDER_HOST,
|
|
768
|
-
{ required: true }
|
|
751
|
+
turnMode = await prompt.askYesNo("Enable built-in TURN (coturn)?", true) ? "coturn" : "none";
|
|
752
|
+
routingMode = "universal";
|
|
753
|
+
sipProviderUri = "";
|
|
754
|
+
sipTransport = "udp";
|
|
755
|
+
sipPort = "5060";
|
|
756
|
+
allowedDomains = "";
|
|
757
|
+
webphoneOrigin = "*";
|
|
758
|
+
sipTrustedIps = "";
|
|
759
|
+
turnApiToken = "";
|
|
760
|
+
|
|
761
|
+
if (advanced) {
|
|
762
|
+
const restrictToSingleProvider = await prompt.askYesNo(
|
|
763
|
+
"Restrict to single SIP provider?",
|
|
764
|
+
existing.ROUTING_MODE === "single-provider"
|
|
769
765
|
);
|
|
770
766
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
sipTransport = sipTransportChoice;
|
|
778
|
-
if (sipTransportChoice === "tcp" || sipTransportChoice === "udp") {
|
|
779
|
-
sipPort = "5060";
|
|
780
|
-
} else if (sipTransportChoice === "tls") {
|
|
781
|
-
sipPort = "5061";
|
|
782
|
-
} else {
|
|
783
|
-
sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
|
|
784
|
-
sipPort = await prompt.askText(
|
|
785
|
-
"Custom SIP port",
|
|
786
|
-
sipTransport === "tls" ? "5061" : "5060",
|
|
767
|
+
if (restrictToSingleProvider) {
|
|
768
|
+
routingMode = "single-provider";
|
|
769
|
+
sipProviderHost = await prompt.askText(
|
|
770
|
+
"SIP provider host",
|
|
771
|
+
providerFromEnv.host || DEFAULT_PROVIDER_HOST,
|
|
787
772
|
{ required: true }
|
|
788
773
|
);
|
|
774
|
+
sipTransport = await prompt.askChoice(
|
|
775
|
+
"SIP provider transport",
|
|
776
|
+
["udp", "tcp", "tls"],
|
|
777
|
+
providerFromEnv.transport === "tcp" ? 1 : providerFromEnv.transport === "tls" ? 2 : 0
|
|
778
|
+
);
|
|
779
|
+
const defaultProviderPort =
|
|
780
|
+
providerFromEnv.port || (sipTransport === "tls" ? "5061" : "5060");
|
|
781
|
+
sipPort = await prompt.askText("SIP provider port", defaultProviderPort, { required: true });
|
|
782
|
+
sipProviderUri = `sip:${sipProviderHost}:${sipPort};transport=${sipTransport}`;
|
|
789
783
|
}
|
|
790
784
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
sipTrustedIps = await prompt.askText(
|
|
795
|
-
"Trusted SIP source IPs (optional, comma-separated)",
|
|
796
|
-
sipTrustedIps
|
|
797
|
-
);
|
|
798
|
-
|
|
799
|
-
const existingTurn = existing.TURN_MODE || "coturn";
|
|
800
|
-
const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
|
|
801
|
-
turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
|
|
802
|
-
if (turnMode === "coturn") {
|
|
803
|
-
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
804
|
-
turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
|
|
805
|
-
turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
|
|
806
|
-
} else if (turnMode === "external") {
|
|
807
|
-
turnSecret = "";
|
|
808
|
-
turnApiToken = "";
|
|
809
|
-
turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
|
|
810
|
-
required: true,
|
|
811
|
-
});
|
|
812
|
-
turnExternalUsername = await prompt.askText(
|
|
813
|
-
"External TURN username",
|
|
814
|
-
existing.TURN_EXTERNAL_USERNAME || ""
|
|
785
|
+
allowedDomains = await prompt.askText(
|
|
786
|
+
"SIP domain allowlist (optional, comma-separated)",
|
|
787
|
+
existing.ALLOWED_SIP_DOMAINS || ""
|
|
815
788
|
);
|
|
816
|
-
|
|
817
|
-
"
|
|
818
|
-
existing.
|
|
789
|
+
webphoneOrigin = await prompt.askText(
|
|
790
|
+
"Webphone origin restriction (optional, * for any)",
|
|
791
|
+
existing.WEBPHONE_ORIGIN || "*"
|
|
792
|
+
);
|
|
793
|
+
webphoneOrigin = webphoneOrigin.trim() ? webphoneOrigin : "*";
|
|
794
|
+
sipTrustedIps = await prompt.askText(
|
|
795
|
+
"SIP trusted source IPs (optional, comma-separated)",
|
|
796
|
+
existing.SIP_TRUSTED_IPS || ""
|
|
819
797
|
);
|
|
820
|
-
} else {
|
|
821
|
-
turnSecret = "";
|
|
822
|
-
turnApiToken = "";
|
|
823
|
-
}
|
|
824
798
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
);
|
|
799
|
+
if (turnMode === "coturn") {
|
|
800
|
+
turnApiToken = await prompt.askText("TURN API token (optional)", existing.TURN_API_TOKEN || "");
|
|
801
|
+
}
|
|
829
802
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
803
|
+
const customizePorts = await prompt.askYesNo("Customize RTP/TURN ports?", false);
|
|
804
|
+
if (customizePorts) {
|
|
805
|
+
rtpMin = await prompt.askText("RTP minimum port", rtpMin, { required: true });
|
|
806
|
+
rtpMax = await prompt.askText("RTP maximum port", rtpMax, { required: true });
|
|
807
|
+
if (turnMode === "coturn") {
|
|
808
|
+
turnUdpPort = await prompt.askText("TURN UDP/TCP port", turnUdpPort, { required: true });
|
|
809
|
+
turnsTcpPort = await prompt.askText("TURNS TCP port", turnsTcpPort, { required: true });
|
|
810
|
+
turnRelayMinPort = await prompt.askText("TURN relay minimum port", turnRelayMinPort, {
|
|
811
|
+
required: true,
|
|
812
|
+
});
|
|
813
|
+
turnRelayMaxPort = await prompt.askText("TURN relay maximum port", turnRelayMaxPort, {
|
|
814
|
+
required: true,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
834
819
|
}
|
|
835
820
|
|
|
836
821
|
if (turnMode === "coturn") {
|
|
@@ -868,12 +853,12 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
868
853
|
turnExternalCredential,
|
|
869
854
|
webphoneOrigin,
|
|
870
855
|
mediaIpv4Only: mediaIpv4Only ? "1" : "0",
|
|
871
|
-
rtpMin
|
|
872
|
-
rtpMax
|
|
873
|
-
turnUdpPort
|
|
874
|
-
turnsTcpPort
|
|
875
|
-
turnRelayMinPort
|
|
876
|
-
turnRelayMaxPort
|
|
856
|
+
rtpMin,
|
|
857
|
+
rtpMax,
|
|
858
|
+
turnUdpPort,
|
|
859
|
+
turnsTcpPort,
|
|
860
|
+
turnRelayMinPort,
|
|
861
|
+
turnRelayMaxPort,
|
|
877
862
|
acmeListenPort: deployMode === "reverse-proxy" ? "8080" : "80",
|
|
878
863
|
wssListenPort: deployMode === "reverse-proxy" ? "8443" : "443",
|
|
879
864
|
internalWssPort: "8443",
|
|
@@ -885,10 +870,10 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
885
870
|
validateProductionConfig(config);
|
|
886
871
|
}
|
|
887
872
|
|
|
888
|
-
const
|
|
889
|
-
printSummary(config,
|
|
873
|
+
const securityNotes = buildSecurityNotes(config);
|
|
874
|
+
printSummary(config, securityNotes);
|
|
890
875
|
|
|
891
|
-
if (
|
|
876
|
+
if (initProfile !== "dev") {
|
|
892
877
|
const proceed = await prompt.askYesNo("Proceed with provisioning", true);
|
|
893
878
|
if (!proceed) {
|
|
894
879
|
throw new Error("Initialization canceled.");
|
|
@@ -1089,6 +1074,9 @@ async function initCommand(initOptions = {}) {
|
|
|
1089
1074
|
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1090
1075
|
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1091
1076
|
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
1077
|
+
console.log("WARNING: Self-signed certificates do not work with browsers.");
|
|
1078
|
+
console.log("WebSocket connections will be rejected. Use --dev for local testing only.");
|
|
1079
|
+
console.log("For browser access, re-run with Let's Encrypt: sudo bitcall-gateway init");
|
|
1092
1080
|
} else {
|
|
1093
1081
|
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1094
1082
|
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
@@ -1120,6 +1108,92 @@ async function initCommand(initOptions = {}) {
|
|
|
1120
1108
|
}
|
|
1121
1109
|
}
|
|
1122
1110
|
|
|
1111
|
+
async function reconfigureCommand(options) {
|
|
1112
|
+
ensureInitialized();
|
|
1113
|
+
printBanner();
|
|
1114
|
+
const ctx = createInstallContext(options);
|
|
1115
|
+
|
|
1116
|
+
let preflight;
|
|
1117
|
+
await ctx.step("Checks", async () => {
|
|
1118
|
+
preflight = await runPreflight(ctx);
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
const existingEnv = fs.existsSync(ENV_PATH) ? loadEnvFile(ENV_PATH) : {};
|
|
1122
|
+
|
|
1123
|
+
let config;
|
|
1124
|
+
await ctx.step("Config", async () => {
|
|
1125
|
+
config = await runWizard(existingEnv, preflight, options);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
ensureInstallLayout();
|
|
1129
|
+
|
|
1130
|
+
let tlsCertPath;
|
|
1131
|
+
let tlsKeyPath;
|
|
1132
|
+
|
|
1133
|
+
await ctx.step("Firewall", async () => {
|
|
1134
|
+
if (config.configureUfw) {
|
|
1135
|
+
const ufwReady = await ensureUfwAvailable(ctx);
|
|
1136
|
+
if (ufwReady) {
|
|
1137
|
+
configureFirewall(config, ctx.exec);
|
|
1138
|
+
} else {
|
|
1139
|
+
config.configureUfw = false;
|
|
1140
|
+
printRequiredPorts(config);
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
printRequiredPorts(config);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (config.mediaIpv4Only === "1") {
|
|
1147
|
+
try {
|
|
1148
|
+
const applied = applyMediaIpv4OnlyRules(mediaFirewallOptionsFromConfig(config));
|
|
1149
|
+
console.log(`Applied IPv6 media block rules (${applied.backend}).`);
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
console.error(`IPv6 media block setup failed: ${error.message}`);
|
|
1152
|
+
config.mediaIpv4Only = "0";
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
await ctx.step("TLS", async () => {
|
|
1158
|
+
if (config.tlsMode === "custom") {
|
|
1159
|
+
const custom = provisionCustomCert(config);
|
|
1160
|
+
tlsCertPath = custom.certPath;
|
|
1161
|
+
tlsKeyPath = custom.keyPath;
|
|
1162
|
+
} else if (config.tlsMode === "dev-self-signed") {
|
|
1163
|
+
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1164
|
+
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1165
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 30, ctx.exec);
|
|
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;
|
|
1172
|
+
} else {
|
|
1173
|
+
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1174
|
+
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
1175
|
+
generateSelfSigned(tlsCertPath, tlsKeyPath, config.domain, 1, ctx.exec);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const envContent = renderEnvContent(config, tlsCertPath, tlsKeyPath);
|
|
1180
|
+
writeFileWithMode(ENV_PATH, envContent, 0o600);
|
|
1181
|
+
writeComposeTemplate();
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
await ctx.step("Start", async () => {
|
|
1185
|
+
startGatewayStack(ctx.exec);
|
|
1186
|
+
if (config.tlsMode === "letsencrypt" && (!tlsCertPath || !tlsCertPath.includes("letsencrypt"))) {
|
|
1187
|
+
runLetsEncrypt(config, ctx.exec);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
await ctx.step("Done", async () => {});
|
|
1192
|
+
|
|
1193
|
+
console.log("\nGateway reconfigured.");
|
|
1194
|
+
console.log(`WSS URL: wss://${config.domain}`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1123
1197
|
function runSystemctl(args, fallbackComposeArgs) {
|
|
1124
1198
|
const result = run("systemctl", args, { check: false, stdio: "inherit" });
|
|
1125
1199
|
if (result.status !== 0 && fallbackComposeArgs) {
|
|
@@ -1143,6 +1217,30 @@ function restartCommand() {
|
|
|
1143
1217
|
runSystemctl(["reload", SERVICE_NAME], ["restart"]);
|
|
1144
1218
|
}
|
|
1145
1219
|
|
|
1220
|
+
function pauseCommand() {
|
|
1221
|
+
ensureInitialized();
|
|
1222
|
+
runCompose(["pause"], { stdio: "inherit" });
|
|
1223
|
+
console.log("Gateway paused.");
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function resumeCommand() {
|
|
1227
|
+
ensureInitialized();
|
|
1228
|
+
runCompose(["unpause"], { stdio: "inherit" });
|
|
1229
|
+
console.log("Gateway resumed.");
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
function enableCommand() {
|
|
1233
|
+
ensureInitialized();
|
|
1234
|
+
run("systemctl", ["enable", SERVICE_NAME], { stdio: "inherit" });
|
|
1235
|
+
console.log("Gateway will start on boot.");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function disableCommand() {
|
|
1239
|
+
ensureInitialized();
|
|
1240
|
+
run("systemctl", ["disable", SERVICE_NAME], { stdio: "inherit" });
|
|
1241
|
+
console.log("Gateway will NOT start on boot.");
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1146
1244
|
function logsCommand(service, options) {
|
|
1147
1245
|
ensureInitialized();
|
|
1148
1246
|
const args = ["logs", "--tail", String(options.tail || 200)];
|
|
@@ -1227,7 +1325,7 @@ function statusCommand() {
|
|
|
1227
1325
|
|
|
1228
1326
|
console.log("\nConfig summary:");
|
|
1229
1327
|
console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
|
|
1230
|
-
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "
|
|
1328
|
+
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "production"}`);
|
|
1231
1329
|
console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
|
|
1232
1330
|
console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
|
|
1233
1331
|
console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
|
|
@@ -1307,8 +1405,24 @@ async function certInstallCommand(options) {
|
|
|
1307
1405
|
|
|
1308
1406
|
function updateCommand() {
|
|
1309
1407
|
ensureInitialized();
|
|
1408
|
+
const envMap = loadEnvFile(ENV_PATH);
|
|
1409
|
+
const currentImage = envMap.BITCALL_GATEWAY_IMAGE || "";
|
|
1410
|
+
const targetImage = DEFAULT_GATEWAY_IMAGE;
|
|
1411
|
+
|
|
1412
|
+
if (currentImage === targetImage) {
|
|
1413
|
+
console.log(`Image is current: ${targetImage}`);
|
|
1414
|
+
console.log("Checking for newer image layers...");
|
|
1415
|
+
} else {
|
|
1416
|
+
console.log(`Updating image: ${currentImage || "(none)"} → ${targetImage}`);
|
|
1417
|
+
updateGatewayEnv({ BITCALL_GATEWAY_IMAGE: targetImage });
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1310
1420
|
runCompose(["pull"], { stdio: "inherit" });
|
|
1311
1421
|
runSystemctl(["reload", SERVICE_NAME], ["up", "-d", "--remove-orphans"]);
|
|
1422
|
+
|
|
1423
|
+
console.log(`\nGateway updated to ${PACKAGE_VERSION}.`);
|
|
1424
|
+
console.log("To update the CLI itself:");
|
|
1425
|
+
console.log(" sudo npm i -g @bitcall/webrtc-sip-gateway@latest");
|
|
1312
1426
|
}
|
|
1313
1427
|
|
|
1314
1428
|
function mediaStatusCommand() {
|
|
@@ -1391,11 +1505,40 @@ async function uninstallCommand(options) {
|
|
|
1391
1505
|
fs.rmSync(RENEW_HOOK_PATH, { force: true });
|
|
1392
1506
|
}
|
|
1393
1507
|
|
|
1508
|
+
// Remove Bitcall-tagged UFW rules.
|
|
1509
|
+
if (commandExists("ufw")) {
|
|
1510
|
+
const ufwOutput = output("ufw", ["status", "numbered"]);
|
|
1511
|
+
const lines = (ufwOutput || "").split("\n");
|
|
1512
|
+
const ruleNumbers = [];
|
|
1513
|
+
for (const line of lines) {
|
|
1514
|
+
const match = line.match(/^\[\s*(\d+)\]/);
|
|
1515
|
+
if (match && line.includes("Bitcall")) {
|
|
1516
|
+
ruleNumbers.push(match[1]);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
// Delete in reverse order so indices do not shift.
|
|
1521
|
+
for (const num of ruleNumbers.reverse()) {
|
|
1522
|
+
run("ufw", ["--force", "delete", num], { check: false, stdio: "inherit" });
|
|
1523
|
+
}
|
|
1524
|
+
if (ruleNumbers.length > 0) {
|
|
1525
|
+
run("ufw", ["reload"], { check: false, stdio: "inherit" });
|
|
1526
|
+
console.log(`Removed ${ruleNumbers.length} UFW rule(s).`);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1394
1530
|
if (fs.existsSync(GATEWAY_DIR)) {
|
|
1395
1531
|
fs.rmSync(GATEWAY_DIR, { recursive: true, force: true });
|
|
1396
1532
|
}
|
|
1397
1533
|
|
|
1398
1534
|
console.log("Gateway uninstalled.");
|
|
1535
|
+
const domain = uninstallEnv.DOMAIN || "";
|
|
1536
|
+
console.log("\nTo remove the CLI:");
|
|
1537
|
+
console.log(" sudo npm uninstall -g @bitcall/webrtc-sip-gateway");
|
|
1538
|
+
if (domain && uninstallEnv.TLS_MODE === "letsencrypt") {
|
|
1539
|
+
console.log("\nTo remove Let's Encrypt certificate:");
|
|
1540
|
+
console.log(` sudo certbot delete --cert-name ${domain}`);
|
|
1541
|
+
}
|
|
1399
1542
|
}
|
|
1400
1543
|
|
|
1401
1544
|
function configCommand() {
|
|
@@ -1421,7 +1564,7 @@ function buildProgram() {
|
|
|
1421
1564
|
.description("Run setup wizard and provision gateway")
|
|
1422
1565
|
.option("--advanced", "Show advanced configuration prompts")
|
|
1423
1566
|
.option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
|
|
1424
|
-
.option("--production", "Production setup
|
|
1567
|
+
.option("--production", "Production setup profile")
|
|
1425
1568
|
.option("--domain <domain>", "Gateway domain")
|
|
1426
1569
|
.option("--email <email>", "Let's Encrypt email")
|
|
1427
1570
|
.option("--verbose", "Stream full installer command output")
|
|
@@ -1452,6 +1595,18 @@ function buildProgram() {
|
|
|
1452
1595
|
|
|
1453
1596
|
program.command("update").description("Pull latest image and restart service").action(updateCommand);
|
|
1454
1597
|
program.command("config").description("Print active configuration (secrets hidden)").action(configCommand);
|
|
1598
|
+
program.command("pause").description("Pause gateway containers").action(pauseCommand);
|
|
1599
|
+
program.command("resume").description("Resume paused gateway containers").action(resumeCommand);
|
|
1600
|
+
program.command("enable").description("Enable auto-start on boot").action(enableCommand);
|
|
1601
|
+
program.command("disable").description("Disable auto-start on boot").action(disableCommand);
|
|
1602
|
+
program
|
|
1603
|
+
.command("reconfigure")
|
|
1604
|
+
.description("Re-run wizard with current values as defaults")
|
|
1605
|
+
.option("--advanced", "Show advanced configuration prompts")
|
|
1606
|
+
.option("--dev", "Dev mode")
|
|
1607
|
+
.option("--production", "Production mode")
|
|
1608
|
+
.option("--verbose", "Verbose output")
|
|
1609
|
+
.action(reconfigureCommand);
|
|
1455
1610
|
|
|
1456
1611
|
const media = program.command("media").description("Media firewall operations");
|
|
1457
1612
|
media.command("status").description("Show media IPv4-only firewall state").action(mediaStatusCommand);
|
|
@@ -1477,8 +1632,9 @@ module.exports = {
|
|
|
1477
1632
|
main,
|
|
1478
1633
|
normalizeInitProfile,
|
|
1479
1634
|
validateProductionConfig,
|
|
1480
|
-
|
|
1635
|
+
buildSecurityNotes,
|
|
1481
1636
|
buildQuickFlowDefaults,
|
|
1637
|
+
shouldRequireAllowlist,
|
|
1482
1638
|
isOriginWildcard,
|
|
1483
1639
|
isSingleProviderConfigured,
|
|
1484
1640
|
printRequiredPorts,
|