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