220 lines
8.9 KiB
PowerShell
220 lines
8.9 KiB
PowerShell
[CmdletBinding(SupportsShouldProcess)]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[ValidateNotNullOrEmpty()]
|
|
[string]$AdvertiseIP,
|
|
|
|
[Parameter()]
|
|
[string]$PublicBaseURL = "",
|
|
|
|
[Parameter()]
|
|
[string]$PublicWebBaseURL = "",
|
|
|
|
[Parameter()]
|
|
[string]$AdminBindIP = "127.0.0.1",
|
|
|
|
[Parameter()]
|
|
[switch]$HostNetwork,
|
|
|
|
[Parameter()]
|
|
[switch]$BridgeNetwork,
|
|
|
|
[Parameter()]
|
|
[switch]$AllowInsecureDevelopmentAuth
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
if ($HostNetwork -and $BridgeNetwork) {
|
|
throw "HostNetwork and BridgeNetwork are mutually exclusive."
|
|
}
|
|
|
|
$parsedIP = $null
|
|
if (-not [System.Net.IPAddress]::TryParse($AdvertiseIP, [ref]$parsedIP)) {
|
|
throw "AdvertiseIP must be an IPv4 or IPv6 address, not a DNS name."
|
|
}
|
|
$isLoopback = [System.Net.IPAddress]::IsLoopback($parsedIP)
|
|
$isIPv6 = $parsedIP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6
|
|
|
|
$parsedAdminBindIP = $null
|
|
if (-not [System.Net.IPAddress]::TryParse($AdminBindIP, [ref]$parsedAdminBindIP)) {
|
|
throw "AdminBindIP must be an IPv4 or IPv6 address."
|
|
}
|
|
|
|
if ([string]::IsNullOrWhiteSpace($PublicBaseURL)) {
|
|
if (-not $isLoopback) {
|
|
throw "PublicBaseURL is required when AdvertiseIP is not loopback."
|
|
}
|
|
$loopbackHost = if ($isIPv6) { "[::1]" } else { "127.0.0.1" }
|
|
$PublicBaseURL = "http://${loopbackHost}:2401"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($PublicWebBaseURL)) {
|
|
$PublicWebBaseURL = $PublicBaseURL
|
|
}
|
|
|
|
function Assert-HTTPURL {
|
|
param([string]$Name, [string]$Value)
|
|
$uri = $null
|
|
if (-not [Uri]::TryCreate($Value, [UriKind]::Absolute, [ref]$uri) -or
|
|
($uri.Scheme -ne "http" -and $uri.Scheme -ne "https") -or
|
|
-not [string]::IsNullOrEmpty($uri.UserInfo)) {
|
|
throw "$Name must be an absolute HTTP(S) URL without embedded credentials."
|
|
}
|
|
}
|
|
|
|
Assert-HTTPURL -Name "PublicBaseURL" -Value $PublicBaseURL
|
|
Assert-HTTPURL -Name "PublicWebBaseURL" -Value $PublicWebBaseURL
|
|
if (-not $isLoopback -and -not $AllowInsecureDevelopmentAuth) {
|
|
throw "Internet/LAN development-code auth requires AllowInsecureDevelopmentAuth; for production generate on loopback, then configure the webhook provider before startup."
|
|
}
|
|
|
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
|
$dockerDir = Join-Path $repoRoot "deploy\docker"
|
|
$templatePath = Join-Path $dockerDir ".env.example"
|
|
$outputPath = Join-Path $dockerDir ".env"
|
|
if (-not (Test-Path -LiteralPath $templatePath -PathType Leaf)) {
|
|
throw "Docker environment template not found: $templatePath"
|
|
}
|
|
if (Test-Path -LiteralPath $outputPath) {
|
|
throw "$outputPath already exists. Initialization never overwrites live credentials."
|
|
}
|
|
|
|
function New-HexSecret {
|
|
param([int]$Bytes = 32)
|
|
$buffer = New-Object byte[] $Bytes
|
|
$generator = [System.Security.Cryptography.RandomNumberGenerator]::Create()
|
|
try {
|
|
$generator.GetBytes($buffer)
|
|
}
|
|
finally {
|
|
$generator.Dispose()
|
|
}
|
|
return [BitConverter]::ToString($buffer).Replace("-", "").ToLowerInvariant()
|
|
}
|
|
|
|
function Get-GitValue {
|
|
param([string[]]$GitArguments)
|
|
try {
|
|
$value = & git -C $repoRoot @GitArguments 2>$null
|
|
if ($LASTEXITCODE -eq 0) { return ($value | Select-Object -First 1).Trim() }
|
|
}
|
|
catch {}
|
|
return "unknown"
|
|
}
|
|
|
|
$publicBindIP = if ($isIPv6) { "::" } else { "0.0.0.0" }
|
|
$localBindIP = if ($isIPv6) { "::1" } else { "127.0.0.1" }
|
|
if ($isLoopback) {
|
|
$publicBindIP = $parsedIP.ToString()
|
|
$localBindIP = $parsedIP.ToString()
|
|
}
|
|
elseif (([Uri]$PublicBaseURL).Scheme -eq "http") {
|
|
$publicLinkIP = $null
|
|
$publicLinkHost = ([Uri]$PublicBaseURL).Host.Trim([char[]]"[]")
|
|
if ([System.Net.IPAddress]::TryParse($publicLinkHost, [ref]$publicLinkIP) -and $publicLinkIP.Equals($parsedIP)) {
|
|
$localBindIP = $parsedIP.ToString()
|
|
}
|
|
}
|
|
|
|
$adminHealthIP = $parsedAdminBindIP.ToString()
|
|
if ($adminHealthIP -eq "0.0.0.0") { $adminHealthIP = "127.0.0.1" }
|
|
if ($adminHealthIP -eq "::") { $adminHealthIP = "::1" }
|
|
$publicListenHost = if ($publicBindIP.Contains(":")) { "[${publicBindIP}]" } else { $publicBindIP }
|
|
$localListenHost = if ($localBindIP.Contains(":")) { "[${localBindIP}]" } else { $localBindIP }
|
|
$serverHealthURLHost = $localListenHost
|
|
$adminListenIP = $parsedAdminBindIP.ToString()
|
|
$adminListenHost = if ($adminListenIP.Contains(":")) { "[${adminListenIP}]" } else { $adminListenIP }
|
|
$turnEnabled = (-not $isIPv6).ToString().ToLowerInvariant()
|
|
$turnAdvertiseIP = if ($isIPv6) { "127.0.0.1" } else { $parsedIP.ToString() }
|
|
$rtmpHost = if ($isIPv6) { "[$($parsedIP.ToString())]" } else { $parsedIP.ToString() }
|
|
$postgresPassword = New-HexSecret 24
|
|
|
|
$treeState = "unknown"
|
|
try {
|
|
$treeOutput = & git -C $repoRoot status --porcelain 2>$null
|
|
if ($LASTEXITCODE -eq 0) { $treeState = if (@($treeOutput).Count -gt 0) { "dirty" } else { "clean" } }
|
|
}
|
|
catch {}
|
|
|
|
$values = [ordered]@{
|
|
TELESRV_BUILD_COMMIT = Get-GitValue @("rev-parse", "HEAD")
|
|
TELESRV_BUILD_BRANCH = Get-GitValue @("rev-parse", "--abbrev-ref", "HEAD")
|
|
TELESRV_BUILD_TREE_STATE = $treeState
|
|
TELESRV_BUILD_DATE = [DateTime]::UtcNow.ToString("o")
|
|
POSTGRES_PASSWORD = $postgresPassword
|
|
TELESRV_POSTGRES_DSN = "postgres://telesrv:${postgresPassword}@127.0.0.1:15432/telesrv_main?sslmode=disable"
|
|
TELESRV_REDIS_PASSWORD = New-HexSecret 32
|
|
TELESRV_ADMIN_API_TOKEN = New-HexSecret 32
|
|
TELESRV_ADMIN_UI_PASSWORD = New-HexSecret 24
|
|
TELESRV_ADMIN_SESSION_KEY = New-HexSecret 32
|
|
TELESRV_TURN_SECRET = New-HexSecret 32
|
|
TELESRV_OTP_WEBHOOK_SECRET = New-HexSecret 32
|
|
TELESRV_ALLOW_INSECURE_DEVELOPMENT_AUTH = ($isLoopback -or $AllowInsecureDevelopmentAuth).ToString().ToLowerInvariant()
|
|
TELESRV_ADVERTISE_IP = $parsedIP.ToString()
|
|
TELESRV_PUBLIC_BASE_URL = $PublicBaseURL
|
|
TELESRV_PUBLIC_WEB_BASE_URL = $PublicWebBaseURL
|
|
TELESRV_SERVER_HOST_NETWORK = (-not $BridgeNetwork).ToString().ToLowerInvariant()
|
|
TELESRV_SFU_ADVERTISE_IP = $parsedIP.ToString()
|
|
TELESRV_TURN_ENABLE = $turnEnabled
|
|
TELESRV_TURN_ADVERTISE_IP = $turnAdvertiseIP
|
|
TELESRV_LIVESTREAM_RTMP_URL = "rtmp://${rtmpHost}:2400/live"
|
|
TELESRV_PUBLIC_BIND_IP = $publicBindIP
|
|
TELESRV_PUBLIC_LISTEN_HOST = $publicListenHost
|
|
TELESRV_LOCAL_BIND_IP = $localBindIP
|
|
TELESRV_LOCAL_LISTEN_HOST = $localListenHost
|
|
TELESRV_SERVER_HEALTH_IP = $localBindIP
|
|
TELESRV_SERVER_HEALTH_URL_HOST = $serverHealthURLHost
|
|
TELESRV_ADMIN_BIND_IP = $adminListenIP
|
|
TELESRV_ADMIN_LISTEN_HOST = $adminListenHost
|
|
TELESRV_ADMIN_HEALTH_IP = $adminHealthIP
|
|
}
|
|
|
|
$content = [IO.File]::ReadAllText($templatePath)
|
|
foreach ($entry in $values.GetEnumerator()) {
|
|
$pattern = "(?m)^$([Regex]::Escape($entry.Key))=.*$"
|
|
if (-not [Regex]::IsMatch($content, $pattern)) { throw "Template is missing $($entry.Key)." }
|
|
$replacement = ("{0}={1}" -f $entry.Key, $entry.Value).Replace('$', '$$')
|
|
$content = [Regex]::Replace($content, $pattern, $replacement)
|
|
}
|
|
|
|
function Protect-SecretFile {
|
|
param([string]$Path)
|
|
|
|
if ($env:OS -eq "Windows_NT") {
|
|
$owner = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner($owner)
|
|
$identities = @(
|
|
$owner,
|
|
(New-Object System.Security.Principal.SecurityIdentifier("S-1-5-18")),
|
|
(New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544"))
|
|
)
|
|
foreach ($identity in $identities) {
|
|
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
|
|
$identity,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.AccessControlType]::Allow
|
|
)
|
|
[void]$acl.AddAccessRule($rule)
|
|
}
|
|
Set-Acl -LiteralPath $Path -AclObject $acl
|
|
return
|
|
}
|
|
|
|
& chmod 600 -- $Path
|
|
if ($LASTEXITCODE -ne 0) { throw "chmod 600 failed for $Path" }
|
|
}
|
|
|
|
if ($PSCmdlet.ShouldProcess($outputPath, "write owner-only Docker deployment environment")) {
|
|
$temporaryPath = "$outputPath.tmp.$PID"
|
|
try {
|
|
[IO.File]::WriteAllText($temporaryPath, $content, (New-Object Text.UTF8Encoding($false)))
|
|
Protect-SecretFile -Path $temporaryPath
|
|
Move-Item -LiteralPath $temporaryPath -Destination $outputPath
|
|
}
|
|
finally {
|
|
if (Test-Path -LiteralPath $temporaryPath) { Remove-Item -LiteralPath $temporaryPath -Force }
|
|
}
|
|
Write-Host "Created $outputPath with generated deployment credentials."
|
|
}
|