mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-04 04:32:22 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdba252fa8 | |||
| 9faeac6ff9 | |||
| 6c357f2842 |
@@ -56,11 +56,21 @@ func (srv *RttyServer) ListenAPI() error {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
r := gin.New()
|
||||
|
||||
r.Use(func(c *gin.Context) {
|
||||
hi := getHostInfoFromRequest(c.Request)
|
||||
|
||||
host := hi.Host
|
||||
allowedHost := cfg.WebUIHost
|
||||
// If WebUIHost is configured, enforce host validation
|
||||
if allowedHost != "" && !isIPHost(host) {
|
||||
if !domainAllowed(host, allowedHost) {
|
||||
html := generateErrorHTML("invalid")
|
||||
c.Data(http.StatusBadRequest, "text/html; charset=utf-8", []byte(html))
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Next()
|
||||
log.Debug().Msgf("%s - \"%s %s %s %d\"", c.ClientIP(),
|
||||
c.Request.Method, c.Request.URL.Path, c.Request.Proto, c.Writer.Status())
|
||||
})
|
||||
|
||||
if cfg.AllowOrigins {
|
||||
|
||||
@@ -91,6 +91,10 @@ type Config struct {
|
||||
// Host[:port] used to generate device remote access address:
|
||||
// <deviceId>.<DEVICE_ENDPOINT_HOST>
|
||||
DeviceEndpointHost string
|
||||
|
||||
// Platform access domain restriction.
|
||||
// When set, only requests with a matching domain are allowed to access the platform.
|
||||
WebUIHost string
|
||||
}
|
||||
|
||||
// docker mode fixed path for reading certificate
|
||||
@@ -293,6 +297,23 @@ func parseYamlCfg(cfg *Config, conf string) error {
|
||||
cfg.DeviceEndpointHost = cleaned
|
||||
}
|
||||
|
||||
if v := strings.TrimSpace(os.Getenv("WEB_UI_HOST")); v != "" {
|
||||
cleaned := v
|
||||
// 1. Remove scheme if present (http:// or https://)
|
||||
if idx := strings.Index(cleaned, "://"); idx != -1 {
|
||||
cleaned = cleaned[idx+3:]
|
||||
}
|
||||
|
||||
// 2. Remove path/query/fragment if present
|
||||
if idx := strings.IndexAny(cleaned, "/?#"); idx != -1 {
|
||||
cleaned = cleaned[:idx]
|
||||
}
|
||||
|
||||
// 3. Final trim
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
cfg.WebUIHost = cleaned
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,25 @@ REVERSE_PROXY_ENABLED=false
|
||||
# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect).
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
|
||||
|
||||
# =====================================================
|
||||
# Platform Access Domain Restriction
|
||||
# =====================================================
|
||||
# Restrict the domain used to access the GLKVM Cloud platform.
|
||||
#
|
||||
# When set, only requests with a matching domain are allowed to access
|
||||
# the Web UI and API. Requests using other domains will be rejected
|
||||
# as invalid access.
|
||||
#
|
||||
# Examples:
|
||||
# WEB_UI_HOST=www.example.com
|
||||
#
|
||||
# Notes:
|
||||
# - Do NOT include scheme (http:// or https://)
|
||||
# - Do NOT include path (/xxx)
|
||||
# - Leave empty to disable domain restriction (allow access via any domain)
|
||||
WEB_UI_HOST=
|
||||
|
||||
# GLKVM access IP seen by devices/users.
|
||||
# Leave empty to auto-detect at container start.
|
||||
GLKVM_ACCESS_IP=
|
||||
|
||||
@@ -45,6 +45,24 @@ REVERSE_PROXY_ENABLED=false
|
||||
# Leave empty to derive the host/port from X-Forwarded-* headers (auto-detect).
|
||||
DEVICE_ENDPOINT_HOST=
|
||||
|
||||
# =====================================================
|
||||
# Platform Access Domain Restriction
|
||||
# =====================================================
|
||||
# Restrict the domain used to access the GLKVM Cloud platform.
|
||||
#
|
||||
# When set, only requests with a matching domain are allowed to access
|
||||
# the Web UI and API. Requests using other domains will be rejected
|
||||
# as invalid access.
|
||||
#
|
||||
# Examples:
|
||||
# WEB_UI_HOST=www.example.com
|
||||
#
|
||||
# Notes:
|
||||
# - Do NOT include scheme (http:// or https://)
|
||||
# - Do NOT include path (/xxx)
|
||||
# - Leave empty to disable domain restriction (allow access via any domain)
|
||||
WEB_UI_HOST=
|
||||
|
||||
GLKVM access IP seen by devices/users.
|
||||
# Leave empty to auto-detect at container start.
|
||||
GLKVM_ACCESS_IP=
|
||||
|
||||
@@ -5,7 +5,6 @@ services:
|
||||
image: ${GLKVM_IMAGE:-glzhitong/glkvm-cloud:latest}
|
||||
container_name: glkvm_cloud
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
environment:
|
||||
# Preferred: set GLKVM_ACCESS_IP explicitly; if empty, entrypoint will auto-detect once.
|
||||
GLKVM_ACCESS_IP: ${GLKVM_ACCESS_IP:-}
|
||||
@@ -56,6 +55,8 @@ services:
|
||||
|
||||
# ---- Device Endpoint Host ----
|
||||
DEVICE_ENDPOINT_HOST: ${DEVICE_ENDPOINT_HOST:-}
|
||||
# ---- Web UI Host ----
|
||||
WEB_UI_HOST: ${WEB_UI_HOST:-}
|
||||
volumes:
|
||||
- ./templates/rttys.conf.template:/tpl/rttys.conf.tmpl:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
@@ -64,12 +65,15 @@ services:
|
||||
- ./database:/home/database:rw
|
||||
entrypoint: ["/bin/sh", "/docker-entrypoint.sh"]
|
||||
command: ["rttys"]
|
||||
ports:
|
||||
- "${RTTYS_WEBUI_PORT:-443}:${RTTYS_WEBUI_PORT:-443}"
|
||||
- "${RTTYS_HTTP_PROXY_PORT:-10443}:${RTTYS_HTTP_PROXY_PORT:-10443}"
|
||||
- "${RTTYS_DEVICE_PORT:-5912}:${RTTYS_DEVICE_PORT:-5912}"
|
||||
|
||||
coturn:
|
||||
image: ${COTURN_IMAGE:-coturn/coturn:edge-alpine}
|
||||
container_name: glkvm_coturn
|
||||
restart: always
|
||||
network_mode: "host"
|
||||
environment:
|
||||
# Same semantics as above: prefer explicit value, else auto-detect
|
||||
GLKVM_ACCESS_IP: ${GLKVM_ACCESS_IP:-}
|
||||
@@ -80,4 +84,7 @@ services:
|
||||
command: ["coturn"]
|
||||
volumes:
|
||||
- ./templates/turnserver.conf.template:/tpl/turnserver.conf.tmpl:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
- ./scripts/docker-entrypoint.sh:/docker-entrypoint.sh:ro
|
||||
ports:
|
||||
- "${TURN_PORT:-3478}:3478/tcp"
|
||||
- "${TURN_PORT:-3478}:3478/udp"
|
||||
@@ -25,284 +25,284 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"rttys/db"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"context"
|
||||
"encoding/json"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"rttys/db"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
|
||||
xlog "rttys/log"
|
||||
xlog "rttys/log"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/urfave/cli/v3"
|
||||
)
|
||||
|
||||
const RttysVersion = "5.2.0"
|
||||
const KVMCloudVersion = "v1.7.0"
|
||||
const KVMCloudVersion = "v1.9.0"
|
||||
|
||||
var (
|
||||
GitCommit = ""
|
||||
BuildTime = ""
|
||||
GitCommit = ""
|
||||
BuildTime = ""
|
||||
)
|
||||
|
||||
func main() {
|
||||
defaultLogPath := "/var/log/rttys.log"
|
||||
if runtime.GOOS == "windows" {
|
||||
defaultLogPath = "rttys.log"
|
||||
}
|
||||
defaultLogPath := "/var/log/rttys.log"
|
||||
if runtime.GOOS == "windows" {
|
||||
defaultLogPath = "rttys.log"
|
||||
}
|
||||
|
||||
cmd := &cli.Command{
|
||||
Name: "rttys",
|
||||
Usage: "The server side for rtty",
|
||||
Version: RttysVersion,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defaultLogPath,
|
||||
Usage: "log file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "info",
|
||||
Usage: "log level(debug, info, warn, error)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "conf",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config file to load",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-dev",
|
||||
Value: ":5912",
|
||||
Usage: "address to listen device",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-user",
|
||||
Value: ":5913",
|
||||
Usage: "address to listen user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-http-proxy",
|
||||
Usage: "address to listen for HTTP proxy (default auto)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-url",
|
||||
Usage: "url to redirect for HTTP proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-domain",
|
||||
Usage: "domain for HTTP proxy set cookie",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "token to use",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dev-hook-url",
|
||||
Usage: "called when the device is connected",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-hook-url",
|
||||
Usage: "called when user accesses /connect/:devid, /cmd/:devid, /web/, or /web2/ APIs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "local-auth",
|
||||
Value: true,
|
||||
Usage: "need auth for local",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "web management password",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "allow-origins",
|
||||
Usage: "allow all origins for cross-domain request",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ldap-enabled",
|
||||
Usage: "enable LDAP authentication",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-server",
|
||||
Usage: "LDAP server hostname or IP",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "ldap-port",
|
||||
Value: 389,
|
||||
Usage: "LDAP server port",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ldap-use-tls",
|
||||
Usage: "use TLS/SSL for LDAP connection",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-dn",
|
||||
Usage: "LDAP bind DN for service account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-password",
|
||||
Usage: "LDAP bind password for service account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-base-dn",
|
||||
Usage: "LDAP base DN for user searches",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-user-filter",
|
||||
Value: "(uid=%s)",
|
||||
Usage: "LDAP user filter",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-allowed-groups",
|
||||
Usage: "comma-separated list of allowed LDAP groups",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-allowed-users",
|
||||
Usage: "comma-separated list of allowed LDAP users",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pprof",
|
||||
Usage: "enable pprof and listen on specified address (e.g. localhost:6060)",
|
||||
},
|
||||
cmd := &cli.Command{
|
||||
Name: "rttys",
|
||||
Usage: "The server side for rtty",
|
||||
Version: RttysVersion,
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defaultLogPath,
|
||||
Usage: "log file path",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "log-level",
|
||||
Value: "info",
|
||||
Usage: "log level(debug, info, warn, error)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "conf",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "config file to load",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-dev",
|
||||
Value: ":5912",
|
||||
Usage: "address to listen device",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-user",
|
||||
Value: ":5913",
|
||||
Usage: "address to listen user",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "addr-http-proxy",
|
||||
Usage: "address to listen for HTTP proxy (default auto)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-url",
|
||||
Usage: "url to redirect for HTTP proxy",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "http-proxy-redir-domain",
|
||||
Usage: "domain for HTTP proxy set cookie",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "token to use",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "dev-hook-url",
|
||||
Usage: "called when the device is connected",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "user-hook-url",
|
||||
Usage: "called when user accesses /connect/:devid, /cmd/:devid, /web/, or /web2/ APIs",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "local-auth",
|
||||
Value: true,
|
||||
Usage: "need auth for local",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "password",
|
||||
Usage: "web management password",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "allow-origins",
|
||||
Usage: "allow all origins for cross-domain request",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ldap-enabled",
|
||||
Usage: "enable LDAP authentication",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-server",
|
||||
Usage: "LDAP server hostname or IP",
|
||||
},
|
||||
&cli.IntFlag{
|
||||
Name: "ldap-port",
|
||||
Value: 389,
|
||||
Usage: "LDAP server port",
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "ldap-use-tls",
|
||||
Usage: "use TLS/SSL for LDAP connection",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-dn",
|
||||
Usage: "LDAP bind DN for service account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-bind-password",
|
||||
Usage: "LDAP bind password for service account",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-base-dn",
|
||||
Usage: "LDAP base DN for user searches",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-user-filter",
|
||||
Value: "(uid=%s)",
|
||||
Usage: "LDAP user filter",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-allowed-groups",
|
||||
Usage: "comma-separated list of allowed LDAP groups",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "ldap-allowed-users",
|
||||
Usage: "comma-separated list of allowed LDAP users",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "pprof",
|
||||
Usage: "enable pprof and listen on specified address (e.g. localhost:6060)",
|
||||
},
|
||||
|
||||
// ---- OIDC Authentication (generic OIDC provider) ----
|
||||
&cli.BoolFlag{
|
||||
Name: "oidc-enabled",
|
||||
Usage: "enable OIDC authentication (OpenID Connect)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-id",
|
||||
Usage: "OIDC client ID (issued by the identity provider)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-secret",
|
||||
Usage: "OIDC client secret (read from OIDC_GENERIC_CLIENT_SECRET env by default)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-auth-url",
|
||||
Usage: "OIDC authorization endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-token-url",
|
||||
Usage: "OIDC token endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-redirect-url",
|
||||
Usage: "OIDC redirect/callback URL (must match one registered in IdP)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-scopes",
|
||||
Value: "openid profile email",
|
||||
Usage: "space-separated list of OIDC scopes",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-users",
|
||||
Usage: "optional email whitelist for OIDC logins (exact emails or @domain, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-subs",
|
||||
Usage: "optional subject (sub) whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-usernames",
|
||||
Usage: "optional username whitelist for OIDC logins (preferred_username/name, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-groups",
|
||||
Usage: "optional groups whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
// ---- OIDC Authentication (generic OIDC provider) ----
|
||||
&cli.BoolFlag{
|
||||
Name: "oidc-enabled",
|
||||
Usage: "enable OIDC authentication (OpenID Connect)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-id",
|
||||
Usage: "OIDC client ID (issued by the identity provider)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-client-secret",
|
||||
Usage: "OIDC client secret (read from OIDC_GENERIC_CLIENT_SECRET env by default)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-auth-url",
|
||||
Usage: "OIDC authorization endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-token-url",
|
||||
Usage: "OIDC token endpoint URL",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-redirect-url",
|
||||
Usage: "OIDC redirect/callback URL (must match one registered in IdP)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-scopes",
|
||||
Value: "openid profile email",
|
||||
Usage: "space-separated list of OIDC scopes",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-users",
|
||||
Usage: "optional email whitelist for OIDC logins (exact emails or @domain, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-subs",
|
||||
Usage: "optional subject (sub) whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-usernames",
|
||||
Usage: "optional username whitelist for OIDC logins (preferred_username/name, space/comma-separated)",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "oidc-generic-allowed-groups",
|
||||
Usage: "optional groups whitelist for OIDC logins (space/comma-separated)",
|
||||
},
|
||||
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "more detailed output",
|
||||
},
|
||||
},
|
||||
Action: cmdAction,
|
||||
}
|
||||
&cli.BoolFlag{
|
||||
Name: "verbose",
|
||||
Aliases: []string{"V"},
|
||||
Usage: "more detailed output",
|
||||
},
|
||||
},
|
||||
Action: cmdAction,
|
||||
}
|
||||
|
||||
err := cmd.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
err := cmd.Run(context.Background(), os.Args)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func cmdAction(c context.Context, cmd *cli.Command) error {
|
||||
defer logPanic()
|
||||
defer logPanic()
|
||||
|
||||
xlog.SetPath(cmd.String("log"))
|
||||
xlog.SetPath(cmd.String("log"))
|
||||
|
||||
switch cmd.String("log-level") {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
switch cmd.String("log-level") {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
|
||||
if cmd.Bool("verbose") {
|
||||
xlog.Verbose()
|
||||
}
|
||||
if cmd.Bool("verbose") {
|
||||
xlog.Verbose()
|
||||
}
|
||||
|
||||
log.Info().Msg("Go Version: " + runtime.Version())
|
||||
log.Info().Msgf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
log.Info().Msg("Go Version: " + runtime.Version())
|
||||
log.Info().Msgf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
log.Info().Msg("Rttys Version: " + RttysVersion)
|
||||
log.Info().Msg("Rttys Version: " + RttysVersion)
|
||||
|
||||
if GitCommit != "" {
|
||||
log.Info().Msg("Git Commit: " + GitCommit)
|
||||
}
|
||||
if GitCommit != "" {
|
||||
log.Info().Msg("Git Commit: " + GitCommit)
|
||||
}
|
||||
|
||||
if BuildTime != "" {
|
||||
log.Info().Msg("Build Time: " + BuildTime)
|
||||
}
|
||||
if BuildTime != "" {
|
||||
log.Info().Msg("Build Time: " + BuildTime)
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
go signalHandle()
|
||||
}
|
||||
if runtime.GOOS != "windows" {
|
||||
go signalHandle()
|
||||
}
|
||||
|
||||
cfg := Config{
|
||||
AddrDev: ":5912",
|
||||
AddrUser: ":5913",
|
||||
LocalAuth: true,
|
||||
}
|
||||
cfg := Config{
|
||||
AddrDev: ":5912",
|
||||
AddrUser: ":5913",
|
||||
LocalAuth: true,
|
||||
}
|
||||
|
||||
err := cfg.Parse(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := cfg.Parse(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ===== 打印完整配置(验证配置是否加载正确) =====
|
||||
{
|
||||
importJSON, _ := json.MarshalIndent(cfg, "", " ")
|
||||
log.Info().Msg("==== Loaded Configuration ====")
|
||||
log.Info().Msg(string(importJSON))
|
||||
log.Info().Msg("==============================")
|
||||
}
|
||||
// ===== 打印完整配置(验证配置是否加载正确) =====
|
||||
{
|
||||
importJSON, _ := json.MarshalIndent(cfg, "", " ")
|
||||
log.Info().Msg("==== Loaded Configuration ====")
|
||||
log.Info().Msg(string(importJSON))
|
||||
log.Info().Msg("==============================")
|
||||
}
|
||||
|
||||
// Initialize the SQLite database connection
|
||||
db.Init()
|
||||
// Initialize the SQLite database connection
|
||||
db.Init()
|
||||
|
||||
srv := &RttyServer{cfg: cfg}
|
||||
srv := &RttyServer{cfg: cfg}
|
||||
|
||||
return srv.Run()
|
||||
return srv.Run()
|
||||
}
|
||||
|
||||
func logPanic() {
|
||||
if r := recover(); r != nil {
|
||||
saveCrashLog(r, debug.Stack())
|
||||
os.Exit(2)
|
||||
}
|
||||
if r := recover(); r != nil {
|
||||
saveCrashLog(r, debug.Stack())
|
||||
os.Exit(2)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCrashLog(p any, stack []byte) {
|
||||
log.Error().Msgf("%v", p)
|
||||
log.Error().Msg(string(stack))
|
||||
log.Error().Msgf("%v", p)
|
||||
log.Error().Msg(string(stack))
|
||||
}
|
||||
|
||||
Executable
+232
@@ -0,0 +1,232 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type HostInfo struct {
|
||||
Host string // pure host without port
|
||||
Port string // external port if known
|
||||
Scheme string // http/https
|
||||
RawHost string // req.Host (may include port)
|
||||
XFHost string // X-Forwarded-Host (raw)
|
||||
XFProto string // X-Forwarded-Proto (raw)
|
||||
XFPort string // X-Forwarded-Port (raw)
|
||||
}
|
||||
|
||||
func getHostInfoFromRequest(req *http.Request) HostInfo {
|
||||
hi := HostInfo{
|
||||
RawHost: req.Host,
|
||||
XFHost: req.Header.Get("X-Forwarded-Host"),
|
||||
XFProto: req.Header.Get("X-Forwarded-Proto"),
|
||||
XFPort: req.Header.Get("X-Forwarded-Port"),
|
||||
}
|
||||
|
||||
// host: prefer X-Forwarded-Host
|
||||
host := strings.TrimSpace(hi.XFHost)
|
||||
if host != "" {
|
||||
host = strings.TrimSpace(strings.Split(host, ",")[0])
|
||||
} else {
|
||||
host = strings.TrimSpace(req.Host)
|
||||
}
|
||||
|
||||
// split port if host contains it
|
||||
if h, p, err := net.SplitHostPort(host); err == nil {
|
||||
hi.Host = h
|
||||
hi.Port = p
|
||||
} else {
|
||||
hi.Host = strings.TrimSuffix(host, ".")
|
||||
}
|
||||
|
||||
// scheme
|
||||
proto := strings.TrimSpace(hi.XFProto)
|
||||
if proto != "" {
|
||||
proto = strings.ToLower(strings.TrimSpace(strings.Split(proto, ",")[0]))
|
||||
hi.Scheme = proto
|
||||
} else if req.TLS != nil {
|
||||
hi.Scheme = "https"
|
||||
} else {
|
||||
hi.Scheme = "http"
|
||||
}
|
||||
|
||||
// forwarded port overrides
|
||||
fp := strings.TrimSpace(hi.XFPort)
|
||||
if fp != "" {
|
||||
hi.Port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
}
|
||||
|
||||
return hi
|
||||
}
|
||||
|
||||
// isIPHost checks whether host is an IP address.
|
||||
func isIPHost(host string) bool {
|
||||
ip := net.ParseIP(strings.TrimSpace(host))
|
||||
return ip != nil
|
||||
}
|
||||
|
||||
// domainAllowed checks whether host is allowed.
|
||||
// Allow:
|
||||
// - exact match: base
|
||||
// - subdomain: *.base
|
||||
func domainAllowed(host, base string) bool {
|
||||
host = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), "."))
|
||||
base = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(base), "."))
|
||||
|
||||
if host == "" || base == "" {
|
||||
return false
|
||||
}
|
||||
if host == base {
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(host, "."+base)
|
||||
}
|
||||
|
||||
// buildRedirectHost removes the first label of the hostname and prepends devid.
|
||||
// Rules:
|
||||
// - "www.example.com" -> "devid.example.com"
|
||||
// - "www.l1.example.com" -> "devid.l1.example.com"
|
||||
// - "www.l1.l2.example.com" -> "devid.l1.l2.example.com"
|
||||
// - Two-level domain "example.com" -> "devid.example.com"
|
||||
// - Single label / abnormal cases -> "devid." + hostname (fallback)
|
||||
//
|
||||
// The input hostname must be a pure hostname without port.
|
||||
func buildRedirectHost(hostname, devid string) string {
|
||||
// Allow FQDN with trailing dot like "example.com."
|
||||
hostname = strings.TrimSuffix(hostname, ".")
|
||||
|
||||
// Split into labels
|
||||
labels := strings.Split(hostname, ".")
|
||||
// Remove empty labels (in case of consecutive dots)
|
||||
compact := make([]string, 0, len(labels))
|
||||
for _, l := range labels {
|
||||
if l != "" {
|
||||
compact = append(compact, l)
|
||||
}
|
||||
}
|
||||
labels = compact
|
||||
|
||||
switch len(labels) {
|
||||
case 0:
|
||||
return devid // extreme case: just return devid
|
||||
case 1:
|
||||
// Single label (e.g., "localhost") — keep original as suffix
|
||||
return devid + "." + labels[0]
|
||||
default:
|
||||
// >=2: drop the leftmost label
|
||||
suffix := strings.Join(labels[1:], ".")
|
||||
return devid + "." + suffix
|
||||
}
|
||||
}
|
||||
|
||||
func joinHostPortIfNeeded(host, scheme, port string) string {
|
||||
if port == "" {
|
||||
return host
|
||||
}
|
||||
// avoid adding default ports
|
||||
if (scheme == "https" && port == "443") || (scheme == "http" && port == "80") {
|
||||
return host
|
||||
}
|
||||
return net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
func buildRedirectLocation(scheme, hostPort, path, sid string) string {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
u := &url.URL{
|
||||
Scheme: scheme,
|
||||
Host: hostPort,
|
||||
Path: path,
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("sid", sid)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String()
|
||||
}
|
||||
|
||||
// getRequestHostInfo extracts domain(host), port and scheme(proto) from request headers.
|
||||
// Priority:
|
||||
// 1) X-Forwarded-Host / X-Forwarded-Proto / X-Forwarded-Port (reverse proxy)
|
||||
// 2) Host header / TLS info
|
||||
func getRequestHostInfo(req *http.Request) (host string, port string, proto string) {
|
||||
// 1) Reverse-proxy headers
|
||||
xfh := strings.TrimSpace(req.Header.Get("X-Forwarded-Host"))
|
||||
xfp := strings.TrimSpace(req.Header.Get("X-Forwarded-Proto"))
|
||||
xfport := strings.TrimSpace(req.Header.Get("X-Forwarded-Port"))
|
||||
|
||||
// X-Forwarded-Host may contain a comma-separated list. Take the first one.
|
||||
if xfh != "" {
|
||||
if i := strings.IndexByte(xfh, ','); i >= 0 {
|
||||
xfh = strings.TrimSpace(xfh[:i])
|
||||
}
|
||||
host = xfh
|
||||
}
|
||||
|
||||
// 2) Fallback to Host header
|
||||
if host == "" {
|
||||
host = strings.TrimSpace(req.Host)
|
||||
}
|
||||
|
||||
// Split host:port if present
|
||||
if h, p, err := net.SplitHostPort(host); err == nil {
|
||||
host = h
|
||||
port = p
|
||||
} else {
|
||||
// no explicit port in Host header
|
||||
port = ""
|
||||
}
|
||||
|
||||
// scheme/proto
|
||||
if xfp != "" {
|
||||
if i := strings.IndexByte(xfp, ','); i >= 0 {
|
||||
xfp = strings.TrimSpace(xfp[:i])
|
||||
}
|
||||
proto = xfp
|
||||
} else if req.TLS != nil {
|
||||
proto = "https"
|
||||
} else {
|
||||
proto = "http"
|
||||
}
|
||||
|
||||
// forwarded port overrides parsed port if present
|
||||
if xfport != "" {
|
||||
if i := strings.IndexByte(xfport, ','); i >= 0 {
|
||||
xfport = strings.TrimSpace(xfport[:i])
|
||||
}
|
||||
port = xfport
|
||||
}
|
||||
|
||||
return host, port, proto
|
||||
}
|
||||
|
||||
// extractDeviceIDFromHost extracts deviceId from hostname.
|
||||
// Rules:
|
||||
// - IP address -> ("", false)
|
||||
// - lv99862.example.com -> ("lv99862", true)
|
||||
// - lv99862.l1.example.com -> ("lv99862", true)
|
||||
// - localhost / single label -> ("localhost", true)
|
||||
func extractDeviceIDFromHost(host string) (string, bool) {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// remove trailing dot
|
||||
host = strings.TrimSuffix(host, ".")
|
||||
|
||||
// If host is IP, skip
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
labels := strings.Split(host, ".")
|
||||
for _, l := range labels {
|
||||
if l != "" {
|
||||
return l, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
Reference in New Issue
Block a user