diff --git a/conf.go b/conf.go index de10266..a7c46c3 100644 --- a/conf.go +++ b/conf.go @@ -13,6 +13,9 @@ type server struct { Docroot string URLRules []rule EnableProxy bool + EnableAuth bool + PasswdFile string + Realm string Vhost []vhost } diff --git a/config_example.yaml b/config_example.yaml index 04644cf..e60dc1f 100644 --- a/config_example.yaml +++ b/config_example.yaml @@ -14,7 +14,10 @@ # default document root docroot: /srv/www - enableproxy: false + enableproxy: true + enableauth: true + passwdfile: ./passwdfile + realm: example.com # default host's url rule # urlrules: diff --git a/digest_passwd_file.go b/digest_passwd_file.go new file mode 100644 index 0000000..3f06c40 --- /dev/null +++ b/digest_passwd_file.go @@ -0,0 +1,90 @@ +package main + +import ( + "bufio" + "os" + "strings" + "sync" + "time" +) + +type digestPwFile struct { + path string + entry []pwEntry + mtime time.Time + mu *sync.Mutex +} + +type pwEntry struct { + user string + realm string + hashPw string +} + +func newDigestSecret(f string) (*digestPwFile, error) { + a := &digestPwFile{path: f, mu: new(sync.Mutex)} + if err := a.loadFile(); err != nil { + return nil, err + } + go a.tryReload() + return a, nil +} + +func (df *digestPwFile) tryReload() { + for { + time.Sleep(10 * time.Second) + fi, _ := os.Stat(df.path) + t1 := fi.ModTime() + if t1 != df.mtime { + df.loadFile() + } + } +} + +func (df *digestPwFile) loadFile() error { + df.mu.Lock() + defer df.mu.Unlock() + + fp, err := os.Open(df.path) + if err != nil { + return err + } + + defer fp.Close() + + entry := []pwEntry{} + + r := bufio.NewReader(fp) + for { + line, err := r.ReadString('\n') + + if err != nil { + break + } + + line1 := strings.Trim(line, " \r\n") + if line1 == "" || line1[0] == '#' { + continue + } + fields := strings.SplitN(line1, ":", 3) + entry = append(entry, pwEntry{fields[0], fields[1], fields[2]}) + } + + df.entry = entry + fi, _ := os.Stat(df.path) + df.mtime = fi.ModTime() + + return nil +} + +func (df *digestPwFile) getPw(user, realm string) string { + df.mu.Lock() + defer df.mu.Unlock() + + for i := range df.entry { + if df.entry[i].user == user && df.entry[i].realm == realm { + return df.entry[i].hashPw + } + } + return "" +} diff --git a/handler.go b/handler.go index 89da5d4..153b72f 100644 --- a/handler.go +++ b/handler.go @@ -2,6 +2,7 @@ package main import ( "fmt" + auth "github.com/abbot/go-http-auth" "io" "log" "net" @@ -15,6 +16,8 @@ import ( type handler struct { handler http.Handler enableProxy bool + enableAuth bool + authMethod *auth.DigestAuth localDomains []string } @@ -57,6 +60,14 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + if h.enableAuth { + u, _ := h.authMethod.CheckAuth(r) + if u == "" { + h.authMethod.RequireAuth(w, r) + return + } + } + if r.Method == http.MethodConnect { // CONNECT request h.handleCONNECT(w, r) diff --git a/passwdfile b/passwdfile new file mode 100644 index 0000000..c2323a2 --- /dev/null +++ b/passwdfile @@ -0,0 +1,10 @@ +# format +# user:realm:hashed_passwd +# +# hashed_passwd = MD5(user:realm:plain_passwd) + +# +# user "test", realm "example.com", password "test" +# MD5("test:example:test") = 3441b753b98a6dc702183c989e35970f +# the entry is +test:example.com:3441b753b98a6dc702183c989e35970f diff --git a/routers.go b/routers.go index 73cec13..43ea74e 100644 --- a/routers.go +++ b/routers.go @@ -3,6 +3,7 @@ package main import ( "crypto/tls" "fmt" + auth "github.com/abbot/go-http-auth" "github.com/fangdingjun/gofast" "github.com/gorilla/mux" "log" @@ -82,8 +83,23 @@ func initRouters(cfg conf) { hdlr := &handler{ handler: router, enableProxy: l.EnableProxy, + enableAuth: l.EnableAuth, localDomains: domains, } + + if l.EnableAuth { + if l.PasswdFile == "" { + log.Fatal("passwdfile required") + } + du, err := newDigestSecret(l.PasswdFile) + if err != nil { + log.Fatal(err) + } + digestAuth := auth.NewDigestAuthenticator(l.Realm, du.getPw) + digestAuth.Headers = auth.ProxyHeaders + hdlr.authMethod = digestAuth + } + if len(certs) > 0 { tlsconfig := &tls.Config{ Certificates: certs,