diff options
| -rw-r--r-- | auth.go | 31 | ||||
| -rw-r--r-- | config.go | 49 | ||||
| -rw-r--r-- | example.conf.hcl (renamed from conf.example.hcl) | 6 | ||||
| -rw-r--r-- | example.htpasswd | 1 | ||||
| -rw-r--r-- | go.mod | 7 | ||||
| -rw-r--r-- | go.sum | 7 | ||||
| -rw-r--r-- | main.go | 28 | ||||
| -rw-r--r-- | server.go | 67 |
8 files changed, 173 insertions, 23 deletions
@@ -0,0 +1,31 @@ +package main + +import ( + "syscall" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/term" +) + +func ReadPasswordFromUserInput() (string, error) { + bs, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return "", err + } else { + return string(bs), nil + } +} + +func GeneratePasswordHash(password string) (string, error) { + bs, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } else { + return string(bs), nil + } +} + +func VerifyPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} @@ -30,9 +30,10 @@ type ACMEChallengeConfig struct { } type ProxyConfig struct { - Name string - From ProxyFromConfig - To ProxyToConfig + Name string + From ProxyFromConfig + To ProxyToConfig + BasicAuth *ProxyBasicAuthConfig } type ProxyFromConfig struct { @@ -45,6 +46,11 @@ type ProxyToConfig struct { Port int } +type ProxyBasicAuthConfig struct { + Realm string + CredentialFile string +} + type InternalHCLConfig struct { User string `hcl:"user,optional"` Servers []InternalHCLServerConfig `hcl:"server,block"` @@ -66,9 +72,10 @@ type InternalHCLACMEChallengeConfig struct { } type InternalHCLProxyConfig struct { - Name string `hcl:"name,label"` - From InternalHCLProxyFromConfig `hcl:"from,block"` - To InternalHCLProxyToConfig `hcl:"to,block"` + Name string `hcl:"name,label"` + From InternalHCLProxyFromConfig `hcl:"from,block"` + To InternalHCLProxyToConfig `hcl:"to,block"` + Auths []InternalHCLProxyAuthConfig `hcl:"auth,block"` } type InternalHCLProxyFromConfig struct { @@ -81,6 +88,12 @@ type InternalHCLProxyToConfig struct { Port int `hcl:"port"` } +type InternalHCLProxyAuthConfig struct { + Scheme string `hcl:"scheme,label"` + Realm string `hcl:"realm"` + CredentialFile string `hcl:"credential_file"` +} + func fromHCLConfigToConfig(hclConfig *InternalHCLConfig) *Config { servers := make([]ServerConfig, len(hclConfig.Servers)) for i, s := range hclConfig.Servers { @@ -92,6 +105,14 @@ func fromHCLConfigToConfig(hclConfig *InternalHCLConfig) *Config { } proxies := make([]ProxyConfig, len(s.Proxies)) for j, p := range s.Proxies { + var basicAuth *ProxyBasicAuthConfig + if len(p.Auths) != 0 { + auth := p.Auths[0] + basicAuth = &ProxyBasicAuthConfig{ + Realm: auth.Realm, + CredentialFile: auth.CredentialFile, + } + } proxies[j] = ProxyConfig{ Name: p.Name, From: ProxyFromConfig{ @@ -102,6 +123,7 @@ func fromHCLConfigToConfig(hclConfig *InternalHCLConfig) *Config { Host: p.To.Host, Port: p.To.Port, }, + BasicAuth: basicAuth, } } servers[i] = ServerConfig{ @@ -199,6 +221,21 @@ func LoadConfig(fileName string) (*Config, error) { if err != nil { return nil, fmt.Errorf("Invalid host or port: %s:%d", p.To.Host, p.To.Port) } + if 2 <= len(p.Auths) { + return nil, fmt.Errorf("Too many auth blocks found") + } + if len(p.Auths) == 1 { + auth := p.Auths[0] + if auth.Scheme != "basic" { + return nil, fmt.Errorf("Only basic auth is supported") + } + if auth.Realm == "" { + return nil, fmt.Errorf("realm is required") + } + if auth.CredentialFile == "" { + return nil, fmt.Errorf("credential_file is required") + } + } } } if redirectToHTTPS && !listenHTTPS { diff --git a/conf.example.hcl b/example.conf.hcl index dfe18f8..e6f9d51 100644 --- a/conf.example.hcl +++ b/example.conf.hcl @@ -20,6 +20,12 @@ server http { host = "127.0.0.1" port = 8002 } + auth basic { + realm = "basic auth b" + credential_file = "example.htpasswd" + # user: nsfisis + # password: password + } } proxy c { diff --git a/example.htpasswd b/example.htpasswd new file mode 100644 index 0000000..50ab87b --- /dev/null +++ b/example.htpasswd @@ -0,0 +1 @@ +nsfisis:$2a$10$WDhYzn/jj0MaTRtSvSzF2eexjw2.NZETAG3NA2gnBVfqfB4eM1ix6 @@ -2,7 +2,11 @@ module github.com/nsfisis/mioproxy go 1.20 -require github.com/hashicorp/hcl/v2 v2.18.0 +require ( + github.com/hashicorp/hcl/v2 v2.18.0 + golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 +) require ( github.com/agext/levenshtein v1.2.1 // indirect @@ -11,5 +15,6 @@ require ( github.com/google/go-cmp v0.3.1 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/zclconf/go-cty v1.13.0 // indirect + golang.org/x/sys v0.5.0 // indirect golang.org/x/text v0.11.0 // indirect ) @@ -18,5 +18,12 @@ github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZX github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/zclconf/go-cty v1.13.0 h1:It5dfKTTZHe9aeppbNOda3mN7Ag7sg6QkBNm6TkyFa0= github.com/zclconf/go-cty v1.13.0/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 h1:O8uGbHCqlTp2P6QJSLmCojM4mN6UemYv8K+dCnmHmu0= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= @@ -74,6 +74,23 @@ func downgradeToUser(uname string) error { } func main() { + // Generate password mode + if len(os.Args) == 3 && os.Args[1] == "-genpw" { + userName := os.Args[2] + fmt.Fprintf(os.Stderr, "Enter password for user %s: ", userName) + password, err := ReadPasswordFromUserInput() + fmt.Fprintln(os.Stderr) + if err != nil { + log.Fatalf("%s", err) + } + passwordHash, err := GeneratePasswordHash(password) + if err != nil { + log.Fatalf("%s", err) + } + fmt.Println(userName + ":" + passwordHash) + return + } + // Check mode if len(os.Args) == 3 && os.Args[1] == "-check" { configFileName := os.Args[2] @@ -118,7 +135,16 @@ func main() { if s.TLSKeyFile != "" { s.TLSKeyFile = filepath.Join(configFileDir, s.TLSKeyFile) } - servers = append(servers, NewServer(&s)) + for _, p := range s.Proxies { + if p.BasicAuth != nil { + p.BasicAuth.CredentialFile = filepath.Join(configFileDir, p.BasicAuth.CredentialFile) + } + } + server, err := NewServer(&s) + if err != nil { + log.Fatalf("Failed to create server: %s", err) + } + servers = append(servers, server) } // Downgrade to non-root user. @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httputil" "net/url" + "os" "strings" ) @@ -19,7 +20,7 @@ type rewriteRule struct { fromHost string fromPath string toUrl *url.URL - proxy *httputil.ReverseProxy + proxy http.Handler } func (r *rewriteRule) matches(host, path string) bool { @@ -33,29 +34,62 @@ func (r *rewriteRule) matches(host, path string) bool { return ret } -func newMultipleReverseProxyServer(ps []ProxyConfig) *multipleReverseProxyServer { +func basicAuthHandler(handler http.Handler, realm, username, passwordHash string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + inputUsername, inputPassword, ok := r.BasicAuth() + if !ok || inputUsername != username || !VerifyPassword(inputPassword, passwordHash) { + w.Header().Set( + "WWW-Authenticate", + fmt.Sprintf("Basic realm=\"%s\"", realm), + ) + http.Error(w, "401 unauthorized", http.StatusUnauthorized) + return + } + handler.ServeHTTP(w, r) + }) +} + +func newMultipleReverseProxyServer(ps []ProxyConfig) (*multipleReverseProxyServer, error) { var rules []rewriteRule for _, p := range ps { targetUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", p.To.Host, p.To.Port)) if err != nil { - // This setting should be validated when loading config. - panic(err) + return nil, err + } + var proxy http.Handler = &httputil.ReverseProxy{ + Rewrite: func(r *httputil.ProxyRequest) { + r.SetURL(targetUrl) + r.SetXForwarded() + }, + } + if p.BasicAuth != nil { + credentialFileContent, err := os.ReadFile(p.BasicAuth.CredentialFile) + if err != nil { + return nil, err + } + usernameAndPasswordHash := strings.Split(strings.TrimSuffix(string(credentialFileContent), "\n"), ":") + if len(usernameAndPasswordHash) != 2 { + return nil, fmt.Errorf("invalid credential file format") + } + username := usernameAndPasswordHash[0] + passwordHash := usernameAndPasswordHash[1] + proxy = basicAuthHandler( + proxy, + p.BasicAuth.Realm, + username, + passwordHash, + ) } rules = append(rules, rewriteRule{ fromHost: p.From.Host, fromPath: p.From.Path, toUrl: targetUrl, - proxy: &httputil.ReverseProxy{ - Rewrite: func(r *httputil.ProxyRequest) { - r.SetURL(targetUrl) - r.SetXForwarded() - }, - }, + proxy: proxy, }) } return &multipleReverseProxyServer{ rules: rules, - } + }, nil } func (s *multipleReverseProxyServer) tryServeHTTP( @@ -77,7 +111,7 @@ type Server struct { tlsEnabled bool } -func NewServer(cfg *ServerConfig) *Server { +func NewServer(cfg *ServerConfig) (*Server, error) { h := http.NewServeMux() if cfg.ACMEChallenge != nil { @@ -95,7 +129,10 @@ func NewServer(cfg *ServerConfig) *Server { http.Redirect(w, r, target.String(), http.StatusMovedPermanently) }) } else { - reverseProxyServer := newMultipleReverseProxyServer(cfg.Proxies) + reverseProxyServer, err := newMultipleReverseProxyServer(cfg.Proxies) + if err != nil { + return nil, err + } h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // r.Host may have ":port" part. hostWithoutPort, _, err := net.SplitHostPort(r.Host) @@ -114,7 +151,7 @@ func NewServer(cfg *ServerConfig) *Server { if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" { cert, err := tls.LoadX509KeyPair(cfg.TLSCertFile, cfg.TLSKeyFile) if err != nil { - panic(err) + return nil, err } tlsConfig = &tls.Config{ Certificates: []tls.Certificate{cert}, @@ -128,7 +165,7 @@ func NewServer(cfg *ServerConfig) *Server { Handler: h, TLSConfig: tlsConfig, }, - } + }, nil } func (s *Server) Label() string { |
