summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--auth.go31
-rw-r--r--config.go49
-rw-r--r--example.conf.hcl (renamed from conf.example.hcl)6
-rw-r--r--example.htpasswd1
-rw-r--r--go.mod7
-rw-r--r--go.sum7
-rw-r--r--main.go28
-rw-r--r--server.go67
8 files changed, 173 insertions, 23 deletions
diff --git a/auth.go b/auth.go
new file mode 100644
index 0000000..53e8db0
--- /dev/null
+++ b/auth.go
@@ -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
+}
diff --git a/config.go b/config.go
index 6ad988f..e191632 100644
--- a/config.go
+++ b/config.go
@@ -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
diff --git a/go.mod b/go.mod
index efb5b72..3df47d4 100644
--- a/go.mod
+++ b/go.mod
@@ -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
)
diff --git a/go.sum b/go.sum
index 4c4ea60..4289916 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/main.go b/main.go
index 2b955fb..234e4c2 100644
--- a/main.go
+++ b/main.go
@@ -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.
diff --git a/server.go b/server.go
index 830a6e4..f971d45 100644
--- a/server.go
+++ b/server.go
@@ -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 {