From 6c36d6d1c8994b9b6fc73841c550e069c7cee583 Mon Sep 17 00:00:00 2001 From: Jianhui Zhao Date: Mon, 14 Jul 2025 13:08:02 +0800 Subject: [PATCH] Add user hook URL support for API access validation - Forward all original HTTP headers plus custom rttys headers: - X-Rttys-Hook: true - X-Original-Method: original request method - X-Original-URL: original request URL - Hook must return HTTP 200 to allow API access. This enables external services to validate and control user API access by receiving the complete original request context through HTTP hooks. Signed-off-by: Jianhui Zhao --- api.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.go | 3 +++ main.go | 4 ++++ rttys.conf | 8 ++++++++ 4 files changed, 70 insertions(+) diff --git a/api.go b/api.go index 1f0823a..3dbb267 100644 --- a/api.go +++ b/api.go @@ -67,6 +67,11 @@ func (srv *RttyServer) ListenAPI() error { } authorized := r.Group("/", func(c *gin.Context) { + if !callUserHookUrl(cfg, c) { + c.AbortWithStatus(http.StatusForbidden) + return + } + if !cfg.LocalAuth && isLocalRequest(c) { return } @@ -284,6 +289,56 @@ func (srv *RttyServer) ListenAPI() error { return r.RunListener(ln) } +func callUserHookUrl(cfg *Config, c *gin.Context) bool { + if cfg.UserHookUrl == "" { + return true + } + + upath := c.Request.URL.RawPath + + // Create HTTP request with original headers + req, err := http.NewRequest("GET", cfg.UserHookUrl, nil) + if err != nil { + log.Error().Err(err).Msgf("create hook request for \"%s\" fail", upath) + return false + } + + // Copy all headers from original request + for key, values := range c.Request.Header { + lowerKey := strings.ToLower(key) + if lowerKey == "upgrade" || lowerKey == "connection" || lowerKey == "accept-encoding" { + continue + } + + for _, value := range values { + req.Header.Add(key, value) + } + } + + // Add custom headers for hook identification + req.Header.Set("X-Rttys-Hook", "true") + req.Header.Set("X-Original-Method", c.Request.Method) + req.Header.Set("X-Original-URL", c.Request.URL.String()) + + cli := &http.Client{ + Timeout: 3 * time.Second, + } + + resp, err := cli.Do(req) + if err != nil { + log.Error().Err(err).Msgf("call user hook url for \"%s\" fail", upath) + return false + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Error().Msgf("call user hook url for \"%s\", StatusCode: %d", upath, resp.StatusCode) + return false + } + + return true +} + func httpLogin(cfg *Config, password string) bool { return cfg.Password == "" || cfg.Password == password } diff --git a/config.go b/config.go index 3fda9c4..ace9aec 100644 --- a/config.go +++ b/config.go @@ -40,6 +40,7 @@ type Config struct { HttpProxyRedirDomain string Token string DevHookUrl string + UserHookUrl string LocalAuth bool Password string AllowOrigins bool @@ -60,6 +61,7 @@ func (cfg *Config) Parse(c *cli.Command) error { getFlagOpt(c, "http-proxy-redir-url", &cfg.HttpProxyRedirURL) getFlagOpt(c, "http-proxy-redir-domain", &cfg.HttpProxyRedirDomain) getFlagOpt(c, "dev-hook-url", &cfg.DevHookUrl) + getFlagOpt(c, "user-hook-url", &cfg.UserHookUrl) getFlagOpt(c, "local-auth", &cfg.LocalAuth) getFlagOpt(c, "token", &cfg.Token) getFlagOpt(c, "password", &cfg.Password) @@ -98,6 +100,7 @@ func parseYamlCfg(cfg *Config, conf string) error { getConfigOpt(yamlCfg, "token", &cfg.Token) getConfigOpt(yamlCfg, "dev-hook-url", &cfg.DevHookUrl) + getConfigOpt(yamlCfg, "user-hook-url", &cfg.UserHookUrl) getConfigOpt(yamlCfg, "local-auth", &cfg.LocalAuth) getConfigOpt(yamlCfg, "password", &cfg.Password) getConfigOpt(yamlCfg, "allow-origins", &cfg.AllowOrigins) diff --git a/main.go b/main.go index 1dd889f..35fa358 100644 --- a/main.go +++ b/main.go @@ -103,6 +103,10 @@ func main() { Name: "dev-hook-url", Usage: "called when the device is connected", }, + &cli.StringFlag{ + Name: "user-hook-url", + Usage: "called when the user accesses APIs", + }, &cli.BoolFlag{ Name: "local-auth", Value: true, diff --git a/rttys.conf b/rttys.conf index beb9dc9..5a22e84 100644 --- a/rttys.conf +++ b/rttys.conf @@ -17,6 +17,14 @@ # Return HTTP 200 to indicate that the device is allowed to connect. #dev-hook-url: http://127.0.0.1:8080/rttys-dev-hook +# This URL will be called when the user accesses APIs if this URL is configured. +# Rttys will pass all original headers, along with several additional specific headers: +# X-Rttys-Hook: true +# X-Original-Method: original request method +# X-Original-URL: original request URL +# Return HTTP 200 to indicate that the user is allowed to access the API. +#user-hook-url: http://127.0.0.1:8080/rttys-user-hook + # Local access does not require authentication #local-auth: false