mirror of
https://github.com/gl-inet/glkvm-cloud.git
synced 2026-10-04 04:32:22 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdba252fa8 | |||
| 9faeac6ff9 |
@@ -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:-}
|
||||
@@ -66,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:-}
|
||||
@@ -82,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,36 +25,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"rttys/utils"
|
||||
"rttys/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
type HttpProxySession struct {
|
||||
expire atomic.Int64
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
devid string
|
||||
group string
|
||||
destaddr string
|
||||
https bool
|
||||
expire atomic.Int64
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
devid string
|
||||
group string
|
||||
destaddr string
|
||||
https bool
|
||||
}
|
||||
|
||||
var httpProxySessions = sync.Map{}
|
||||
@@ -62,503 +62,500 @@ var httpProxySessions = sync.Map{}
|
||||
const httpProxySessionsExpire = 15 * time.Minute
|
||||
|
||||
func (ses *HttpProxySession) Expire() {
|
||||
ses.expire.Store(time.Now().Add(httpProxySessionsExpire).Unix())
|
||||
ses.expire.Store(time.Now().Add(httpProxySessionsExpire).Unix())
|
||||
}
|
||||
|
||||
func (ses *HttpProxySession) String() string {
|
||||
return fmt.Sprintf("{devid: %s, group: %s, destaddr: %s, https: %v}",
|
||||
ses.devid, ses.group, ses.destaddr, ses.https)
|
||||
return fmt.Sprintf("{devid: %s, group: %s, destaddr: %s, https: %v}",
|
||||
ses.devid, ses.group, ses.destaddr, ses.https)
|
||||
}
|
||||
|
||||
func (srv *RttyServer) ListenHttpProxy() {
|
||||
cfg := &srv.cfg
|
||||
cfg := &srv.cfg
|
||||
|
||||
if cfg.AddrHttpProxy != "" {
|
||||
addr, err := net.ResolveTCPAddr("tcp", cfg.AddrHttpProxy)
|
||||
if err != nil {
|
||||
log.Warn().Msg("invalid http proxy addr: " + err.Error())
|
||||
} else {
|
||||
srv.httpProxyPort = addr.Port
|
||||
}
|
||||
}
|
||||
if cfg.AddrHttpProxy != "" {
|
||||
addr, err := net.ResolveTCPAddr("tcp", cfg.AddrHttpProxy)
|
||||
if err != nil {
|
||||
log.Warn().Msg("invalid http proxy addr: " + err.Error())
|
||||
} else {
|
||||
srv.httpProxyPort = addr.Port
|
||||
}
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", cfg.AddrHttpProxy)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
defer ln.Close()
|
||||
ln, err := net.Listen("tcp", cfg.AddrHttpProxy)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
// In reverse proxy mode (TLS terminated by nginx), never enable TLS here.
|
||||
enableTLS := !cfg.ReverseProxyEnabled && cfg.SslCert != "" && cfg.SslKey != ""
|
||||
if enableTLS {
|
||||
crt, err := tls.LoadX509KeyPair(cfg.SslCert, cfg.SslKey)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
// In reverse proxy mode (TLS terminated by nginx), never enable TLS here.
|
||||
enableTLS := !cfg.ReverseProxyEnabled && cfg.SslCert != "" && cfg.SslKey != ""
|
||||
if enableTLS {
|
||||
crt, err := tls.LoadX509KeyPair(cfg.SslCert, cfg.SslKey)
|
||||
if err != nil {
|
||||
log.Fatal().Msg(err.Error())
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{Certificates: []tls.Certificate{crt}}
|
||||
tlsConfig := &tls.Config{Certificates: []tls.Certificate{crt}}
|
||||
|
||||
ln = tls.NewListener(ln, tlsConfig)
|
||||
}
|
||||
ln = tls.NewListener(ln, tlsConfig)
|
||||
}
|
||||
|
||||
srv.httpProxyPort = ln.Addr().(*net.TCPAddr).Port
|
||||
srv.httpProxyPort = ln.Addr().(*net.TCPAddr).Port
|
||||
|
||||
log.Info().Msgf("Listen http proxy on: %s", ln.Addr().(*net.TCPAddr))
|
||||
log.Info().Msgf("Listen http proxy on: %s", ln.Addr().(*net.TCPAddr))
|
||||
|
||||
go httpProxySessionsClean()
|
||||
go httpProxySessionsClean()
|
||||
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Error().Msg(err.Error())
|
||||
continue
|
||||
}
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Error().Msg(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
go doHttpProxy(srv, c)
|
||||
}
|
||||
go doHttpProxy(srv, c)
|
||||
}
|
||||
}
|
||||
|
||||
func httpProxySessionsClean() {
|
||||
for {
|
||||
time.Sleep(time.Second * 30)
|
||||
for {
|
||||
time.Sleep(time.Second * 30)
|
||||
|
||||
httpProxySessions.Range(func(key, value any) bool {
|
||||
ses := value.(*HttpProxySession)
|
||||
if time.Now().Unix() > ses.expire.Load() {
|
||||
log.Debug().Msgf("Http proxy session '%s' expired", key)
|
||||
ses.cancel()
|
||||
httpProxySessions.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
httpProxySessions.Range(func(key, value any) bool {
|
||||
ses := value.(*HttpProxySession)
|
||||
if time.Now().Unix() > ses.expire.Load() {
|
||||
log.Debug().Msgf("Http proxy session '%s' expired", key)
|
||||
ses.cancel()
|
||||
httpProxySessions.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func doHttpProxy(srv *RttyServer, c net.Conn) {
|
||||
defer logPanic()
|
||||
defer c.Close()
|
||||
defer logPanic()
|
||||
defer c.Close()
|
||||
|
||||
br := bufio.NewReader(c)
|
||||
br := bufio.NewReader(c)
|
||||
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
domain, port, proto := getRequestHostInfo(req)
|
||||
log.Debug().Msgf("http proxy incoming host=%s port=%s proto=%s uri=%s",
|
||||
domain, port, proto, req.URL.String())
|
||||
devID, ok := extractDeviceIDFromHost(domain)
|
||||
if ok {
|
||||
log.Debug().Msgf("parsed deviceId from host: %s", devID)
|
||||
} else {
|
||||
log.Debug().Msgf("host is IP or invalid, skip deviceId parsing")
|
||||
}
|
||||
domain, port, proto := getRequestHostInfo(req)
|
||||
log.Debug().Msgf("http proxy incoming host=%s port=%s proto=%s uri=%s",
|
||||
domain, port, proto, req.URL.String())
|
||||
devID, ok := extractDeviceIDFromHost(domain)
|
||||
if ok {
|
||||
log.Debug().Msgf("parsed deviceId from host: %s", devID)
|
||||
} else {
|
||||
log.Debug().Msgf("host is IP or invalid, skip deviceId parsing")
|
||||
}
|
||||
|
||||
// 获取 URL 查询参数
|
||||
queryParams := req.URL.Query()
|
||||
name := queryParams.Get("sid")
|
||||
if name != "" {
|
||||
location := "/"
|
||||
location += fmt.Sprintf("?_=%d", time.Now().Unix())
|
||||
// 获取 URL 查询参数
|
||||
queryParams := req.URL.Query()
|
||||
name := queryParams.Get("sid")
|
||||
if name != "" {
|
||||
location := "/"
|
||||
|
||||
Write302WithCookie(c, location, "rtty-http-sid", name)
|
||||
return
|
||||
}
|
||||
Write302WithCookie(c, location, "rtty-http-sid", name)
|
||||
return
|
||||
}
|
||||
|
||||
cookie, err := req.Cookie("rtty-http-sid")
|
||||
if err != nil {
|
||||
log.Debug().Msgf(`not found cookie "rtty-http-sid"`)
|
||||
sendHTTPErrorResponse(c, "invalid")
|
||||
return
|
||||
}
|
||||
sid := cookie.Value
|
||||
cookie, err := req.Cookie("rtty-http-sid")
|
||||
if err != nil {
|
||||
log.Debug().Msgf(`not found cookie "rtty-http-sid"`)
|
||||
sendHTTPErrorResponse(c, "invalid")
|
||||
return
|
||||
}
|
||||
sid := cookie.Value
|
||||
|
||||
sesVal, ok := httpProxySessions.Load(sid)
|
||||
if !ok {
|
||||
log.Debug().Msgf(`not found httpProxySession "%s"`, sid)
|
||||
sendHTTPErrorResponse(c, "unauthorized")
|
||||
return
|
||||
}
|
||||
sesVal, ok := httpProxySessions.Load(sid)
|
||||
if !ok {
|
||||
log.Debug().Msgf(`not found httpProxySession "%s"`, sid)
|
||||
sendHTTPErrorResponse(c, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
ses := sesVal.(*HttpProxySession)
|
||||
ses := sesVal.(*HttpProxySession)
|
||||
|
||||
dev := srv.GetDevice(ses.group, ses.devid)
|
||||
if dev == nil {
|
||||
log.Debug().Msgf(`device "%s" group "%s" offline`, ses.devid, ses.group)
|
||||
sendHTTPErrorResponse(c, "offline")
|
||||
return
|
||||
}
|
||||
dev := srv.GetDevice(ses.group, ses.devid)
|
||||
if dev == nil {
|
||||
log.Debug().Msgf(`device "%s" group "%s" offline`, ses.devid, ses.group)
|
||||
sendHTTPErrorResponse(c, "offline")
|
||||
return
|
||||
}
|
||||
|
||||
// 3) match hostDevID vs session devid, and optionally lookup by hostDevID
|
||||
if devID != "" {
|
||||
match := devID == ses.devid
|
||||
log.Debug().Msgf(
|
||||
"http proxy devid check: hostDevID=%s sessionDevid=%s match=%v hostDevFound=%v sid=%s group=%s",
|
||||
devID, ses.devid, match, domain, sid, ses.group,
|
||||
)
|
||||
// 3) match hostDevID vs session devid, and optionally lookup by hostDevID
|
||||
if devID != "" {
|
||||
match := devID == ses.devid
|
||||
log.Debug().Msgf(
|
||||
"http proxy devid check: hostDevID=%s sessionDevid=%s match=%v hostDevFound=%v sid=%s group=%s",
|
||||
devID, ses.devid, match, domain, sid, ses.group,
|
||||
)
|
||||
|
||||
// If you want, you can also log when mismatch happens
|
||||
if !match {
|
||||
log.Info().Msgf(
|
||||
"http proxy devid mismatch: hostDevID=%s sessionDevid=%s sid=%s group=%s host=%s uri=%s",
|
||||
devID, ses.devid, sid, ses.group, domain, req.URL.String(),
|
||||
)
|
||||
sendHTTPErrorResponse(c, "invalid")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf(
|
||||
"http proxy devid check skipped: no hostDevID (host=%s) sid=%s group=%s sessionDevid=%s",
|
||||
domain, sid, ses.group, ses.devid,
|
||||
)
|
||||
}
|
||||
// If you want, you can also log when mismatch happens
|
||||
if !match {
|
||||
log.Info().Msgf(
|
||||
"http proxy devid mismatch: hostDevID=%s sessionDevid=%s sid=%s group=%s host=%s uri=%s",
|
||||
devID, ses.devid, sid, ses.group, domain, req.URL.String(),
|
||||
)
|
||||
sendHTTPErrorResponse(c, "invalid")
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf(
|
||||
"http proxy devid check skipped: no hostDevID (host=%s) sid=%s group=%s sessionDevid=%s",
|
||||
domain, sid, ses.group, ses.devid,
|
||||
)
|
||||
}
|
||||
|
||||
hostHeaderRewrite := ses.destaddr
|
||||
hostHeaderRewrite := ses.destaddr
|
||||
|
||||
destAddr := genDestAddr(hostHeaderRewrite)
|
||||
srcAddr := tcpAddr2Bytes(c.RemoteAddr().(*net.TCPAddr))
|
||||
destAddr := genDestAddr(hostHeaderRewrite)
|
||||
srcAddr := tcpAddr2Bytes(c.RemoteAddr().(*net.TCPAddr))
|
||||
|
||||
ctx, cancel := context.WithCancel(ses.ctx)
|
||||
defer cancel()
|
||||
ctx, cancel := context.WithCancel(ses.ctx)
|
||||
defer cancel()
|
||||
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.Close()
|
||||
log.Debug().Msgf("http proxy conn closed: %s", ses)
|
||||
dev.https.Delete(string(srcAddr))
|
||||
sendHttpReq(dev, ses.https, srcAddr[:], destAddr, nil)
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
c.Close()
|
||||
log.Debug().Msgf("http proxy conn closed: %s", ses)
|
||||
dev.https.Delete(string(srcAddr))
|
||||
sendHttpReq(dev, ses.https, srcAddr[:], destAddr, nil)
|
||||
}()
|
||||
|
||||
log.Debug().Msgf("new http proxy conn: %s", ses)
|
||||
log.Debug().Msgf("new http proxy conn: %s", ses)
|
||||
|
||||
dev.https.Store(string(srcAddr), c)
|
||||
dev.https.Store(string(srcAddr), c)
|
||||
|
||||
hpw := &HttpProxyWriter{destAddr, srcAddr, hostHeaderRewrite, dev, ses.https}
|
||||
hpw := &HttpProxyWriter{destAddr, srcAddr, hostHeaderRewrite, dev, ses.https}
|
||||
|
||||
req.Host = hostHeaderRewrite
|
||||
hpw.WriteRequest(req)
|
||||
req.Host = hostHeaderRewrite
|
||||
hpw.WriteRequest(req)
|
||||
|
||||
if req.Header.Get("Upgrade") == "websocket" {
|
||||
b := make([]byte, 4096)
|
||||
if req.Header.Get("Upgrade") == "websocket" {
|
||||
b := make([]byte, 4096)
|
||||
|
||||
for {
|
||||
n, err := c.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sendHttpReq(dev, ses.https, srcAddr, destAddr, b[:n])
|
||||
ses.Expire()
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hpw.WriteRequest(req)
|
||||
ses.Expire()
|
||||
}
|
||||
}
|
||||
for {
|
||||
n, err := c.Read(b)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
sendHttpReq(dev, ses.https, srcAddr, destAddr, b[:n])
|
||||
ses.Expire()
|
||||
}
|
||||
} else {
|
||||
for {
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
hpw.WriteRequest(req)
|
||||
ses.Expire()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func httpProxyRedirect(srv *RttyServer, c *gin.Context, group string) {
|
||||
cfg := &srv.cfg
|
||||
devid := c.Param("devid")
|
||||
proto := c.Param("proto")
|
||||
addr := c.Param("addr")
|
||||
rawPath := c.Param("path")
|
||||
log.Info().Msgf("httpProxyRedirect devid: %s, proto: %s, addr: %s, path: %s", devid, proto, addr, rawPath)
|
||||
cfg := &srv.cfg
|
||||
devid := c.Param("devid")
|
||||
proto := c.Param("proto")
|
||||
addr := c.Param("addr")
|
||||
rawPath := c.Param("path")
|
||||
log.Info().Msgf("httpProxyRedirect devid: %s, proto: %s, addr: %s, path: %s", devid, proto, addr, rawPath)
|
||||
|
||||
if !callUserHookUrl(cfg, c) {
|
||||
c.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if !callUserHookUrl(cfg, c) {
|
||||
c.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debug().Msgf("httpProxyRedirect devid: %s, proto: %s, addr: %s, path: %s", devid, proto, addr, rawPath)
|
||||
log.Debug().Msgf("httpProxyRedirect devid: %s, proto: %s, addr: %s, path: %s", devid, proto, addr, rawPath)
|
||||
|
||||
_, _, err := httpProxyVaildAddr(addr)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("invalid addr: %s", addr)
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_, _, err := httpProxyVaildAddr(addr)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("invalid addr: %s", addr)
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
path, err := url.Parse(rawPath)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("invalid path: %s", rawPath)
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
path, err := url.Parse(rawPath)
|
||||
if err != nil {
|
||||
log.Debug().Msgf("invalid path: %s", rawPath)
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
dev := srv.GetDevice(group, devid)
|
||||
if dev == nil {
|
||||
c.Redirect(http.StatusFound, "/error/offline")
|
||||
return
|
||||
}
|
||||
dev := srv.GetDevice(group, devid)
|
||||
if dev == nil {
|
||||
c.Redirect(http.StatusFound, "/error/offline")
|
||||
return
|
||||
}
|
||||
|
||||
location := c.Request.Header.Get("HttpProxyRedir")
|
||||
log.Info().Msgf("HttpProxyRedir location: %s, devid: %s", location, devid)
|
||||
if location == "" {
|
||||
location = cfg.HttpProxyRedirURL
|
||||
if location != "" {
|
||||
log.Debug().Msgf("use HttpProxyRedirURL from config: %s, devid: %s", location, devid)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("use HttpProxyRedir from HTTP header: %s, devid: %s", location, devid)
|
||||
}
|
||||
location := c.Request.Header.Get("HttpProxyRedir")
|
||||
log.Info().Msgf("HttpProxyRedir location: %s, devid: %s", location, devid)
|
||||
if location == "" {
|
||||
location = cfg.HttpProxyRedirURL
|
||||
if location != "" {
|
||||
log.Debug().Msgf("use HttpProxyRedirURL from config: %s, devid: %s", location, devid)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("use HttpProxyRedir from HTTP header: %s, devid: %s", location, devid)
|
||||
}
|
||||
|
||||
if location == "" {
|
||||
host, _, err := net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
if location == "" {
|
||||
host, _, err := net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
|
||||
location = "http://" + host
|
||||
location = "http://" + host
|
||||
|
||||
if srv.httpProxyPort != 80 {
|
||||
location += fmt.Sprintf(":%d", srv.httpProxyPort)
|
||||
}
|
||||
}
|
||||
if srv.httpProxyPort != 80 {
|
||||
location += fmt.Sprintf(":%d", srv.httpProxyPort)
|
||||
}
|
||||
}
|
||||
|
||||
location += path.Path
|
||||
location += path.Path
|
||||
|
||||
location += fmt.Sprintf("?_=%d", time.Now().Unix())
|
||||
if path.RawQuery != "" {
|
||||
location += "&" + path.RawQuery
|
||||
}
|
||||
|
||||
if path.RawQuery != "" {
|
||||
location += "&" + path.RawQuery
|
||||
}
|
||||
sid, err := c.Cookie("rtty-http-sid")
|
||||
log.Info().Msgf("rtty-http-sid: %s", sid)
|
||||
if err == nil {
|
||||
if v, loaded := httpProxySessions.LoadAndDelete(sid); loaded {
|
||||
s := v.(*HttpProxySession)
|
||||
s.cancel()
|
||||
log.Debug().Msgf(`del old httpProxySession "%s" for device "%s"`, sid, devid)
|
||||
}
|
||||
}
|
||||
|
||||
sid, err := c.Cookie("rtty-http-sid")
|
||||
log.Info().Msgf("rtty-http-sid: %s", sid)
|
||||
if err == nil {
|
||||
if v, loaded := httpProxySessions.LoadAndDelete(sid); loaded {
|
||||
s := v.(*HttpProxySession)
|
||||
s.cancel()
|
||||
log.Debug().Msgf(`del old httpProxySession "%s" for device "%s"`, sid, devid)
|
||||
}
|
||||
}
|
||||
sid = utils.GenUniqueID()
|
||||
log.Info().Msgf("rtty-http-sid: %s", sid)
|
||||
ctx, cancel := context.WithCancel(dev.ctx)
|
||||
|
||||
sid = utils.GenUniqueID()
|
||||
log.Info().Msgf("rtty-http-sid: %s", sid)
|
||||
ctx, cancel := context.WithCancel(dev.ctx)
|
||||
ses := &HttpProxySession{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
devid: devid,
|
||||
group: group,
|
||||
destaddr: addr,
|
||||
https: proto == "https",
|
||||
}
|
||||
ses.Expire()
|
||||
httpProxySessions.Store(sid, ses)
|
||||
|
||||
ses := &HttpProxySession{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
devid: devid,
|
||||
group: group,
|
||||
destaddr: addr,
|
||||
https: proto == "https",
|
||||
}
|
||||
ses.Expire()
|
||||
httpProxySessions.Store(sid, ses)
|
||||
log.Debug().Msgf(`new httpProxySession "%s" for device "%s"`, sid, devid)
|
||||
|
||||
log.Debug().Msgf(`new httpProxySession "%s" for device "%s"`, sid, devid)
|
||||
domain := c.Request.Header.Get("HttpProxyRedirDomain")
|
||||
if domain == "" {
|
||||
domain = cfg.HttpProxyRedirDomain
|
||||
if domain != "" {
|
||||
log.Debug().Msgf("set cookie domain from config: %s, devid: %s", domain, devid)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("set cookie domain from HTTP header: %s, devid: %s", domain, devid)
|
||||
}
|
||||
|
||||
domain := c.Request.Header.Get("HttpProxyRedirDomain")
|
||||
if domain == "" {
|
||||
domain = cfg.HttpProxyRedirDomain
|
||||
if domain != "" {
|
||||
log.Debug().Msgf("set cookie domain from config: %s, devid: %s", domain, devid)
|
||||
}
|
||||
} else {
|
||||
log.Debug().Msgf("set cookie domain from HTTP header: %s, devid: %s", domain, devid)
|
||||
}
|
||||
// Get domain info
|
||||
host := c.Request.Host
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
hostname = host
|
||||
}
|
||||
log.Info().Msgf("hostname: %s", hostname)
|
||||
|
||||
// Get domain info
|
||||
host := c.Request.Host
|
||||
hostname, _, err := net.SplitHostPort(host)
|
||||
if err != nil {
|
||||
hostname = host
|
||||
}
|
||||
log.Info().Msgf("hostname: %s", hostname)
|
||||
ip := net.ParseIP(hostname)
|
||||
isIP := ip != nil
|
||||
if isIP {
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", hostname, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using IP redirect: %s", location)
|
||||
} else {
|
||||
redirHost := buildRedirectHost(hostname, devid)
|
||||
// Keep original behavior when NOT in reverse proxy mode
|
||||
if !cfg.ReverseProxyEnabled {
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using domain redirect: %s", location)
|
||||
} else {
|
||||
// ---- verify forwarded headers from reverse proxy ----
|
||||
rawHost := c.GetHeader("Host")
|
||||
xfHost := c.GetHeader("X-Forwarded-Host")
|
||||
xfProto := c.GetHeader("X-Forwarded-Proto")
|
||||
xfPort := c.GetHeader("X-Forwarded-Port")
|
||||
xRealIP := c.GetHeader("X-Real-IP")
|
||||
xFF := c.GetHeader("X-Forwarded-For")
|
||||
|
||||
ip := net.ParseIP(hostname)
|
||||
isIP := ip != nil
|
||||
if isIP {
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", hostname, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using IP redirect: %s", location)
|
||||
} else {
|
||||
redirHost := buildRedirectHost(hostname, devid)
|
||||
// Keep original behavior when NOT in reverse proxy mode
|
||||
if !cfg.ReverseProxyEnabled {
|
||||
location = fmt.Sprintf("https://%s%s?sid=%s", redirHost, cfg.AddrHttpProxy, sid)
|
||||
log.Info().Msgf("Using domain redirect: %s", location)
|
||||
} else {
|
||||
// ---- verify forwarded headers from reverse proxy ----
|
||||
rawHost := c.GetHeader("Host")
|
||||
xfHost := c.GetHeader("X-Forwarded-Host")
|
||||
xfProto := c.GetHeader("X-Forwarded-Proto")
|
||||
xfPort := c.GetHeader("X-Forwarded-Port")
|
||||
xRealIP := c.GetHeader("X-Real-IP")
|
||||
xFF := c.GetHeader("X-Forwarded-For")
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy info: method=%s uri=%s host=%q tls=%v remoteIP=%q",
|
||||
c.Request.Method,
|
||||
c.Request.URL.String(),
|
||||
rawHost,
|
||||
c.Request.TLS != nil,
|
||||
c.ClientIP(),
|
||||
)
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy headers: Host=%q X-Forwarded-Host=%q X-Forwarded-Proto=%q X-Forwarded-Port=%q X-Real-IP=%q X-Forwarded-For=%q",
|
||||
rawHost, xfHost, xfProto, xfPort, xRealIP, xFF,
|
||||
)
|
||||
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy info: method=%s uri=%s host=%q tls=%v remoteIP=%q",
|
||||
c.Request.Method,
|
||||
c.Request.URL.String(),
|
||||
rawHost,
|
||||
c.Request.TLS != nil,
|
||||
c.ClientIP(),
|
||||
)
|
||||
log.Info().Msgf(
|
||||
"reverse-proxy headers: Host=%q X-Forwarded-Host=%q X-Forwarded-Proto=%q X-Forwarded-Port=%q X-Real-IP=%q X-Forwarded-For=%q",
|
||||
rawHost, xfHost, xfProto, xfPort, xRealIP, xFF,
|
||||
)
|
||||
// -------------------------------------------------
|
||||
// Proxy mode:
|
||||
// 1) If DEVICE_ENDPOINT_HOST is configured, use it directly
|
||||
// 2) Otherwise, fallback to forwarded-header logic
|
||||
// -------------------------------------------------
|
||||
|
||||
// -------------------------------------------------
|
||||
// Proxy mode:
|
||||
// 1) If DEVICE_ENDPOINT_HOST is configured, use it directly
|
||||
// 2) Otherwise, fallback to forwarded-header logic
|
||||
// -------------------------------------------------
|
||||
// 0) scheme: follow reverse proxy
|
||||
scheme := ""
|
||||
if v := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); v != "" {
|
||||
scheme = strings.ToLower(strings.Split(v, ",")[0])
|
||||
} else if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
// 0) scheme: follow reverse proxy
|
||||
scheme := ""
|
||||
if v := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto")); v != "" {
|
||||
scheme = strings.ToLower(strings.Split(v, ",")[0])
|
||||
} else if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
// [A] Prefer explicit DEVICE_ENDPOINT_HOST if set
|
||||
if v := strings.TrimSpace(cfg.DeviceEndpointHost); v != "" {
|
||||
endpoint := v // already normalized when reading env: host[:port] only
|
||||
|
||||
// [A] Prefer explicit DEVICE_ENDPOINT_HOST if set
|
||||
if v := strings.TrimSpace(cfg.DeviceEndpointHost); v != "" {
|
||||
endpoint := v // already normalized when reading env: host[:port] only
|
||||
baseHost := endpoint
|
||||
port := ""
|
||||
if h, p, err := net.SplitHostPort(endpoint); err == nil {
|
||||
baseHost = h
|
||||
port = p
|
||||
}
|
||||
|
||||
baseHost := endpoint
|
||||
port := ""
|
||||
if h, p, err := net.SplitHostPort(endpoint); err == nil {
|
||||
baseHost = h
|
||||
port = p
|
||||
}
|
||||
// Build device host: <deviceId>.<baseHost>
|
||||
// NOTE: DEVICE_ENDPOINT_HOST is a base domain (host[:port]) for device access,
|
||||
baseHost = strings.TrimSuffix(strings.TrimSpace(baseHost), ".")
|
||||
deviceHost := devid
|
||||
if baseHost != "" {
|
||||
deviceHost = devid + "." + baseHost
|
||||
}
|
||||
|
||||
// Build device host: <deviceId>.<baseHost>
|
||||
// NOTE: DEVICE_ENDPOINT_HOST is a base domain (host[:port]) for device access,
|
||||
baseHost = strings.TrimSuffix(strings.TrimSpace(baseHost), ".")
|
||||
deviceHost := devid
|
||||
if baseHost != "" {
|
||||
deviceHost = devid + "." + baseHost
|
||||
}
|
||||
hostPort := joinHostPortIfNeeded(deviceHost, scheme, port)
|
||||
|
||||
hostPort := joinHostPortIfNeeded(deviceHost, scheme, port)
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode, DEVICE_ENDPOINT_HOST): %s", location)
|
||||
} else {
|
||||
// 1) external port: prefer the one user actually accessed
|
||||
port := ""
|
||||
if fp := strings.TrimSpace(c.GetHeader("X-Forwarded-Port")); fp != "" {
|
||||
port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
} else if fh := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); fh != "" {
|
||||
fh = strings.TrimSpace(strings.Split(fh, ",")[0])
|
||||
if _, p, err := net.SplitHostPort(fh); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
log.Info().Msgf("port: %s", port)
|
||||
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode, DEVICE_ENDPOINT_HOST): %s", location)
|
||||
} else {
|
||||
// 1) external port: prefer the one user actually accessed
|
||||
port := ""
|
||||
if fp := strings.TrimSpace(c.GetHeader("X-Forwarded-Port")); fp != "" {
|
||||
port = strings.TrimSpace(strings.Split(fp, ",")[0])
|
||||
} else if fh := strings.TrimSpace(c.GetHeader("X-Forwarded-Host")); fh != "" {
|
||||
fh = strings.TrimSpace(strings.Split(fh, ",")[0])
|
||||
if _, p, err := net.SplitHostPort(fh); err == nil && p != "" {
|
||||
port = p
|
||||
}
|
||||
}
|
||||
log.Info().Msgf("port: %s", port)
|
||||
// 3) Build host: in proxy mode redirect domain to be redirHost
|
||||
hostPort := joinHostPortIfNeeded(redirHost, scheme, port)
|
||||
|
||||
// 3) Build host: in proxy mode redirect domain to be redirHost
|
||||
hostPort := joinHostPortIfNeeded(redirHost, scheme, port)
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode): %s", location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redirectPath := c.Request.URL.Path
|
||||
location = buildRedirectLocation(scheme, hostPort, redirectPath, sid)
|
||||
log.Info().Msgf("Using domain redirect (proxy mode): %s", location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Info().Msgf("Final redirect location: %s", location)
|
||||
c.Redirect(http.StatusFound, location)
|
||||
log.Info().Msgf("Final redirect location: %s", location)
|
||||
c.Redirect(http.StatusFound, location)
|
||||
}
|
||||
|
||||
func sendHttpReq(dev *Device, https bool, srcAddr []byte, destAddr []byte, data []byte) {
|
||||
bb := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(bb)
|
||||
bb := bytebufferpool.Get()
|
||||
defer bytebufferpool.Put(bb)
|
||||
|
||||
if dev.proto > 3 {
|
||||
if https {
|
||||
bb.WriteByte(1)
|
||||
} else {
|
||||
bb.WriteByte(0)
|
||||
}
|
||||
}
|
||||
if dev.proto > 3 {
|
||||
if https {
|
||||
bb.WriteByte(1)
|
||||
} else {
|
||||
bb.WriteByte(0)
|
||||
}
|
||||
}
|
||||
|
||||
bb.Write(srcAddr)
|
||||
bb.Write(destAddr)
|
||||
bb.Write(data)
|
||||
bb.Write(srcAddr)
|
||||
bb.Write(destAddr)
|
||||
bb.Write(data)
|
||||
|
||||
dev.WriteMsg(msgTypeHttp, "", bb.Bytes())
|
||||
dev.WriteMsg(msgTypeHttp, "", bb.Bytes())
|
||||
}
|
||||
|
||||
func genDestAddr(addr string) []byte {
|
||||
destIP, destPort, err := httpProxyVaildAddr(addr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
destIP, destPort, err := httpProxyVaildAddr(addr)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
b := make([]byte, 6)
|
||||
copy(b, destIP)
|
||||
b := make([]byte, 6)
|
||||
copy(b, destIP)
|
||||
|
||||
binary.BigEndian.PutUint16(b[4:], destPort)
|
||||
binary.BigEndian.PutUint16(b[4:], destPort)
|
||||
|
||||
return b
|
||||
return b
|
||||
}
|
||||
|
||||
func tcpAddr2Bytes(addr *net.TCPAddr) []byte {
|
||||
b := make([]byte, 18)
|
||||
b := make([]byte, 18)
|
||||
|
||||
binary.BigEndian.PutUint16(b[:2], uint16(addr.Port))
|
||||
binary.BigEndian.PutUint16(b[:2], uint16(addr.Port))
|
||||
|
||||
copy(b[2:], addr.IP)
|
||||
copy(b[2:], addr.IP)
|
||||
|
||||
return b
|
||||
return b
|
||||
}
|
||||
|
||||
func httpProxyVaildAddr(addr string) (net.IP, uint16, error) {
|
||||
ips, ports, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
ips = addr
|
||||
ports = "80"
|
||||
}
|
||||
ips, ports, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
ips = addr
|
||||
ports = "80"
|
||||
}
|
||||
|
||||
ip := net.ParseIP(ips)
|
||||
if ip == nil {
|
||||
return nil, 0, errors.New("invalid IPv4 Addr")
|
||||
}
|
||||
ip := net.ParseIP(ips)
|
||||
if ip == nil {
|
||||
return nil, 0, errors.New("invalid IPv4 Addr")
|
||||
}
|
||||
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return nil, 0, errors.New("invalid IPv4 Addr")
|
||||
}
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return nil, 0, errors.New("invalid IPv4 Addr")
|
||||
}
|
||||
|
||||
port, _ := strconv.Atoi(ports)
|
||||
port, _ := strconv.Atoi(ports)
|
||||
|
||||
return ip, uint16(port), nil
|
||||
return ip, uint16(port), nil
|
||||
}
|
||||
|
||||
type HttpProxyWriter struct {
|
||||
destAddr []byte
|
||||
srcAddr []byte
|
||||
hostHeaderRewrite string
|
||||
dev *Device
|
||||
https bool
|
||||
destAddr []byte
|
||||
srcAddr []byte
|
||||
hostHeaderRewrite string
|
||||
dev *Device
|
||||
https bool
|
||||
}
|
||||
|
||||
func (rw *HttpProxyWriter) Write(p []byte) (n int, err error) {
|
||||
sendHttpReq(rw.dev, rw.https, rw.srcAddr, rw.destAddr, p)
|
||||
return len(p), nil
|
||||
sendHttpReq(rw.dev, rw.https, rw.srcAddr, rw.destAddr, p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (rw *HttpProxyWriter) WriteRequest(req *http.Request) {
|
||||
req.Host = rw.hostHeaderRewrite
|
||||
req.Write(rw)
|
||||
req.Host = rw.hostHeaderRewrite
|
||||
req.Write(rw)
|
||||
}
|
||||
|
||||
func generateErrorHTML(errorType string) string {
|
||||
return fmt.Sprintf(
|
||||
`<!DOCTYPE html>
|
||||
return fmt.Sprintf(
|
||||
`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
@@ -712,28 +709,28 @@ func generateErrorHTML(errorType string) string {
|
||||
}
|
||||
|
||||
func sendHTTPErrorResponse(conn net.Conn, errorType string) {
|
||||
htmlContent := generateErrorHTML(errorType)
|
||||
htmlContent := generateErrorHTML(errorType)
|
||||
|
||||
response := "HTTP/1.1 200 OK\r\n"
|
||||
response += "Content-Type: text/html; charset=utf-8\r\n"
|
||||
response += fmt.Sprintf("Content-Length: %d\r\n", len(htmlContent))
|
||||
response += "Connection: close\r\n"
|
||||
response += "\r\n"
|
||||
response += htmlContent
|
||||
response := "HTTP/1.1 200 OK\r\n"
|
||||
response += "Content-Type: text/html; charset=utf-8\r\n"
|
||||
response += fmt.Sprintf("Content-Length: %d\r\n", len(htmlContent))
|
||||
response += "Connection: close\r\n"
|
||||
response += "\r\n"
|
||||
response += htmlContent
|
||||
|
||||
conn.Write([]byte(response))
|
||||
conn.Write([]byte(response))
|
||||
}
|
||||
|
||||
func Write302WithCookie(conn net.Conn, location, cookieName, cookieValue string) {
|
||||
cookie := fmt.Sprintf("%s=%s; Path=/; HttpOnly", cookieName, cookieValue)
|
||||
response := fmt.Sprintf(
|
||||
"HTTP/1.1 302 Found\r\n"+
|
||||
"Location: %s\r\n"+
|
||||
"Set-Cookie: %s\r\n"+
|
||||
"Content-Length: 0\r\n"+
|
||||
"Connection: close\r\n"+
|
||||
"\r\n",
|
||||
location, cookie,
|
||||
)
|
||||
_, _ = conn.Write([]byte(response))
|
||||
cookie := fmt.Sprintf("%s=%s; Path=/; HttpOnly", cookieName, cookieValue)
|
||||
response := fmt.Sprintf(
|
||||
"HTTP/1.1 302 Found\r\n"+
|
||||
"Location: %s\r\n"+
|
||||
"Set-Cookie: %s\r\n"+
|
||||
"Content-Length: 0\r\n"+
|
||||
"Connection: close\r\n"+
|
||||
"\r\n",
|
||||
location, cookie,
|
||||
)
|
||||
_, _ = conn.Write([]byte(response))
|
||||
}
|
||||
|
||||
@@ -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.8.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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user