From 245f8770d1815ef2adc14b0ce7c7bd22c13b3873 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Wed, 8 Sep 2021 14:33:50 +0300 Subject: [PATCH] HTTP authorization --- auth.go | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ doc/index.texi | 19 +++------ trip.go | 39 ++++++++++++++++++- 3 files changed, 148 insertions(+), 14 deletions(-) create mode 100644 auth.go diff --git a/auth.go b/auth.go new file mode 100644 index 0000000..62155d8 --- /dev/null +++ b/auth.go @@ -0,0 +1,104 @@ +/* +tofuproxy -- HTTP proxy with TLS certificates management +Copyright (C) 2021 Sergey Matveev + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +package tofuproxy + +import ( + "bytes" + "errors" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "sync" +) + +var ( + authCache = make(map[string][2]string) + authCacheM sync.Mutex +) + +func findInNetrc(host string) (string, string) { + netrcPath, ok := os.LookupEnv("NETRC") + if !ok { + netrcPath = filepath.Join(os.Getenv("HOME"), ".netrc") + } + data, err := ioutil.ReadFile(netrcPath) + if err != nil { + if os.IsNotExist(err) { + return "", "" + } + log.Fatalln(err) + } + var login string + var password string + for _, line := range strings.Split(string(data), "\n") { + if i := strings.Index(line, "#"); i >= 0 { + line = line[:i] + } + f := strings.Fields(line) + if len(f) >= 6 && + f[0] == "machine" && f[1] == host && + f[2] == "login" && f[4] == "password" { + login, password = f[3], f[5] + break + } + } + return login, password +} + +func authDialog(host, realm string) (string, string, error) { + var b bytes.Buffer + userInit, passInit := findInNetrc(host) + b.WriteString(fmt.Sprintf(` +wm title . "Unauthorized: %s" + +label .luser -text "User" +set userinit "%s" +set u [entry .user -textvariable userinit] +grid .luser .user + +label .lpass -text "Password" +set passinit "%s" +set p [entry .pass -show "*" -textvariable passinit] +grid .lpass .pass + +proc submit {} { + global u p + puts [$u get] + puts [$p get] + exit +} + +button .submit -text "Submit" -command submit +grid .submit +`, realm, userInit, passInit)) + cmd := exec.Command(CmdWish) + cmd.Stdin = &b + out, err := cmd.Output() + if err != nil { + return "", "", err + } + lines := strings.Split(string(out), "\n") + if len(lines) < 2 { + return "", "", errors.New("invalid output from authorization form") + } + return lines[0], lines[1], nil +} diff --git a/doc/index.texi b/doc/index.texi index 7759710..be8e76f 100644 --- a/doc/index.texi +++ b/doc/index.texi @@ -34,6 +34,8 @@ extensions for that. kind of @url{https://en.wikipedia.org/wiki/Privoxy, Privoxy}, but it is not friendly with TLS connections, obviously. +@item Xombrero sometimes has problems with HTTP-based authorization. + @item Hardly anyone does @url{https://en.wikipedia.org/wiki/DNS-based_Authentication_of_Named_Entities, DANE} checks. @@ -91,6 +93,10 @@ creating some kind of complex configuration framework. @item Even when native Go's checks are failed, you can still make a decision to forcefully trust the domain. +@item HTTP-based unauthorized responses are intercepted and + user/password input dialog is shown. It automatically loads initial + form values from @file{.netrc}. + @item Optionally DANE-EE check is also made for each domain you visit. @item TLS session resumption and keep-alives are also supported. @@ -101,16 +107,3 @@ creating some kind of complex configuration framework. @end itemize @include usage.texi - -@node TODO -@unnumbered TODO - -What I am planning possibly to do? Just brainstorming: - -@itemize - -@item HTTP authorization dialog. - -@item TLS client certificates usage capability. - -@end itemize diff --git a/trip.go b/trip.go index 3ad6eb1..fa34445 100644 --- a/trip.go +++ b/trip.go @@ -71,6 +71,9 @@ func roundTrip(w http.ResponseWriter, req *http.Request) { } } + reqFlags := []string{} + unauthorized := false +Retry: resp, err := transport.RoundTrip(req) if err != nil { fifos.SinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) @@ -78,6 +81,39 @@ func roundTrip(w http.ResponseWriter, req *http.Request) { return } + if resp.StatusCode == http.StatusUnauthorized { + resp.Body.Close() + authCacheM.Lock() + if unauthorized { + delete(authCache, req.URL.Host) + } else { + unauthorized = true + if creds, ok := authCache[req.URL.Host]; ok { + authCacheM.Unlock() + req.SetBasicAuth(creds[0], creds[1]) + goto Retry + } + } + fifos.SinkOther <- fmt.Sprintf("%s\tauthorization required", req.URL.Host) + user, pass, err := authDialog(host, resp.Header.Get("WWW-Authenticate")) + if err != nil { + authCacheM.Unlock() + fifos.SinkErr <- fmt.Sprintf("%s\t%s", req.URL.Host, err.Error()) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + authCache[req.URL.Host] = [2]string{user, pass} + authCacheM.Unlock() + req.SetBasicAuth(user, pass) + goto Retry + } + if unauthorized { + reqFlags = append(reqFlags, "auth") + } + if resp.TLS != nil && resp.TLS.NegotiatedProtocol != "" { + reqFlags = append(reqFlags, resp.TLS.NegotiatedProtocol) + } + for k, vs := range resp.Header { if _, ok := proxyHeaders[k]; ok { continue @@ -116,12 +152,13 @@ func roundTrip(w http.ResponseWriter, req *http.Request) { } resp.Body.Close() msg := fmt.Sprintf( - "%s %s\t%s\t%s\t%s", + "%s %s\t%s\t%s\t%s\t%s", req.Method, req.URL.String(), resp.Status, resp.Header.Get("Content-Type"), humanize.IBytes(uint64(n)), + strings.Join(reqFlags, ","), ) if resp.StatusCode == http.StatusOK { fifos.SinkOK <- msg -- 2.44.0