Go:Add build pipeline for go server and fix web login bug
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
@echo off
|
@echo off
|
||||||
:: SimpleRemoter Quick Build Script
|
:: SimpleRemoter Quick Build Script
|
||||||
:: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish]
|
:: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish|go-server]
|
||||||
|
:: go-server Build Go fallback server only -> Bin\YamaGo_x64.exe
|
||||||
|
:: go-server publish Same, plus UPX --best compression
|
||||||
|
|
||||||
setlocal enabledelayedexpansion
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
@@ -18,6 +20,7 @@ if /i "%~1"=="all" (set PLATFORM=all& shift& goto :parse_args)
|
|||||||
if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args)
|
if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args)
|
||||||
if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args)
|
if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args)
|
||||||
if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args)
|
if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args)
|
||||||
|
if /i "%~1"=="go-server" (set EXTRA_ARGS=!EXTRA_ARGS! -GoServer& shift& goto :parse_args)
|
||||||
echo Unknown argument: %~1
|
echo Unknown argument: %~1
|
||||||
shift
|
shift
|
||||||
goto :parse_args
|
goto :parse_args
|
||||||
|
|||||||
103
build.ps1
103
build.ps1
@@ -15,11 +15,110 @@ param(
|
|||||||
|
|
||||||
[switch]$ServerOnly, # Only build main server (Yama), skip client projects
|
[switch]$ServerOnly, # Only build main server (Yama), skip client projects
|
||||||
[switch]$Clean, # Clean before build
|
[switch]$Clean, # Clean before build
|
||||||
[switch]$Publish # Publish mode: rebuild all deps + x64 Release + UPX compress
|
[switch]$Publish, # Publish mode: rebuild all deps + x64 Release + UPX compress
|
||||||
|
[switch]$GoServer # Build Go fallback server (server/go) -> Bin/YamaGo_x64.exe
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$rootDir = $PSScriptRoot
|
||||||
|
$binDir = Join-Path $rootDir "Bin"
|
||||||
|
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
|
||||||
|
|
||||||
|
# Build Go fallback server. No-op (with warning) if Go compiler is not installed.
|
||||||
|
# When -Compress is set, run UPX --best on the output (mirrors C++ publish flow).
|
||||||
|
function Build-GoServer {
|
||||||
|
param(
|
||||||
|
[string]$Configuration,
|
||||||
|
[switch]$Compress
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Building Go server (server/go)..." -ForegroundColor Magenta
|
||||||
|
|
||||||
|
$goCmd = Get-Command go -ErrorAction SilentlyContinue
|
||||||
|
if (-not $goCmd) {
|
||||||
|
Write-Host "WARNING: Go compiler not found in PATH. Skipping Go server build." -ForegroundColor Yellow
|
||||||
|
Write-Host " Install from https://go.dev/dl/ and ensure 'go' is in PATH." -ForegroundColor DarkGray
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Using Go: $($goCmd.Source)" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
$goDir = Join-Path $rootDir "server\go"
|
||||||
|
if (-not (Test-Path $goDir)) {
|
||||||
|
Write-Host "ERROR: Go source directory not found at $goDir" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sync web assets (mirrors Makefile `sync` target — single source is server/web/index.html)
|
||||||
|
$webSrc = Join-Path $rootDir "server\web\index.html"
|
||||||
|
$webDstDir = Join-Path $goDir "web\assets"
|
||||||
|
if (Test-Path $webSrc) {
|
||||||
|
if (-not (Test-Path $webDstDir)) { New-Item -ItemType Directory -Path $webDstDir -Force | Out-Null }
|
||||||
|
Copy-Item -Path $webSrc -Destination (Join-Path $webDstDir "index.html") -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
|
||||||
|
|
||||||
|
$outFile = Join-Path $binDir "YamaGo_x64.exe"
|
||||||
|
# Release strips debug info for smaller binary; Debug keeps symbols.
|
||||||
|
$ldflags = if ($Configuration -eq "Debug") { "" } else { "-s -w" }
|
||||||
|
|
||||||
|
Push-Location $goDir
|
||||||
|
try {
|
||||||
|
$env:GOOS = "windows"
|
||||||
|
$env:GOARCH = "amd64"
|
||||||
|
if ($ldflags) {
|
||||||
|
& go build -ldflags $ldflags -o $outFile ./cmd
|
||||||
|
} else {
|
||||||
|
& go build -o $outFile ./cmd
|
||||||
|
}
|
||||||
|
$code = $LASTEXITCODE
|
||||||
|
} finally {
|
||||||
|
Pop-Location
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($code -ne 0) {
|
||||||
|
Write-Host "ERROR: Go build failed (exit $code)" -ForegroundColor Red
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = (Get-Item $outFile).Length / 1MB
|
||||||
|
Write-Host "OK: $outFile ($($size.ToString('F2')) MB)" -ForegroundColor Green
|
||||||
|
|
||||||
|
# In-place UPX compression. Failure is a warning, not an error — the
|
||||||
|
# uncompressed binary is still usable, and UPX occasionally refuses on
|
||||||
|
# certain PE sections.
|
||||||
|
if ($Compress) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "UPX compressing Go server..." -ForegroundColor Magenta
|
||||||
|
if (-not (Test-Path $upxPath)) {
|
||||||
|
Write-Host "WARNING: UPX not found at $upxPath — skipping compression" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$sizeBefore = (Get-Item $outFile).Length / 1MB
|
||||||
|
Write-Host " Before: $($sizeBefore.ToString('F2')) MB" -ForegroundColor DarkGray
|
||||||
|
& $upxPath --best $outFile
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Host "WARNING: UPX compression failed, uncompressed binary kept" -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$sizeAfter = (Get-Item $outFile).Length / 1MB
|
||||||
|
$ratio = (1 - $sizeAfter / $sizeBefore) * 100
|
||||||
|
Write-Host " After: $($sizeAfter.ToString('F2')) MB (-$($ratio.ToString('F1'))%)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Go-only fast path: skip MSBuild entirely. -Publish here means "compress the
|
||||||
|
# Go binary too" (not the full C++ publish flow).
|
||||||
|
if ($GoServer) {
|
||||||
|
$ok = Build-GoServer -Configuration $Config -Compress:$Publish
|
||||||
|
if (-not $ok) { exit 1 }
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
# Find MSBuild (VS2019 or VS2022, including Insiders/Preview)
|
# Find MSBuild (VS2019 or VS2022, including Insiders/Preview)
|
||||||
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
|
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
|
||||||
$msBuildPaths = @(
|
$msBuildPaths = @(
|
||||||
@@ -72,9 +171,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
|
|||||||
|
|
||||||
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
|
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
|
||||||
|
|
||||||
$rootDir = $PSScriptRoot
|
|
||||||
$slnFile = Join-Path $rootDir "YAMA.sln"
|
$slnFile = Join-Path $rootDir "YAMA.sln"
|
||||||
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
|
|
||||||
|
|
||||||
# Publish mode overrides
|
# Publish mode overrides
|
||||||
if ($Publish) {
|
if ($Publish) {
|
||||||
|
|||||||
@@ -668,6 +668,15 @@ func main() {
|
|||||||
|
|
||||||
log := logger.New(logCfg)
|
log := logger.New(logCfg)
|
||||||
|
|
||||||
|
// Track env vars where we fell back to a built-in default. Printed once
|
||||||
|
// at the end of startup so the operator sees what's in effect — vars the
|
||||||
|
// operator explicitly set are NOT listed (they already know their value).
|
||||||
|
type defaultedEnv struct{ name, value string }
|
||||||
|
var defaultsUsed []defaultedEnv
|
||||||
|
rememberDefault := func(name, value string) {
|
||||||
|
defaultsUsed = append(defaultsUsed, defaultedEnv{name, value})
|
||||||
|
}
|
||||||
|
|
||||||
// Create auth config
|
// Create auth config
|
||||||
authCfg := auth.DefaultConfig()
|
authCfg := auth.DefaultConfig()
|
||||||
// PwdHash can be set from environment or config file
|
// PwdHash can be set from environment or config file
|
||||||
@@ -675,6 +684,7 @@ func main() {
|
|||||||
if authCfg.PwdHash == "" {
|
if authCfg.PwdHash == "" {
|
||||||
// Default placeholder - should be configured in production
|
// Default placeholder - should be configured in production
|
||||||
authCfg.PwdHash = "61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
|
authCfg.PwdHash = "61f04dd637a74ee34493fc1025de2c131022536da751c29e3ff4e9024d8eec43"
|
||||||
|
rememberDefault("YAMA_PWDHASH", authCfg.PwdHash)
|
||||||
}
|
}
|
||||||
authCfg.SuperPass = os.Getenv("YAMA_PWD")
|
authCfg.SuperPass = os.Getenv("YAMA_PWD")
|
||||||
|
|
||||||
@@ -721,19 +731,23 @@ func main() {
|
|||||||
// Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS;
|
// Web user authenticator. Bootstrap admin from env var YAMA_WEB_ADMIN_PASS;
|
||||||
// if unset, fall back to YAMA_PWD (same secret the TCP authorization uses)
|
// if unset, fall back to YAMA_PWD (same secret the TCP authorization uses)
|
||||||
// so a single password env var is enough to bring up the whole stack.
|
// so a single password env var is enough to bring up the whole stack.
|
||||||
// If neither is set, no admin is registered and login will always fail —
|
// If neither is set we use a hard-coded "admin" default so the web UI is
|
||||||
// the user must define a password before browsers can log in.
|
// usable out of the box — the startup banner surfaces this so operators
|
||||||
|
// know to override it in any non-dev deployment.
|
||||||
|
const defaultWebAdminPass = "admin"
|
||||||
webAuth := wsauth.New()
|
webAuth := wsauth.New()
|
||||||
adminPass := os.Getenv("YAMA_WEB_ADMIN_PASS")
|
adminPass := os.Getenv("YAMA_WEB_ADMIN_PASS")
|
||||||
if adminPass == "" {
|
if adminPass == "" {
|
||||||
adminPass = os.Getenv("YAMA_PWD")
|
adminPass = os.Getenv("YAMA_PWD")
|
||||||
}
|
}
|
||||||
if adminPass != "" {
|
usingDefaultWebPass := false
|
||||||
|
if adminPass == "" {
|
||||||
|
adminPass = defaultWebAdminPass
|
||||||
|
rememberDefault("YAMA_WEB_ADMIN_PASS", defaultWebAdminPass)
|
||||||
|
usingDefaultWebPass = true
|
||||||
|
}
|
||||||
webAuth.AddAdminFromPlainPassword("admin", adminPass)
|
webAuth.AddAdminFromPlainPassword("admin", adminPass)
|
||||||
log.Info("Web admin user configured")
|
log.Info("Web admin user configured")
|
||||||
} else {
|
|
||||||
log.Warn("Neither YAMA_WEB_ADMIN_PASS nor YAMA_PWD is set; web login will be unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persistent users live in users.json next to the binary's working dir
|
// Persistent users live in users.json next to the binary's working dir
|
||||||
// — same default the C++ WebService uses (m_ConfigDir + "users.json").
|
// — same default the C++ WebService uses (m_ConfigDir + "users.json").
|
||||||
@@ -741,6 +755,7 @@ func main() {
|
|||||||
usersFile := os.Getenv("YAMA_USERS_FILE")
|
usersFile := os.Getenv("YAMA_USERS_FILE")
|
||||||
if usersFile == "" {
|
if usersFile == "" {
|
||||||
usersFile = "users.json"
|
usersFile = "users.json"
|
||||||
|
rememberDefault("YAMA_USERS_FILE", usersFile)
|
||||||
}
|
}
|
||||||
if err := webAuth.SetUsersFile(usersFile); err != nil {
|
if err := webAuth.SetUsersFile(usersFile); err != nil {
|
||||||
log.Warn("Failed to load %s: %v (continuing with admin only)", usersFile, err)
|
log.Warn("Failed to load %s: %v (continuing with admin only)", usersFile, err)
|
||||||
@@ -849,10 +864,21 @@ func main() {
|
|||||||
fmt.Printf("Server started on port(s): %v\n", ports)
|
fmt.Printf("Server started on port(s): %v\n", ports)
|
||||||
if *httpPort != 0 {
|
if *httpPort != 0 {
|
||||||
fmt.Printf("Web UI on http://localhost:%d/\n", *httpPort)
|
fmt.Printf("Web UI on http://localhost:%d/\n", *httpPort)
|
||||||
|
if usingDefaultWebPass {
|
||||||
|
fmt.Printf(" Default login: admin / %s (set YAMA_WEB_ADMIN_PASS to override)\n",
|
||||||
|
defaultWebAdminPass)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if licenseHTTP != nil {
|
if licenseHTTP != nil {
|
||||||
fmt.Printf("License Server on http://%s/license/{sign,heartbeat}\n", licAddr)
|
fmt.Printf("License Server on http://%s/license/{sign,heartbeat}\n", licAddr)
|
||||||
}
|
}
|
||||||
|
if len(defaultsUsed) > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("[!] Using built-in defaults (set the env var to override):")
|
||||||
|
for _, d := range defaultsUsed {
|
||||||
|
fmt.Printf(" %s = %s\n", d.name, d.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
fmt.Println("Logs are written to: logs/server.log")
|
fmt.Println("Logs are written to: logs/server.log")
|
||||||
fmt.Println("Press Ctrl+C to stop...")
|
fmt.Println("Press Ctrl+C to stop...")
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,25 @@ import (
|
|||||||
|
|
||||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/hub"
|
"github.com/yuanyuanxiang/SimpleRemoter/server/go/hub"
|
||||||
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
|
"github.com/yuanyuanxiang/SimpleRemoter/server/go/protocol"
|
||||||
|
"github.com/yuanyuanxiang/SimpleRemoter/server/go/wsauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// rotateChallenge replaces the client's nonce with a fresh one and pushes a
|
||||||
|
// {cmd:"challenge"} message so the browser updates its stored nonce. Called
|
||||||
|
// after every login failure so a retry on the SAME WebSocket works without
|
||||||
|
// the user having to refresh — the old nonce is still burned for replay
|
||||||
|
// protection, but the client now has a usable new one.
|
||||||
|
func (h *wsHub) rotateChallenge(c *wsClient) {
|
||||||
|
n, err := wsauth.NewNonce()
|
||||||
|
if err != nil {
|
||||||
|
h.log.Error("nonce regen failed: %v", err)
|
||||||
|
c.nonce = ""
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.nonce = n
|
||||||
|
c.queue([]byte(`{"cmd":"challenge","nonce":"` + n + `"}`))
|
||||||
|
}
|
||||||
|
|
||||||
// dispatch routes one inbound message to its handler. The `raw` payload is
|
// dispatch routes one inbound message to its handler. The `raw` payload is
|
||||||
// passed through so handlers can re-parse to their own shape.
|
// passed through so handlers can re-parse to their own shape.
|
||||||
//
|
//
|
||||||
@@ -125,10 +142,12 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) {
|
|||||||
// with a uniform "credentials" error so the limit is not detectable.
|
// with a uniform "credentials" error so the limit is not detectable.
|
||||||
if !h.allowLoginByIP(c) || !h.allowLoginByUsername(in.Username) {
|
if !h.allowLoginByIP(c) || !h.allowLoginByUsername(in.Username) {
|
||||||
h.log.Warn("ws login throttled: user=%s addr=%s", in.Username, c.addr)
|
h.log.Warn("ws login throttled: user=%s addr=%s", in.Username, c.addr)
|
||||||
// Burn the challenge so the attacker can't immediately replay.
|
|
||||||
c.nonce = ""
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"}))
|
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"}))
|
||||||
|
// Rotate the challenge: burns the previous nonce (replay protection)
|
||||||
|
// AND hands the client a fresh one so the next attempt does not
|
||||||
|
// require a page refresh.
|
||||||
|
h.rotateChallenge(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,18 +155,18 @@ func (h *wsHub) handleLogin(c *wsClient, raw []byte) {
|
|||||||
// replays from a different connection can't reuse a captured response.
|
// replays from a different connection can't reuse a captured response.
|
||||||
if in.Nonce == "" || in.Nonce != c.nonce {
|
if in.Nonce == "" || in.Nonce != c.nonce {
|
||||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid challenge"}))
|
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid challenge"}))
|
||||||
|
h.rotateChallenge(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, role, err := h.auth.VerifyLogin(in.Username, in.Response, in.Nonce)
|
token, role, err := h.auth.VerifyLogin(in.Username, in.Response, in.Nonce)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Burn the challenge on failure too — forces a new round on retry.
|
|
||||||
c.nonce = ""
|
|
||||||
// Fixed delay on failure: makes online brute force impractical
|
// Fixed delay on failure: makes online brute force impractical
|
||||||
// even within the rate-limit budget, and erases the timing
|
// even within the rate-limit budget, and erases the timing
|
||||||
// difference between "wrong password" and "wrong nonce".
|
// difference between "wrong password" and "wrong nonce".
|
||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"}))
|
c.queue(mustJSON(map[string]any{"cmd": "login_result", "ok": false, "msg": "Invalid credentials"}))
|
||||||
|
h.rotateChallenge(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Successful login: clear the per-IP/per-user budgets so a legitimate
|
// Successful login: clear the per-IP/per-user budgets so a legitimate
|
||||||
|
|||||||
Reference in New Issue
Block a user