Feature: Support subpath deployment for Go web UI
Add optional YAMA_WEB_BASE_PATH to mount the whole web UI (index, manifest, static assets, /health, /api/devices, /ws) under a URL prefix such as /remoter behind a reverse proxy. index.html now uses a __BASE_PATH__ placeholder substituted at serve time, the PWA manifest start_url is rooted at the base path, and a mount wrapper redirects the bare prefix to prefix/ and strips it before routing. Include nginx, systemd, and env-file deployment examples for the subpath setup. The shared placeholder is also handled by the MFC server, which serves from the domain root and strips __BASE_PATH__ (to "") so its web remote desktop URLs stay rooted. Co-Authored-By: deepseek-v4-pro
This commit is contained in:
1 parent
32cc7af371
commit
42df128f62
7 files changed
+254
-12
No files matched your search
@@ -35,6 +35,16 @@ static std::string LoadBinaryResourceAsString(int resourceId) {
|
||||
return std::string(static_cast<const char*>(p), size);
|
||||
}
|
||||
|
||||
// Replace all occurrences of `from` with `to` in-place.
|
||||
static void ReplaceAll(std::string& s, const std::string& from, const std::string& to) {
|
||||
if (from.empty()) return;
|
||||
size_t pos = 0;
|
||||
while ((pos = s.find(from, pos)) != std::string::npos) {
|
||||
s.replace(pos, from.length(), to);
|
||||
pos += to.length();
|
||||
}
|
||||
}
|
||||
|
||||
// Challenge-response nonce storage (prevents replay attacks)
|
||||
static std::map<void*, std::string> s_ClientNonces;
|
||||
static std::mutex s_NonceMutex;
|
||||
@@ -231,6 +241,10 @@ void CWebService::ServerThread(int port) {
|
||||
|
||||
// Serve static HTML page and file downloads
|
||||
static std::string cachedHtml = GetWebPageHTML();
|
||||
// server/web/index.html carries a __BASE_PATH__ placeholder for the Go
|
||||
// server's subpath deployment. The MFC server serves from the domain root,
|
||||
// so strip it (→ "") — otherwise the WebSocket/CSS/JS URLs break.
|
||||
ReplaceAll(cachedHtml, "__BASE_PATH__", "");
|
||||
// Log loaded size; zero bytes means the RC BINARY resource is missing or
|
||||
// server/web/index.html was not embedded — check 2015Remote.rc and rebuild.
|
||||
Mprintf("[WebService] index.html loaded: %zu bytes\n", cachedHtml.size());
|
||||
|
||||
@@ -661,6 +661,19 @@ func splitCSV(s string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// normalizeBasePath turns a YAMA_WEB_BASE_PATH value into a leading-slash,
|
||||
// no-trailing-slash prefix ("" for root). "" (or "/") means the web UI is
|
||||
// served at the domain root (方案A); "/remoter" means it is served under that
|
||||
// prefix (方案B), with a reverse proxy routing the subpath to this process.
|
||||
func normalizeBasePath(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.Trim(s, "/")
|
||||
if s == "" {
|
||||
return ""
|
||||
}
|
||||
return "/" + s
|
||||
}
|
||||
|
||||
// runIssueToken handles the "issue-token" subcommand. It mints a customer JWT
|
||||
// signed with the operator's RSA private key and prints it to stdout.
|
||||
//
|
||||
@@ -918,11 +931,16 @@ func main() {
|
||||
if trustProxy {
|
||||
log.Info("Trusting X-Forwarded-For for client IP — make sure a reverse proxy is in front")
|
||||
}
|
||||
basePath := normalizeBasePath(os.Getenv("YAMA_WEB_BASE_PATH"))
|
||||
if basePath != "" {
|
||||
log.Info("Serving web UI under base path %q (subpath deployment)", basePath)
|
||||
}
|
||||
webCfg := web.Config{
|
||||
AllowedOrigins: allowedOrigins,
|
||||
LoginIPLimit: wsauth.NewRateLimiter(10, time.Minute),
|
||||
LoginUserLimit: wsauth.NewRateLimiter(5, 15*time.Minute),
|
||||
TrustForwardedFor: trustProxy,
|
||||
BasePath: basePath,
|
||||
}
|
||||
|
||||
// Start HTTP server for web UI. Hub gives it read-only access to the
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# ============================================================
|
||||
# YAMA 网页端 nginx 反代示例(方案B:子路径部署)
|
||||
# 作为站点配置放入 /etc/nginx/sites-available/yama 后软链到 sites-enabled
|
||||
# 核心原则:nginx 只做 TLS 终止 + 完整路径转发,由 Go 端
|
||||
# YAMA_WEB_BASE_PATH=/remoter 负责 strip
|
||||
# ============================================================
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name 你的域名.com;
|
||||
|
||||
# TLS 证书(路径换成你自己的,如 certbot/letsencrypt 生成的位置)
|
||||
ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||
|
||||
# 裸 /remoter(没写结尾斜杠)→ 301 到 /remoter/
|
||||
location = /remoter {
|
||||
return 301 /remoter/;
|
||||
}
|
||||
|
||||
# 子路径:完整路径原样转发,Go 端按 YAMA_WEB_BASE_PATH 再 strip
|
||||
location /remoter/ {
|
||||
# 结尾没有 "/" → /remoter/... 原样转发(不要写成 http://127.0.0.1:8080/)
|
||||
# Go 服务端若在另一台机器,换成 http://192.168.1.50:8080;
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
|
||||
# WebSocket 升级(远程桌面走 /remoter/ws,不配这两行连不上)
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 远程桌面连接长时间空闲,避免默认 60s 被 nginx 掐断
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
|
||||
# 大文件/流式传输时避免缓冲占内存(可选)
|
||||
# proxy_buffering off;
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 方案A(根路径部署):不设 YAMA_WEB_BASE_PATH,用下面这段替代上面
|
||||
# ============================================================
|
||||
# server {
|
||||
# listen 443 ssl http2;
|
||||
# server_name 你的域名.com;
|
||||
# ssl_certificate /etc/letsencrypt/live/你的域名.com/fullchain.pem;
|
||||
# ssl_certificate_key /etc/letsencrypt/live/你的域名.com/privkey.pem;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://127.0.0.1:8080;
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Upgrade $http_upgrade;
|
||||
# proxy_set_header Connection "upgrade";
|
||||
# proxy_set_header Host $host;
|
||||
# proxy_set_header X-Real-IP $remote_addr;
|
||||
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# proxy_read_timeout 3600s;
|
||||
# proxy_send_timeout 3600s;
|
||||
# }
|
||||
# }
|
||||
@@ -0,0 +1,64 @@
|
||||
# ============================================================
|
||||
# YAMA 服务端环境变量(完整版)
|
||||
# systemd 通过 EnvironmentFile=-/etc/yama/server.env 读取
|
||||
# 行首 "#" = 未启用(保持默认);去掉 "#" 即生效
|
||||
# ============================================================
|
||||
|
||||
# ---------- 账号 / 密码 ----------
|
||||
|
||||
# Web 管理员账号 "admin" 的初始密码。回退链:
|
||||
# YAMA_WEB_ADMIN_PASS → YAMA_PWD → 硬编码 "admin"
|
||||
# 生产环境务必设置,否则会以 admin/admin 裸奔(启动日志会红色告警)
|
||||
YAMA_WEB_ADMIN_PASS=ChangeMe_Strong
|
||||
|
||||
# 超级密码(明文)。TCP 授权用,也是上面 Web 管理员密码的回退来源
|
||||
#YAMA_PWD=
|
||||
|
||||
# TCP 授权的密码哈希(SHA-256 十六进制)。与 YAMA_PWD 二选一;
|
||||
# 未设置时用内置占位默认值(生产应覆盖)
|
||||
#YAMA_PWDHASH=
|
||||
|
||||
# 持久化 Web 用户文件路径(默认相对工作目录的 users.json)
|
||||
#YAMA_USERS_FILE=/opt/yama/users.json
|
||||
|
||||
# ---------- 网页端 / 反向代理 ----------
|
||||
|
||||
# 子路径部署前缀(方案B)。空 = 根路径(方案A);有值须与 nginx location 一致
|
||||
YAMA_WEB_BASE_PATH=/remoter
|
||||
|
||||
# "1" = 信任反向代理的 X-Forwarded-For(nginx 在 Go 前面时务必开启,
|
||||
# 否则登录限流会把所有客户端当成 nginx 同一个 IP)
|
||||
YAMA_WEB_TRUST_PROXY=1
|
||||
|
||||
# WebSocket Origin 白名单(逗号分隔)。空 = 仅允许同源
|
||||
#YAMA_WEB_ALLOWED_ORIGINS=https://你的域名
|
||||
|
||||
# ---------- 授权 / 许可(signer)----------
|
||||
# 优先级:YAMA_SIGN_PASSWORD > YAMA_LICENSE_DISABLED=1 > YAMA_LICENSE_TOKEN > 匿名试用
|
||||
|
||||
# 本地签名主密钥(HMAC)。设置即进入 LocalSigner 模式(运营方自部署)
|
||||
#YAMA_SIGN_PASSWORD=
|
||||
|
||||
# "1" = 关闭授权校验(NoOpSigner,离线/开发用;客户端将拒绝屏幕/文件功能)
|
||||
#YAMA_LICENSE_DISABLED=1
|
||||
|
||||
# 许可服务器 URL(RemoteSigner 付费客户 / 试用模式的默认地址)
|
||||
#YAMA_LICENSE_SERVER=
|
||||
|
||||
# 客户 JWT(付费客户解锁设备配额)
|
||||
#YAMA_LICENSE_TOKEN=
|
||||
|
||||
# 缓存 TTL(小时,RemoteSigner 离线缓存,默认 24)
|
||||
#YAMA_LICENSE_OFFLINE_HRS=24
|
||||
|
||||
# ---------- 兼做许可服务器(可选,LocalSigner-as-LS)----------
|
||||
|
||||
# issue-token 子命令用:RSA 私钥 PEM 路径(与公钥配对)
|
||||
#YAMA_LICENSE_PRIVATE_KEY=
|
||||
|
||||
# 对外签发许可用:RSA 公钥 PEM 路径 + 监听地址(如 ":8443")
|
||||
#YAMA_LICENSE_PUBLIC_KEY=
|
||||
#YAMA_LICENSE_HTTP_ADDR=:8443
|
||||
|
||||
# 配额状态持久化文件路径
|
||||
#YAMA_LICENSE_STATE_PATH=/opt/yama/license_state.json
|
||||
@@ -0,0 +1,35 @@
|
||||
[Unit]
|
||||
Description=YAMA Remote Administration Server
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=yama
|
||||
Group=yama
|
||||
WorkingDirectory=/opt/yama
|
||||
|
||||
# 密钥 / 部署参数集中放这里(权限 600)。前面的 "-" 表示文件不存在也不报错。
|
||||
EnvironmentFile=-/etc/yama/server.env
|
||||
|
||||
# 6543 = 客户端(agent)回连端口;8080 = 网页端 HTTP(由 nginx 反向代理)
|
||||
# arm64 机器请把二进制名换成 server_linux_arm64
|
||||
ExecStart=/opt/yama/server_linux_amd64 -port 6543 -http-port 8080 -no-console
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
# --- 安全加固(可选,不需要可删掉整段) ---
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=full
|
||||
# 若服务需要写 /opt/yama 之外的目录,再放开这一行:
|
||||
# ReadWritePaths=/opt/yama/data
|
||||
|
||||
# 日志交给 journald
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=yama
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
+50
-7
@@ -1,6 +1,7 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -30,6 +31,7 @@ type Server struct {
|
||||
loginIPLimit *wsauth.RateLimiter
|
||||
loginUserLimit *wsauth.RateLimiter
|
||||
trustForwardedFor bool // honor X-Forwarded-For (behind trusted proxy only)
|
||||
basePath string // web UI mount prefix (e.g. "/remoter"); "" = domain root
|
||||
}
|
||||
|
||||
// Config tunes the server's exposed-on-public-HTTPS hardening knobs.
|
||||
@@ -52,6 +54,12 @@ type Config struct {
|
||||
// exposure the header is client-controlled and would let attackers
|
||||
// evade per-IP rate limits.
|
||||
TrustForwardedFor bool
|
||||
// BasePath mounts the whole web UI (index, manifest, static assets,
|
||||
// /health, /api/devices, /ws) under a URL prefix, so a reverse proxy
|
||||
// can serve it from a subpath of an existing domain (e.g.
|
||||
// https://example.com/remoter). Must be empty or a leading-slash,
|
||||
// no-trailing-slash path; "" (default) serves from the domain root.
|
||||
BasePath string
|
||||
}
|
||||
|
||||
// New creates an HTTP server bound to the given port. port=0 disables the server.
|
||||
@@ -68,6 +76,7 @@ func (s *Server) WithConfig(cfg Config) *Server {
|
||||
s.loginIPLimit = cfg.LoginIPLimit
|
||||
s.loginUserLimit = cfg.LoginUserLimit
|
||||
s.trustForwardedFor = cfg.TrustForwardedFor
|
||||
s.basePath = cfg.BasePath
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -96,7 +105,7 @@ func (s *Server) Start() error {
|
||||
|
||||
s.srv = &http.Server{
|
||||
Addr: ":" + strconv.Itoa(s.port),
|
||||
Handler: mux,
|
||||
Handler: s.mount(mux),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
@@ -129,14 +138,41 @@ func (s *Server) Stop() {
|
||||
_ = s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// basePathPlaceholder is the token in index.html substituted with the
|
||||
// configured base path at serve time. Empty base path → "" → root URLs.
|
||||
const basePathPlaceholder = "__BASE_PATH__"
|
||||
|
||||
// mount wraps the root-path mux with the configured base path. When basePath
|
||||
// is empty the mux is returned unchanged (domain-root serving, 方案A). Otherwise
|
||||
// the bare prefix redirects to prefix+"/", paths outside the prefix 404, and
|
||||
// matching paths are stripped before reaching the mux (方案B, subpath serving).
|
||||
func (s *Server) mount(h http.Handler) http.Handler {
|
||||
if s.basePath == "" {
|
||||
return h
|
||||
}
|
||||
base := s.basePath // normalized by cmd: leading "/", no trailing "/"
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == base {
|
||||
http.Redirect(w, r, base+"/", http.StatusMovedPermanently)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(r.URL.Path, base+"/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.StripPrefix(base, h).ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/index.html" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
body := bytes.ReplaceAll(IndexHTML, []byte(basePathPlaceholder), []byte(s.basePath))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
_, _ = w.Write(IndexHTML)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -190,21 +226,28 @@ func (s *Server) unauthorized(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
// PWA manifest. Referenced by <link rel="manifest"> in index.html.
|
||||
// Static JSON, no template needed.
|
||||
const manifestJSON = `{
|
||||
// start_url is rooted at the configured base path so an installed PWA
|
||||
// launches into the right location; "/" when served from the domain root.
|
||||
func (s *Server) manifestJSON() []byte {
|
||||
start := "/"
|
||||
if s.basePath != "" {
|
||||
start = s.basePath + "/"
|
||||
}
|
||||
return []byte(fmt.Sprintf(`{
|
||||
"name": "SimpleRemoter",
|
||||
"short_name": "Remoter",
|
||||
"start_url": "/",
|
||||
"start_url": "%s",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "#1a1a2e",
|
||||
"theme_color": "#1a1a2e"
|
||||
}`
|
||||
}`, start))
|
||||
}
|
||||
|
||||
func (s *Server) handleManifest(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/manifest+json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=86400")
|
||||
_, _ = w.Write([]byte(manifestJSON))
|
||||
_, _ = w.Write(s.manifestJSON())
|
||||
}
|
||||
|
||||
// staticHandler returns an http.HandlerFunc that serves a fixed byte slice
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Remoter">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="theme-color" content="#1a1a2e">
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
<link rel="manifest" href="__BASE_PATH__/manifest.json">
|
||||
<!-- xterm.js (远程终端). Files served by WebService from RC binary resources. -->
|
||||
<link rel="stylesheet" href="/static/xterm.css">
|
||||
<link rel="stylesheet" href="__BASE_PATH__/static/xterm.css">
|
||||
<link rel="icon" type="image/png" href="data:image/png;base64,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">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
@@ -1349,8 +1349,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/xterm.js"></script>
|
||||
<script src="/static/xterm-fit.js"></script>
|
||||
<script src="__BASE_PATH__/static/xterm.js"></script>
|
||||
<script src="__BASE_PATH__/static/xterm-fit.js"></script>
|
||||
|
||||
<!-- Opus codec for audio decompression -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/opus.js@0.5.0/dist/opus.js"></script>
|
||||
@@ -1524,7 +1524,7 @@
|
||||
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
updateWsStatus('connecting');
|
||||
try {
|
||||
ws = new WebSocket(protocol + '//' + location.host + '/ws');
|
||||
ws = new WebSocket(protocol + '//' + location.host + '__BASE_PATH__/ws');
|
||||
} catch (e) {
|
||||
updateWsStatus('disconnected');
|
||||
scheduleReconnect();
|
||||
|
||||
Reference in new issue
Block a user