@bitcall/webrtc-sip-gateway 0.2.8 → 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 +12 -8
- package/lib/constants.js +2 -1
- package/package.json +1 -1
- package/src/index.js +315 -171
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,14 +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
|
-
Default `init`
|
|
26
|
-
- `BITCALL_ENV=
|
|
25
|
+
Default `init` runs in production profile with universal routing:
|
|
26
|
+
- `BITCALL_ENV=production`
|
|
27
27
|
- `ROUTING_MODE=universal`
|
|
28
|
-
- provider allowlist/origin/source IPs are permissive by default
|
|
28
|
+
- provider allowlist/origin/source IPs are permissive by default
|
|
29
29
|
|
|
30
|
-
Use `sudo bitcall-gateway init --
|
|
31
|
-
|
|
32
|
-
`ALLOWED_SIP_DOMAINS`.
|
|
30
|
+
Use `sudo bitcall-gateway init --advanced` for full security/provider controls.
|
|
31
|
+
Use `sudo bitcall-gateway init --dev` for local testing only.
|
|
33
32
|
Use `--verbose` to stream apt/docker output during install. Default mode keeps
|
|
34
33
|
console output concise and writes command details to
|
|
35
34
|
`/var/log/bitcall-gateway-install.log`.
|
|
@@ -44,6 +43,11 @@ console output concise and writes command details to
|
|
|
44
43
|
- `sudo bitcall-gateway up`
|
|
45
44
|
- `sudo bitcall-gateway down`
|
|
46
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`
|
|
47
51
|
- `sudo bitcall-gateway status`
|
|
48
52
|
- `sudo bitcall-gateway logs [-f] [service]`
|
|
49
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
|
|
|
@@ -545,7 +547,6 @@ function toOriginPattern(origin) {
|
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
function normalizeInitProfile(initOptions = {}, existing = {}) {
|
|
548
|
-
void existing;
|
|
549
550
|
if (initOptions.dev && initOptions.production) {
|
|
550
551
|
throw new Error("Use only one mode: --dev or --production.");
|
|
551
552
|
}
|
|
@@ -555,7 +556,7 @@ function normalizeInitProfile(initOptions = {}, existing = {}) {
|
|
|
555
556
|
if (initOptions.dev) {
|
|
556
557
|
return "dev";
|
|
557
558
|
}
|
|
558
|
-
return "
|
|
559
|
+
return existing.BITCALL_ENV || "production";
|
|
559
560
|
}
|
|
560
561
|
|
|
561
562
|
async function runPreflight(ctx) {
|
|
@@ -601,27 +602,20 @@ async function runPreflight(ctx) {
|
|
|
601
602
|
};
|
|
602
603
|
}
|
|
603
604
|
|
|
604
|
-
function printSummary(config,
|
|
605
|
+
function printSummary(config, securityNotes) {
|
|
605
606
|
const allowedCount = countAllowedDomains(config.allowedDomains);
|
|
606
|
-
const showDevWarnings = config.bitcallEnv === "dev";
|
|
607
607
|
const providerAllowlistSummary =
|
|
608
608
|
allowedCount > 0
|
|
609
609
|
? config.allowedDomains
|
|
610
|
-
: config
|
|
611
|
-
?
|
|
612
|
-
? "(single-provider mode)"
|
|
613
|
-
: "(missing)"
|
|
610
|
+
: isSingleProviderConfigured(config)
|
|
611
|
+
? "(single-provider mode)"
|
|
614
612
|
: "(any)";
|
|
615
613
|
console.log("\nSummary:");
|
|
616
614
|
console.log(` Domain: ${config.domain}`);
|
|
617
615
|
console.log(` Environment: ${config.bitcallEnv}`);
|
|
618
616
|
console.log(` Routing: ${config.routingMode}`);
|
|
619
|
-
console.log(
|
|
620
|
-
|
|
621
|
-
);
|
|
622
|
-
console.log(
|
|
623
|
-
` Webphone origin: ${config.webphoneOrigin === "*" ? `(any)${showDevWarnings ? " [DEV WARNING]" : ""}` : config.webphoneOrigin}`
|
|
624
|
-
);
|
|
617
|
+
console.log(` Provider allowlist: ${providerAllowlistSummary}`);
|
|
618
|
+
console.log(` Webphone origin: ${config.webphoneOrigin === "*" ? "(any)" : config.webphoneOrigin}`);
|
|
625
619
|
console.log(` SIP source IPs: ${(config.sipTrustedIps || "").trim() ? config.sipTrustedIps : "(any)"}`);
|
|
626
620
|
console.log(` TLS: ${config.tlsMode === "letsencrypt" ? "Let's Encrypt" : config.tlsMode}`);
|
|
627
621
|
console.log(` TURN: ${config.turnMode === "coturn" ? "enabled (coturn)" : config.turnMode}`);
|
|
@@ -630,16 +624,18 @@ function printSummary(config, devWarnings) {
|
|
|
630
624
|
);
|
|
631
625
|
console.log(` Firewall: ${config.configureUfw ? "UFW enabled" : "manual setup"}`);
|
|
632
626
|
|
|
633
|
-
if (
|
|
634
|
-
console.log("\
|
|
635
|
-
for (const
|
|
636
|
-
console.log(`
|
|
627
|
+
if (securityNotes.length > 0) {
|
|
628
|
+
console.log("\nInfo:");
|
|
629
|
+
for (const note of securityNotes) {
|
|
630
|
+
console.log(` ℹ ${note}`);
|
|
637
631
|
}
|
|
638
632
|
}
|
|
639
633
|
}
|
|
640
634
|
|
|
641
635
|
function shouldRequireAllowlist(bitcallEnv, routingMode) {
|
|
642
|
-
|
|
636
|
+
void bitcallEnv;
|
|
637
|
+
void routingMode;
|
|
638
|
+
return false;
|
|
643
639
|
}
|
|
644
640
|
|
|
645
641
|
function parseProviderFromUri(uri = "") {
|
|
@@ -662,12 +658,7 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
662
658
|
|
|
663
659
|
try {
|
|
664
660
|
const initProfile = normalizeInitProfile(initOptions, existing);
|
|
665
|
-
|
|
666
|
-
if (initOptions.production && !initOptions.advanced) {
|
|
667
|
-
advanced = true;
|
|
668
|
-
} else if (!initOptions.dev && !initOptions.production && !initOptions.advanced) {
|
|
669
|
-
advanced = await prompt.askYesNo("Advanced setup", false);
|
|
670
|
-
}
|
|
661
|
+
const advanced = Boolean(initOptions.advanced);
|
|
671
662
|
|
|
672
663
|
const detectedIp = detectPublicIp();
|
|
673
664
|
const domainDefault = initOptions.domain || existing.DOMAIN || "";
|
|
@@ -680,45 +671,55 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
680
671
|
}
|
|
681
672
|
|
|
682
673
|
const resolved = resolveDomainIpv4(domain);
|
|
683
|
-
if (resolved.length > 0 && !resolved.includes(publicIp)) {
|
|
674
|
+
if (initProfile !== "dev" && resolved.length > 0 && !resolved.includes(publicIp)) {
|
|
684
675
|
console.log(`Warning: DNS for ${domain} resolves to ${resolved.join(", ")}, not ${publicIp}.`);
|
|
685
676
|
}
|
|
686
677
|
|
|
687
|
-
const
|
|
678
|
+
const detectedDeployMode =
|
|
688
679
|
(preflight.p80 && preflight.p80.inUse) || (preflight.p443 && preflight.p443.inUse)
|
|
689
680
|
? "reverse-proxy"
|
|
690
681
|
: "standalone";
|
|
691
682
|
|
|
692
|
-
|
|
683
|
+
const existingDeployMode =
|
|
684
|
+
existing.DEPLOY_MODE === "standalone" || existing.DEPLOY_MODE === "reverse-proxy"
|
|
685
|
+
? existing.DEPLOY_MODE
|
|
686
|
+
: "";
|
|
687
|
+
let deployMode = existingDeployMode || detectedDeployMode;
|
|
693
688
|
let tlsMode = "letsencrypt";
|
|
694
689
|
let acmeEmail = initOptions.email || existing.ACME_EMAIL || "";
|
|
695
690
|
let customCertPath = "";
|
|
696
691
|
let customKeyPath = "";
|
|
697
|
-
|
|
698
|
-
let
|
|
692
|
+
const quickDefaults = buildQuickFlowDefaults(initProfile, existing);
|
|
693
|
+
let bitcallEnv = quickDefaults.bitcallEnv;
|
|
694
|
+
let routingMode = "universal";
|
|
699
695
|
const providerFromEnv = parseProviderFromUri(existing.SIP_PROVIDER_URI || "");
|
|
700
696
|
let sipProviderHost = providerFromEnv.host || DEFAULT_PROVIDER_HOST;
|
|
701
|
-
let sipTransport =
|
|
702
|
-
let sipPort =
|
|
697
|
+
let sipTransport = "udp";
|
|
698
|
+
let sipPort = "5060";
|
|
703
699
|
let sipProviderUri = "";
|
|
704
|
-
let allowedDomains =
|
|
705
|
-
let sipTrustedIps =
|
|
700
|
+
let allowedDomains = "";
|
|
701
|
+
let sipTrustedIps = "";
|
|
706
702
|
let turnMode = "coturn";
|
|
707
703
|
let turnSecret = "";
|
|
708
704
|
let turnTtl = existing.TURN_TTL || "86400";
|
|
709
|
-
let turnApiToken =
|
|
705
|
+
let turnApiToken = "";
|
|
710
706
|
let turnExternalUrls = "";
|
|
711
707
|
let turnExternalUsername = "";
|
|
712
708
|
let turnExternalCredential = "";
|
|
713
|
-
let webphoneOrigin =
|
|
714
|
-
let configureUfw =
|
|
709
|
+
let webphoneOrigin = "*";
|
|
710
|
+
let configureUfw = true;
|
|
715
711
|
let mediaIpv4Only = existing.MEDIA_IPV4_ONLY ? existing.MEDIA_IPV4_ONLY === "1" : true;
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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";
|
|
722
723
|
routingMode = quickDefaults.routingMode;
|
|
723
724
|
sipProviderUri = quickDefaults.sipProviderUri;
|
|
724
725
|
sipTransport = quickDefaults.sipTransport;
|
|
@@ -726,17 +727,13 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
726
727
|
allowedDomains = quickDefaults.allowedDomains;
|
|
727
728
|
webphoneOrigin = quickDefaults.webphoneOrigin;
|
|
728
729
|
sipTrustedIps = quickDefaults.sipTrustedIps;
|
|
730
|
+
configureUfw = true;
|
|
731
|
+
mediaIpv4Only = true;
|
|
729
732
|
} else {
|
|
730
|
-
deployMode = await prompt.askChoice(
|
|
731
|
-
"Deployment mode",
|
|
732
|
-
["standalone", "reverse-proxy"],
|
|
733
|
-
autoDeployMode === "reverse-proxy" ? 1 : 0
|
|
734
|
-
);
|
|
735
|
-
|
|
736
733
|
tlsMode = await prompt.askChoice(
|
|
737
734
|
"TLS certificate mode",
|
|
738
|
-
["letsencrypt", "custom"
|
|
739
|
-
0
|
|
735
|
+
["letsencrypt", "custom"],
|
|
736
|
+
existing.TLS_MODE === "custom" ? 1 : 0
|
|
740
737
|
);
|
|
741
738
|
|
|
742
739
|
if (tlsMode === "custom") {
|
|
@@ -746,104 +743,79 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
746
743
|
customKeyPath = await prompt.askText("Path to TLS private key (PEM)", existing.TLS_KEY || "", {
|
|
747
744
|
required: true,
|
|
748
745
|
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (tlsMode === "letsencrypt") {
|
|
752
|
-
acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
|
|
753
|
-
} else {
|
|
754
746
|
acmeEmail = "";
|
|
747
|
+
} else {
|
|
748
|
+
acmeEmail = await prompt.askText("Let's Encrypt email", acmeEmail, { required: true });
|
|
755
749
|
}
|
|
756
750
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
: DEFAULT_PROVIDER_HOST,
|
|
772
|
-
{ required: true }
|
|
773
|
-
);
|
|
774
|
-
|
|
775
|
-
const sipTransportChoice = await prompt.askChoice(
|
|
776
|
-
"SIP provider transport",
|
|
777
|
-
["udp", "tcp", "tls", "custom"],
|
|
778
|
-
0
|
|
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"
|
|
779
765
|
);
|
|
780
766
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
} else {
|
|
787
|
-
sipTransport = await prompt.askChoice("Custom transport", ["udp", "tcp", "tls"], 0);
|
|
788
|
-
sipPort = await prompt.askText(
|
|
789
|
-
"Custom SIP port",
|
|
790
|
-
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,
|
|
791
772
|
{ required: true }
|
|
792
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}`;
|
|
793
783
|
}
|
|
794
784
|
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const requireAllowlist = shouldRequireAllowlist(bitcallEnv, routingMode);
|
|
799
|
-
allowedDomains = await prompt.askText(
|
|
800
|
-
requireAllowlist
|
|
801
|
-
? "Allowed SIP domains (comma-separated; required in production universal mode)"
|
|
802
|
-
: "Allowed SIP domains (comma-separated)",
|
|
803
|
-
allowedDomains,
|
|
804
|
-
{ required: requireAllowlist }
|
|
805
|
-
);
|
|
806
|
-
|
|
807
|
-
sipTrustedIps = await prompt.askText(
|
|
808
|
-
"Trusted SIP source IPs (optional, comma-separated)",
|
|
809
|
-
sipTrustedIps
|
|
810
|
-
);
|
|
811
|
-
|
|
812
|
-
const existingTurn = existing.TURN_MODE || "coturn";
|
|
813
|
-
const turnDefaultIndex = existingTurn === "external" ? 2 : existingTurn === "coturn" ? 1 : 0;
|
|
814
|
-
turnMode = await prompt.askChoice("TURN mode", ["none", "coturn", "external"], turnDefaultIndex);
|
|
815
|
-
if (turnMode === "coturn") {
|
|
816
|
-
turnSecret = crypto.randomBytes(32).toString("hex");
|
|
817
|
-
turnTtl = await prompt.askText("TURN credential TTL seconds", turnTtl, { required: true });
|
|
818
|
-
turnApiToken = await prompt.askText("TURN API token (optional)", turnApiToken);
|
|
819
|
-
} else if (turnMode === "external") {
|
|
820
|
-
turnSecret = "";
|
|
821
|
-
turnApiToken = "";
|
|
822
|
-
turnExternalUrls = await prompt.askText("External TURN urls", existing.TURN_EXTERNAL_URLS || "", {
|
|
823
|
-
required: true,
|
|
824
|
-
});
|
|
825
|
-
turnExternalUsername = await prompt.askText(
|
|
826
|
-
"External TURN username",
|
|
827
|
-
existing.TURN_EXTERNAL_USERNAME || ""
|
|
785
|
+
allowedDomains = await prompt.askText(
|
|
786
|
+
"SIP domain allowlist (optional, comma-separated)",
|
|
787
|
+
existing.ALLOWED_SIP_DOMAINS || ""
|
|
828
788
|
);
|
|
829
|
-
|
|
830
|
-
"
|
|
831
|
-
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 || ""
|
|
832
797
|
);
|
|
833
|
-
} else {
|
|
834
|
-
turnSecret = "";
|
|
835
|
-
turnApiToken = "";
|
|
836
|
-
}
|
|
837
798
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
);
|
|
799
|
+
if (turnMode === "coturn") {
|
|
800
|
+
turnApiToken = await prompt.askText("TURN API token (optional)", existing.TURN_API_TOKEN || "");
|
|
801
|
+
}
|
|
842
802
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
+
}
|
|
847
819
|
}
|
|
848
820
|
|
|
849
821
|
if (turnMode === "coturn") {
|
|
@@ -881,12 +853,12 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
881
853
|
turnExternalCredential,
|
|
882
854
|
webphoneOrigin,
|
|
883
855
|
mediaIpv4Only: mediaIpv4Only ? "1" : "0",
|
|
884
|
-
rtpMin
|
|
885
|
-
rtpMax
|
|
886
|
-
turnUdpPort
|
|
887
|
-
turnsTcpPort
|
|
888
|
-
turnRelayMinPort
|
|
889
|
-
turnRelayMaxPort
|
|
856
|
+
rtpMin,
|
|
857
|
+
rtpMax,
|
|
858
|
+
turnUdpPort,
|
|
859
|
+
turnsTcpPort,
|
|
860
|
+
turnRelayMinPort,
|
|
861
|
+
turnRelayMaxPort,
|
|
890
862
|
acmeListenPort: deployMode === "reverse-proxy" ? "8080" : "80",
|
|
891
863
|
wssListenPort: deployMode === "reverse-proxy" ? "8443" : "443",
|
|
892
864
|
internalWssPort: "8443",
|
|
@@ -898,12 +870,14 @@ async function runWizard(existing = {}, preflight = {}, initOptions = {}) {
|
|
|
898
870
|
validateProductionConfig(config);
|
|
899
871
|
}
|
|
900
872
|
|
|
901
|
-
const
|
|
902
|
-
printSummary(config,
|
|
873
|
+
const securityNotes = buildSecurityNotes(config);
|
|
874
|
+
printSummary(config, securityNotes);
|
|
903
875
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
876
|
+
if (initProfile !== "dev") {
|
|
877
|
+
const proceed = await prompt.askYesNo("Proceed with provisioning", true);
|
|
878
|
+
if (!proceed) {
|
|
879
|
+
throw new Error("Initialization canceled.");
|
|
880
|
+
}
|
|
907
881
|
}
|
|
908
882
|
|
|
909
883
|
return config;
|
|
@@ -1100,6 +1074,9 @@ async function initCommand(initOptions = {}) {
|
|
|
1100
1074
|
tlsCertPath = path.join(SSL_DIR, "dev-fullchain.pem");
|
|
1101
1075
|
tlsKeyPath = path.join(SSL_DIR, "dev-privkey.pem");
|
|
1102
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");
|
|
1103
1080
|
} else {
|
|
1104
1081
|
tlsCertPath = path.join(SSL_DIR, "bootstrap-fullchain.pem");
|
|
1105
1082
|
tlsKeyPath = path.join(SSL_DIR, "bootstrap-privkey.pem");
|
|
@@ -1131,6 +1108,92 @@ async function initCommand(initOptions = {}) {
|
|
|
1131
1108
|
}
|
|
1132
1109
|
}
|
|
1133
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
|
+
|
|
1134
1197
|
function runSystemctl(args, fallbackComposeArgs) {
|
|
1135
1198
|
const result = run("systemctl", args, { check: false, stdio: "inherit" });
|
|
1136
1199
|
if (result.status !== 0 && fallbackComposeArgs) {
|
|
@@ -1154,6 +1217,30 @@ function restartCommand() {
|
|
|
1154
1217
|
runSystemctl(["reload", SERVICE_NAME], ["restart"]);
|
|
1155
1218
|
}
|
|
1156
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
|
+
|
|
1157
1244
|
function logsCommand(service, options) {
|
|
1158
1245
|
ensureInitialized();
|
|
1159
1246
|
const args = ["logs", "--tail", String(options.tail || 200)];
|
|
@@ -1238,7 +1325,7 @@ function statusCommand() {
|
|
|
1238
1325
|
|
|
1239
1326
|
console.log("\nConfig summary:");
|
|
1240
1327
|
console.log(`DOMAIN=${envMap.DOMAIN || ""}`);
|
|
1241
|
-
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "
|
|
1328
|
+
console.log(`BITCALL_ENV=${envMap.BITCALL_ENV || "production"}`);
|
|
1242
1329
|
console.log(`ROUTING_MODE=${envMap.ROUTING_MODE || "universal"}`);
|
|
1243
1330
|
console.log(`SIP_PROVIDER_URI=${envMap.SIP_PROVIDER_URI || ""}`);
|
|
1244
1331
|
console.log(`DEPLOY_MODE=${envMap.DEPLOY_MODE || ""}`);
|
|
@@ -1318,8 +1405,24 @@ async function certInstallCommand(options) {
|
|
|
1318
1405
|
|
|
1319
1406
|
function updateCommand() {
|
|
1320
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
|
+
|
|
1321
1420
|
runCompose(["pull"], { stdio: "inherit" });
|
|
1322
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");
|
|
1323
1426
|
}
|
|
1324
1427
|
|
|
1325
1428
|
function mediaStatusCommand() {
|
|
@@ -1402,11 +1505,40 @@ async function uninstallCommand(options) {
|
|
|
1402
1505
|
fs.rmSync(RENEW_HOOK_PATH, { force: true });
|
|
1403
1506
|
}
|
|
1404
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
|
+
|
|
1405
1530
|
if (fs.existsSync(GATEWAY_DIR)) {
|
|
1406
1531
|
fs.rmSync(GATEWAY_DIR, { recursive: true, force: true });
|
|
1407
1532
|
}
|
|
1408
1533
|
|
|
1409
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
|
+
}
|
|
1410
1542
|
}
|
|
1411
1543
|
|
|
1412
1544
|
function configCommand() {
|
|
@@ -1432,7 +1564,7 @@ function buildProgram() {
|
|
|
1432
1564
|
.description("Run setup wizard and provision gateway")
|
|
1433
1565
|
.option("--advanced", "Show advanced configuration prompts")
|
|
1434
1566
|
.option("--dev", "Quick dev setup (minimal prompts, permissive defaults)")
|
|
1435
|
-
.option("--production", "Production setup
|
|
1567
|
+
.option("--production", "Production setup profile")
|
|
1436
1568
|
.option("--domain <domain>", "Gateway domain")
|
|
1437
1569
|
.option("--email <email>", "Let's Encrypt email")
|
|
1438
1570
|
.option("--verbose", "Stream full installer command output")
|
|
@@ -1463,6 +1595,18 @@ function buildProgram() {
|
|
|
1463
1595
|
|
|
1464
1596
|
program.command("update").description("Pull latest image and restart service").action(updateCommand);
|
|
1465
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);
|
|
1466
1610
|
|
|
1467
1611
|
const media = program.command("media").description("Media firewall operations");
|
|
1468
1612
|
media.command("status").description("Show media IPv4-only firewall state").action(mediaStatusCommand);
|
|
@@ -1488,7 +1632,7 @@ module.exports = {
|
|
|
1488
1632
|
main,
|
|
1489
1633
|
normalizeInitProfile,
|
|
1490
1634
|
validateProductionConfig,
|
|
1491
|
-
|
|
1635
|
+
buildSecurityNotes,
|
|
1492
1636
|
buildQuickFlowDefaults,
|
|
1493
1637
|
shouldRequireAllowlist,
|
|
1494
1638
|
isOriginWildcard,
|