From e0f2f29307d6abd962c0bf9e60eb961cf48cebb7 Mon Sep 17 00:00:00 2001 From: Mickael Kerjean Date: Wed, 9 Nov 2022 01:40:55 +1100 Subject: [PATCH] feature (htpasswd): new authentication middleware --- go.mod | 7 +- go.sum | 18 +- server/common/response.go | 13 +- server/ctrl/session.go | 12 +- server/plugin/index.go | 1 + .../plugin/plg_authenticate_htpasswd/index.go | 94 ++++++ .../github.com/abbot/go-http-auth/.gitignore | 5 + vendor/github.com/abbot/go-http-auth/LICENSE | 178 +++++++++++ vendor/github.com/abbot/go-http-auth/Makefile | 12 + .../github.com/abbot/go-http-auth/README.md | 71 +++++ vendor/github.com/abbot/go-http-auth/auth.go | 110 +++++++ vendor/github.com/abbot/go-http-auth/basic.go | 161 ++++++++++ .../github.com/abbot/go-http-auth/digest.go | 288 ++++++++++++++++++ vendor/github.com/abbot/go-http-auth/go.mod | 5 + vendor/github.com/abbot/go-http-auth/go.sum | 8 + .../github.com/abbot/go-http-auth/md5crypt.go | 73 +++++ vendor/github.com/abbot/go-http-auth/misc.go | 146 +++++++++ .../abbot/go-http-auth/test.htdigest | 1 + .../abbot/go-http-auth/test.htpasswd | 4 + vendor/github.com/abbot/go-http-auth/users.go | 151 +++++++++ vendor/modules.txt | 13 +- 21 files changed, 1352 insertions(+), 19 deletions(-) create mode 100644 server/plugin/plg_authenticate_htpasswd/index.go create mode 100644 vendor/github.com/abbot/go-http-auth/.gitignore create mode 100644 vendor/github.com/abbot/go-http-auth/LICENSE create mode 100644 vendor/github.com/abbot/go-http-auth/Makefile create mode 100644 vendor/github.com/abbot/go-http-auth/README.md create mode 100644 vendor/github.com/abbot/go-http-auth/auth.go create mode 100644 vendor/github.com/abbot/go-http-auth/basic.go create mode 100644 vendor/github.com/abbot/go-http-auth/digest.go create mode 100644 vendor/github.com/abbot/go-http-auth/go.mod create mode 100644 vendor/github.com/abbot/go-http-auth/go.sum create mode 100644 vendor/github.com/abbot/go-http-auth/md5crypt.go create mode 100644 vendor/github.com/abbot/go-http-auth/misc.go create mode 100644 vendor/github.com/abbot/go-http-auth/test.htdigest create mode 100644 vendor/github.com/abbot/go-http-auth/test.htpasswd create mode 100644 vendor/github.com/abbot/go-http-auth/users.go diff --git a/go.mod b/go.mod index c8a85982..0db5a5f9 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,13 @@ module github.com/mickael-kerjean/filestash go 1.16 require ( + github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f github.com/aws/aws-sdk-go v1.40.41 github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect github.com/creack/pty v1.1.18 github.com/cretz/bine v0.1.0 github.com/go-sql-driver/mysql v1.5.0 - github.com/google/go-cmp v0.5.6 // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/gorilla/mux v1.7.3 github.com/gorilla/websocket v1.4.1 github.com/h2non/bimg v1.1.5 @@ -28,12 +29,14 @@ require ( github.com/tidwall/gjson v1.13.0 github.com/tidwall/sjson v1.0.4 github.com/wayneashleyberry/terminal-dimensions v1.1.0 // indirect - golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 + golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed golang.org/x/image v0.0.0-20210622092929-e6eecd499c2c + golang.org/x/mod v0.4.1 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 + golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 // indirect google.golang.org/api v0.15.0 gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect diff --git a/go.sum b/go.sum index acc55de6..8afb48d0 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0 h1:ROfEUZz+Gh5pa62DJWXSaonyu3StP6EA6lPEXPI6mCo= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f h1:R2ZVGCZzU95oXFJxncosHS9LsX8N4/MYUdGGWOb2cFk= +github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f/go.mod h1:l2P3JyHa+fjy5Bxol6y1u2o4DV/mv3QMBdBu2cNR53w= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= @@ -43,8 +45,8 @@ github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Z github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -155,8 +157,9 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 h1:/pEO3GD/ABYAjuakUS6xSEmmlyVS4kxBNkeA9tLJiTI= -golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= +golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/image v0.0.0-20210622092929-e6eecd499c2c h1:FRR4fGZm/CMwZka5baQ4z8c8StbxJOMjS/45e0BAxK0= golang.org/x/image v0.0.0-20210622092929-e6eecd499c2c/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= @@ -165,8 +168,9 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -177,6 +181,7 @@ golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -225,8 +230,9 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 h1:M8tBwCtWD/cZV9DZpFYRUgaymAYAr+aIUTWzDaM3uPs= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 h1:K+NlvTLy0oONtRtkl1jRD9xIhnItbG2PiE7YOdjPb+k= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server/common/response.go b/server/common/response.go index c730bd60..8190923f 100644 --- a/server/common/response.go +++ b/server/common/response.go @@ -133,17 +133,22 @@ func Page(stuff string) string { - + ` + stuff + ` + ` } diff --git a/server/ctrl/session.go b/server/ctrl/session.go index 6330f9f8..59e6662d 100644 --- a/server/ctrl/session.go +++ b/server/ctrl/session.go @@ -302,11 +302,17 @@ func SessionAuthMiddleware(ctx *App, res http.ResponseWriter, req *http.Request) // - target of a html form. eg: ldap, mysql, ... // - identity provider redirection uri. eg: oauth2, openid, ... templateBind, err := plugin.Callback(formData, idpParams, res) - if err != nil { + if err == ErrAuthenticationFailed { + http.Redirect( + res, req, + req.URL.Path+"?action=redirect", + http.StatusSeeOther, + ) + return + } else if err != nil { Log.Error("session::authMiddleware 'callback error - %s'", err.Error()) http.Redirect( - res, - req, + res, req, "/?error="+ErrNotAllowed.Error()+"&trace=redirect request failed - "+err.Error(), http.StatusSeeOther, ) diff --git a/server/plugin/index.go b/server/plugin/index.go index 2920f713..4f390bf4 100644 --- a/server/plugin/index.go +++ b/server/plugin/index.go @@ -3,6 +3,7 @@ package plugin import ( . "github.com/mickael-kerjean/filestash/server/common" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_admin" + _ "github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_htpasswd" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_ldap" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_openid" _ "github.com/mickael-kerjean/filestash/server/plugin/plg_authenticate_passthrough" diff --git a/server/plugin/plg_authenticate_htpasswd/index.go b/server/plugin/plg_authenticate_htpasswd/index.go new file mode 100644 index 00000000..92408112 --- /dev/null +++ b/server/plugin/plg_authenticate_htpasswd/index.go @@ -0,0 +1,94 @@ +package plg_authenticate_passthrough + +import ( + "fmt" + auth "github.com/abbot/go-http-auth" + . "github.com/mickael-kerjean/filestash/server/common" + "net/http" + "strings" +) + +func init() { + Hooks.Register.AuthenticationMiddleware("htpasswd", Htpasswd{}) +} + +type Htpasswd struct{} + +func (this Htpasswd) Setup() Form { + return Form{ + Elmnts: []FormElement{ + { + Name: "type", + Type: "hidden", + Value: "htpasswd", + }, + { + Name: "users", + Type: "long_text", + Placeholder: "eg:\nbob123:$apr1$FaPCZHMe$jYiw5.9UevKx25pBH4AsT/\nnancy456:$apr1$mrCHcVhc$oNdJeRcWKPk2z8dlzQI0x/", + Default: "", + Description: "The list of users that will be granted access using the htpasswd file format. This plugin exposes the following variables which you can use from the attribute mapping: {{ .user }}, {{ .password }}", + }, + }, + } +} + +func (this Htpasswd) EntryPoint(idpParams map[string]string, req *http.Request, res http.ResponseWriter) error { + getFlash := func() string { + c, err := req.Cookie("flash") + if err != nil { + return "" + } + http.SetCookie(res, &http.Cookie{ + Name: "flash", + MaxAge: -1, + Path: "/", + }) + return fmt.Sprintf(`

%s

`, c.Value) + } + res.Header().Set("Content-Type", "text/html; charset=utf-8") + res.WriteHeader(http.StatusOK) + res.Write([]byte(Page(` +
+ + + + ` + getFlash() + ` + +
`))) + return nil +} + +func (this Htpasswd) Callback(formData map[string]string, idpParams map[string]string, res http.ResponseWriter) (map[string]string, error) { + lines := strings.Split(idpParams["users"], "\n") + if len(lines) == 0 { + Log.Error("plg_authenticate_htpasswd::callback there is no user configured") + return nil, ErrAuthenticationFailed + } + + for _, line := range lines { + pair := strings.SplitN(line, ":", 2) + if len(pair) != 2 { + continue + } else if formData["user"] != pair[0] { + continue + } else if auth.CheckSecret(formData["password"], pair[1]) == false { + continue + } + return map[string]string{ + "user": formData["user"], + "password": formData["password"], + }, nil + } + http.SetCookie(res, &http.Cookie{ + Name: "flash", + Value: "Invalid username or password", + MaxAge: 1, + Path: "/", + }) + return nil, ErrAuthenticationFailed +} diff --git a/vendor/github.com/abbot/go-http-auth/.gitignore b/vendor/github.com/abbot/go-http-auth/.gitignore new file mode 100644 index 00000000..112ea395 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/.gitignore @@ -0,0 +1,5 @@ +*~ +*.a +*.6 +*.out +_testmain.go diff --git a/vendor/github.com/abbot/go-http-auth/LICENSE b/vendor/github.com/abbot/go-http-auth/LICENSE new file mode 100644 index 00000000..e454a525 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/LICENSE @@ -0,0 +1,178 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/vendor/github.com/abbot/go-http-auth/Makefile b/vendor/github.com/abbot/go-http-auth/Makefile new file mode 100644 index 00000000..25f208da --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/Makefile @@ -0,0 +1,12 @@ +include $(GOROOT)/src/Make.inc + +TARG=auth_digest +GOFILES=\ + auth.go\ + digest.go\ + basic.go\ + misc.go\ + md5crypt.go\ + users.go\ + +include $(GOROOT)/src/Make.pkg diff --git a/vendor/github.com/abbot/go-http-auth/README.md b/vendor/github.com/abbot/go-http-auth/README.md new file mode 100644 index 00000000..73ae9852 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/README.md @@ -0,0 +1,71 @@ +HTTP Authentication implementation in Go +======================================== + +This is an implementation of HTTP Basic and HTTP Digest authentication +in Go language. It is designed as a simple wrapper for +http.RequestHandler functions. + +Features +-------- + + * Supports HTTP Basic and HTTP Digest authentication. + * Supports htpasswd and htdigest formatted files. + * Automatic reloading of password files. + * Pluggable interface for user/password storage. + * Supports MD5, SHA1 and BCrypt for Basic authentication password storage. + * Configurable Digest nonce cache size with expiration. + * Wrapper for legacy http handlers (http.HandlerFunc interface) + +Example usage +------------- + +This is a complete working example for Basic auth: + + package main + + import ( + "fmt" + "net/http" + + auth "github.com/abbot/go-http-auth" + ) + + func Secret(user, realm string) string { + if user == "john" { + // password is "hello" + return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" + } + return "" + } + + func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) { + fmt.Fprintf(w, "

Hello, %s!

", r.Username) + } + + func main() { + authenticator := auth.NewBasicAuthenticator("example.com", Secret) + http.HandleFunc("/", authenticator.Wrap(handle)) + http.ListenAndServe(":8080", nil) + } + +See more examples in the "examples" directory. + +Legal +----- + +This module is developed under Apache 2.0 license, and can be used for +open and proprietary projects. + +Copyright 2012-2013 Lev Shamardin + +Licensed under the Apache License, Version 2.0 (the "License"); you +may not use this file or any other part of this project except in +compliance with the License. You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the License for the specific language governing +permissions and limitations under the License. diff --git a/vendor/github.com/abbot/go-http-auth/auth.go b/vendor/github.com/abbot/go-http-auth/auth.go new file mode 100644 index 00000000..bbe5a129 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/auth.go @@ -0,0 +1,110 @@ +// Package auth is an implementation of HTTP Basic and HTTP Digest authentication. +package auth + +import ( + "context" + "net/http" +) + +// AuthenticatedRequest is passed to AuthenticatedHandlerFunc instead +// of *http.Request. +type AuthenticatedRequest struct { + http.Request + // Username is the authenticated user name. Current API implies that + // Username is never empty, which means that authentication is + // always done before calling the request handler. + Username string +} + +// AuthenticatedHandlerFunc is like http.HandlerFunc, but takes +// AuthenticatedRequest instead of http.Request +type AuthenticatedHandlerFunc func(http.ResponseWriter, *AuthenticatedRequest) + +// Authenticator wraps an AuthenticatedHandlerFunc with +// authentication-checking code. +// +// Typical Authenticator usage is something like: +// +// authenticator := SomeAuthenticator(...) +// http.HandleFunc("/", authenticator(my_handler)) +// +// Authenticator wrapper checks the user authentication and calls the +// wrapped function only after authentication has succeeded. Otherwise, +// it returns a handler which initiates the authentication procedure. +type Authenticator func(AuthenticatedHandlerFunc) http.HandlerFunc + +// Info contains authentication information for the request. +type Info struct { + // Authenticated is set to true when request was authenticated + // successfully, i.e. username and password passed in request did + // pass the check. + Authenticated bool + + // Username contains a user name passed in the request when + // Authenticated is true. It's value is undefined if Authenticated + // is false. + Username string + + // ResponseHeaders contains extra headers that must be set by server + // when sending back HTTP response. + ResponseHeaders http.Header +} + +// UpdateHeaders updates headers with this Info's ResponseHeaders. It is +// safe to call this function on nil Info. +func (i *Info) UpdateHeaders(headers http.Header) { + if i == nil { + return + } + for k, values := range i.ResponseHeaders { + for _, v := range values { + headers.Add(k, v) + } + } +} + +type key int // used for context keys + +var infoKey key + +// AuthenticatorInterface is the interface implemented by BasicAuth +// and DigestAuth authenticators. +// +// Deprecated: this interface is not coherent. New code should define +// and use your own interfaces with a required subset of authenticator +// methods. +type AuthenticatorInterface interface { + // NewContext returns a new context carrying authentication + // information extracted from the request. + NewContext(ctx context.Context, r *http.Request) context.Context + + // Wrap returns an http.HandlerFunc which wraps + // AuthenticatedHandlerFunc with this authenticator's + // authentication checks. + Wrap(AuthenticatedHandlerFunc) http.HandlerFunc +} + +// FromContext returns authentication information from the context or +// nil if no such information present. +func FromContext(ctx context.Context) *Info { + info, ok := ctx.Value(infoKey).(*Info) + if !ok { + return nil + } + return info +} + +// AuthUsernameHeader is the header set by JustCheck functions. It +// contains an authenticated username (if authentication was +// successful). +const AuthUsernameHeader = "X-Authenticated-Username" + +// JustCheck returns a new http.HandlerFunc, which requires +// authenticator to successfully authenticate a user before calling +// wrapped http.HandlerFunc. +func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc { + return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { + ar.Header.Set(AuthUsernameHeader, ar.Username) + wrapped(w, &ar.Request) + }) +} diff --git a/vendor/github.com/abbot/go-http-auth/basic.go b/vendor/github.com/abbot/go-http-auth/basic.go new file mode 100644 index 00000000..f328a328 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/basic.go @@ -0,0 +1,161 @@ +package auth + +import ( + "bytes" + "context" + "crypto/sha1" + "crypto/subtle" + "encoding/base64" + "errors" + "net/http" + "strings" + + "golang.org/x/crypto/bcrypt" +) + +type compareFunc func(hashedPassword, password []byte) error + +var ( + errMismatchedHashAndPassword = errors.New("mismatched hash and password") + + compareFuncs = []struct { + prefix string + compare compareFunc + }{ + {"", compareMD5HashAndPassword}, // default compareFunc + {"{SHA}", compareShaHashAndPassword}, + // Bcrypt is complicated. According to crypt(3) from + // crypt_blowfish version 1.3 (fetched from + // http://www.openwall.com/crypt/crypt_blowfish-1.3.tar.gz), there + // are three different has prefixes: "$2a$", used by versions up + // to 1.0.4, and "$2x$" and "$2y$", used in all later + // versions. "$2a$" has a known bug, "$2x$" was added as a + // migration path for systems with "$2a$" prefix and still has a + // bug, and only "$2y$" should be used by modern systems. The bug + // has something to do with handling of 8-bit characters. Since + // both "$2a$" and "$2x$" are deprecated, we are handling them the + // same way as "$2y$", which will yield correct results for 7-bit + // character passwords, but is wrong for 8-bit character + // passwords. You have to upgrade to "$2y$" if you want sant 8-bit + // character password support with bcrypt. To add to the mess, + // OpenBSD 5.5. introduced "$2b$" prefix, which behaves exactly + // like "$2y$" according to the same source. + {"$2a$", bcrypt.CompareHashAndPassword}, + {"$2b$", bcrypt.CompareHashAndPassword}, + {"$2x$", bcrypt.CompareHashAndPassword}, + {"$2y$", bcrypt.CompareHashAndPassword}, + } +) + +// BasicAuth is an authenticator implementation for 'Basic' HTTP +// Authentication scheme (RFC 7617). +type BasicAuth struct { + Realm string + Secrets SecretProvider + // Headers used by authenticator. Set to ProxyHeaders to use with + // proxy server. When nil, NormalHeaders are used. + Headers *Headers +} + +// check that BasicAuth implements AuthenticatorInterface +var _ = (AuthenticatorInterface)((*BasicAuth)(nil)) + +// CheckAuth checks the username/password combination from the +// request. Returns either an empty string (authentication failed) or +// the name of the authenticated user. +func (a *BasicAuth) CheckAuth(r *http.Request) string { + user, password, ok := r.BasicAuth() + if !ok { + return "" + } + + secret := a.Secrets(user, a.Realm) + if secret == "" { + return "" + } + + if !CheckSecret(password, secret) { + return "" + } + + return user +} + +// CheckSecret returns true if the password matches the encrypted +// secret. +func CheckSecret(password, secret string) bool { + compare := compareFuncs[0].compare + for _, cmp := range compareFuncs[1:] { + if strings.HasPrefix(secret, cmp.prefix) { + compare = cmp.compare + break + } + } + return compare([]byte(secret), []byte(password)) == nil +} + +func compareShaHashAndPassword(hashedPassword, password []byte) error { + d := sha1.New() + d.Write(password) + if subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil)))) != 1 { + return errMismatchedHashAndPassword + } + return nil +} + +func compareMD5HashAndPassword(hashedPassword, password []byte) error { + parts := bytes.SplitN(hashedPassword, []byte("$"), 4) + if len(parts) != 4 { + return errMismatchedHashAndPassword + } + magic := []byte("$" + string(parts[1]) + "$") + salt := parts[2] + if subtle.ConstantTimeCompare(hashedPassword, MD5Crypt(password, salt, magic)) != 1 { + return errMismatchedHashAndPassword + } + return nil +} + +// RequireAuth is an http.HandlerFunc for BasicAuth which initiates +// the authentication process (or requires reauthentication). +func (a *BasicAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { + w.Header().Set(contentType, a.Headers.V().UnauthContentType) + w.Header().Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) + w.WriteHeader(a.Headers.V().UnauthCode) + w.Write([]byte(a.Headers.V().UnauthResponse)) +} + +// Wrap returns an http.HandlerFunc, which wraps +// AuthenticatedHandlerFunc with this BasicAuth authenticator's +// authentication checks. Once the request contains valid credentials, +// it calls wrapped AuthenticatedHandlerFunc. +// +// Deprecated: new code should use NewContext instead. +func (a *BasicAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if username := a.CheckAuth(r); username == "" { + a.RequireAuth(w, r) + } else { + ar := &AuthenticatedRequest{Request: *r, Username: username} + wrapped(w, ar) + } + } +} + +// NewContext returns a context carrying authentication information for the request. +func (a *BasicAuth) NewContext(ctx context.Context, r *http.Request) context.Context { + info := &Info{Username: a.CheckAuth(r), ResponseHeaders: make(http.Header)} + info.Authenticated = (info.Username != "") + if !info.Authenticated { + info.ResponseHeaders.Set(a.Headers.V().Authenticate, `Basic realm="`+a.Realm+`"`) + } + return context.WithValue(ctx, infoKey, info) +} + +// NewBasicAuthenticator returns a BasicAuth initialized with provided +// realm and secrets. +// +// Deprecated: new code should construct BasicAuth values directly. +func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth { + return &BasicAuth{Realm: realm, Secrets: secrets} +} diff --git a/vendor/github.com/abbot/go-http-auth/digest.go b/vendor/github.com/abbot/go-http-auth/digest.go new file mode 100644 index 00000000..88ac4bd4 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/digest.go @@ -0,0 +1,288 @@ +package auth + +import ( + "context" + "crypto/subtle" + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +type digestClient struct { + nc uint64 + lastSeen int64 +} + +// DigestAuth is an authenticator implementation for 'Digest' HTTP Authentication scheme (RFC 7616). +// +// Note: this implementation was written following now deprecated RFC +// 2617, and supports only MD5 algorithm. +// +// TODO: Add support for SHA-256 and SHA-512/256 algorithms. +type DigestAuth struct { + Realm string + Opaque string + Secrets SecretProvider + PlainTextSecrets bool + IgnoreNonceCount bool + // Headers used by authenticator. Set to ProxyHeaders to use with + // proxy server. When nil, NormalHeaders are used. + Headers *Headers + + /* + Approximate size of Client's Cache. When actual number of + tracked client nonces exceeds + ClientCacheSize+ClientCacheTolerance, ClientCacheTolerance*2 + older entries are purged. + */ + ClientCacheSize int + ClientCacheTolerance int + + clients map[string]*digestClient + mutex sync.RWMutex +} + +// check that DigestAuth implements AuthenticatorInterface +var _ = (AuthenticatorInterface)((*DigestAuth)(nil)) + +type digestCacheEntry struct { + nonce string + lastSeen int64 +} + +type digestCache []digestCacheEntry + +func (c digestCache) Less(i, j int) bool { + return c[i].lastSeen < c[j].lastSeen +} + +func (c digestCache) Len() int { + return len(c) +} + +func (c digestCache) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +// Purge removes count oldest entries from DigestAuth.clients +func (da *DigestAuth) Purge(count int) { + da.mutex.Lock() + da.purgeLocked(count) + da.mutex.Unlock() +} + +func (da *DigestAuth) purgeLocked(count int) { + entries := make([]digestCacheEntry, 0, len(da.clients)) + for nonce, client := range da.clients { + entries = append(entries, digestCacheEntry{nonce, client.lastSeen}) + } + cache := digestCache(entries) + sort.Sort(cache) + for _, client := range cache[:count] { + delete(da.clients, client.nonce) + } +} + +// RequireAuth is an http.HandlerFunc which initiates the +// authentication process (or requires reauthentication). +func (da *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { + da.mutex.RLock() + clientsLen := len(da.clients) + da.mutex.RUnlock() + + if clientsLen > da.ClientCacheSize+da.ClientCacheTolerance { + da.Purge(da.ClientCacheTolerance * 2) + } + nonce := RandomKey() + + da.mutex.Lock() + da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()} + da.mutex.Unlock() + + da.mutex.RLock() + w.Header().Set(contentType, da.Headers.V().UnauthContentType) + w.Header().Set(da.Headers.V().Authenticate, + fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`, + da.Realm, nonce, da.Opaque)) + w.WriteHeader(da.Headers.V().UnauthCode) + w.Write([]byte(da.Headers.V().UnauthResponse)) + da.mutex.RUnlock() +} + +// DigestAuthParams parses Authorization header from the +// http.Request. Returns a map of auth parameters or nil if the header +// is not a valid parsable Digest auth header. +func DigestAuthParams(authorization string) map[string]string { + s := strings.SplitN(authorization, " ", 2) + if len(s) != 2 || s[0] != "Digest" { + return nil + } + + return ParsePairs(s[1]) +} + +// CheckAuth checks whether the request contains valid authentication +// data. Returns a pair of username, authinfo, where username is the +// name of the authenticated user or an empty string and authinfo is +// the contents for the optional Authentication-Info response header. +func (da *DigestAuth) CheckAuth(r *http.Request) (username string, authinfo *string) { + da.mutex.RLock() + defer da.mutex.RUnlock() + username = "" + authinfo = nil + auth := DigestAuthParams(r.Header.Get(da.Headers.V().Authorization)) + if auth == nil { + return "", nil + } + // RFC2617 Section 3.2.1 specifies that unset value of algorithm in + // WWW-Authenticate Response header should be treated as + // "MD5". According to section 3.2.2 the "algorithm" value in + // subsequent Request Authorization header must be set to whatever + // was supplied in the WWW-Authenticate Response header. This + // implementation always returns an algorithm in WWW-Authenticate + // header, however there seems to be broken clients in the wild + // which do not set the algorithm. Assume the unset algorithm in + // Authorization header to be equal to MD5. + if _, ok := auth["algorithm"]; !ok { + auth["algorithm"] = "MD5" + } + if da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" { + return "", nil + } + + // Check if the requested URI matches auth header + if r.RequestURI != auth["uri"] { + // We allow auth["uri"] to be a full path prefix of request-uri + // for some reason lost in history, which is probably wrong, but + // used to be like that for quite some time + // (https://tools.ietf.org/html/rfc2617#section-3.2.2 explicitly + // says that auth["uri"] is the request-uri). + // + // TODO: make an option to allow only strict checking. + switch u, err := url.Parse(auth["uri"]); { + case err != nil: + return "", nil + case r.URL == nil: + return "", nil + case len(u.Path) > len(r.URL.Path): + return "", nil + case !strings.HasPrefix(r.URL.Path, u.Path): + return "", nil + } + } + + HA1 := da.Secrets(auth["username"], da.Realm) + if da.PlainTextSecrets { + HA1 = H(auth["username"] + ":" + da.Realm + ":" + HA1) + } + HA2 := H(r.Method + ":" + auth["uri"]) + KD := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], HA2}, ":")) + + if subtle.ConstantTimeCompare([]byte(KD), []byte(auth["response"])) != 1 { + return "", nil + } + + // At this point crypto checks are completed and validated. + // Now check if the session is valid. + + nc, err := strconv.ParseUint(auth["nc"], 16, 64) + if err != nil { + return "", nil + } + + client, ok := da.clients[auth["nonce"]] + if !ok { + return "", nil + } + if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount { + return "", nil + } + client.nc = nc + client.lastSeen = time.Now().UnixNano() + + respHA2 := H(":" + auth["uri"]) + rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], respHA2}, ":")) + + info := fmt.Sprintf(`qop="auth", rspauth="%s", cnonce="%s", nc="%s"`, rspauth, auth["cnonce"], auth["nc"]) + return auth["username"], &info +} + +// Default values for ClientCacheSize and ClientCacheTolerance for DigestAuth +const ( + DefaultClientCacheSize = 1000 + DefaultClientCacheTolerance = 100 +) + +// Wrap returns an http.HandlerFunc wraps AuthenticatedHandlerFunc +// with this DigestAuth authentication checks. Once the request +// contains valid credentials, it calls wrapped +// AuthenticatedHandlerFunc. +// +// Deprecated: new code should use NewContext instead. +func (da *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if username, authinfo := da.CheckAuth(r); username == "" { + da.RequireAuth(w, r) + } else { + ar := &AuthenticatedRequest{Request: *r, Username: username} + if authinfo != nil { + w.Header().Set(da.Headers.V().AuthInfo, *authinfo) + } + wrapped(w, ar) + } + } +} + +// JustCheck returns a new http.HandlerFunc, which requires +// DigestAuth to successfully authenticate a user before calling +// wrapped http.HandlerFunc. +// +// Authenticated Username is passed as an extra +// X-Authenticated-Username header to the wrapped HandlerFunc. +func (da *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc { + return da.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { + ar.Header.Set(AuthUsernameHeader, ar.Username) + wrapped(w, &ar.Request) + }) +} + +// NewContext returns a context carrying authentication information for the request. +func (da *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context { + username, authinfo := da.CheckAuth(r) + da.mutex.Lock() + defer da.mutex.Unlock() + info := &Info{Username: username, ResponseHeaders: make(http.Header)} + if username != "" { + info.Authenticated = true + info.ResponseHeaders.Set(da.Headers.V().AuthInfo, *authinfo) + } else { + // return back digest WWW-Authenticate header + if len(da.clients) > da.ClientCacheSize+da.ClientCacheTolerance { + da.purgeLocked(da.ClientCacheTolerance * 2) + } + nonce := RandomKey() + da.clients[nonce] = &digestClient{nc: 0, lastSeen: time.Now().UnixNano()} + info.ResponseHeaders.Set(da.Headers.V().Authenticate, + fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm=MD5, qop="auth"`, + da.Realm, nonce, da.Opaque)) + } + return context.WithValue(ctx, infoKey, info) +} + +// NewDigestAuthenticator generates a new DigestAuth object +func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth { + da := &DigestAuth{ + Opaque: RandomKey(), + Realm: realm, + Secrets: secrets, + PlainTextSecrets: false, + ClientCacheSize: DefaultClientCacheSize, + ClientCacheTolerance: DefaultClientCacheTolerance, + clients: map[string]*digestClient{}} + return da +} diff --git a/vendor/github.com/abbot/go-http-auth/go.mod b/vendor/github.com/abbot/go-http-auth/go.mod new file mode 100644 index 00000000..80cdc218 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/go.mod @@ -0,0 +1,5 @@ +module github.com/abbot/go-http-auth + +go 1.14 + +require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 diff --git a/vendor/github.com/abbot/go-http-auth/go.sum b/vendor/github.com/abbot/go-http-auth/go.sum new file mode 100644 index 00000000..814b1a38 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/vendor/github.com/abbot/go-http-auth/md5crypt.go b/vendor/github.com/abbot/go-http-auth/md5crypt.go new file mode 100644 index 00000000..ed7ccc37 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/md5crypt.go @@ -0,0 +1,73 @@ +package auth + +import "crypto/md5" + +const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +var md5CryptSwaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} + +// MD5Crypt is the MD5 password crypt implementation. +func MD5Crypt(password, salt, magic []byte) []byte { + d := md5.New() + + d.Write(password) + d.Write(magic) + d.Write(salt) + + d2 := md5.New() + d2.Write(password) + d2.Write(salt) + d2.Write(password) + + for i, mixin := 0, d2.Sum(nil); i < len(password); i++ { + d.Write([]byte{mixin[i%16]}) + } + + for i := len(password); i != 0; i >>= 1 { + if i&1 == 0 { + d.Write([]byte{password[0]}) + } else { + d.Write([]byte{0}) + } + } + + final := d.Sum(nil) + + for i := 0; i < 1000; i++ { + d2 := md5.New() + if i&1 == 0 { + d2.Write(final) + } else { + d2.Write(password) + } + + if i%3 != 0 { + d2.Write(salt) + } + + if i%7 != 0 { + d2.Write(password) + } + + if i&1 == 0 { + d2.Write(password) + } else { + d2.Write(final) + } + final = d2.Sum(nil) + } + + result := make([]byte, 0, 22) + v := uint(0) + bits := uint(0) + for _, i := range md5CryptSwaps { + v |= (uint(final[i]) << bits) + for bits = bits + 8; bits > 6; bits -= 6 { + result = append(result, itoa64[v&0x3f]) + v >>= 6 + } + } + result = append(result, itoa64[v&0x3f]) + + return append(append(append(magic, salt...), '$'), result...) +} diff --git a/vendor/github.com/abbot/go-http-auth/misc.go b/vendor/github.com/abbot/go-http-auth/misc.go new file mode 100644 index 00000000..c4380255 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/misc.go @@ -0,0 +1,146 @@ +package auth + +import ( + "bytes" + "crypto/md5" + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + "strings" +) + +// RandomKey returns a random 16-byte base64 alphabet string +func RandomKey() string { + k := make([]byte, 12) + for bytes := 0; bytes < len(k); { + n, err := rand.Read(k[bytes:]) + if err != nil { + panic("rand.Read() failed") + } + bytes += n + } + return base64.StdEncoding.EncodeToString(k) +} + +// H function for MD5 algorithm (returns a lower-case hex MD5 digest) +func H(data string) string { + digest := md5.New() + digest.Write([]byte(data)) + return fmt.Sprintf("%x", digest.Sum(nil)) +} + +// ParseList parses a comma-separated list of values as described by +// RFC 2068 and returns list elements. +// +// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go +// which was ported from urllib2.parse_http_list, from the Python +// standard library. +func ParseList(value string) []string { + var list []string + var escape, quote bool + b := new(bytes.Buffer) + for _, r := range value { + switch { + case escape: + b.WriteRune(r) + escape = false + case quote: + if r == '\\' { + escape = true + } else { + if r == '"' { + quote = false + } + b.WriteRune(r) + } + case r == ',': + list = append(list, strings.TrimSpace(b.String())) + b.Reset() + case r == '"': + quote = true + b.WriteRune(r) + default: + b.WriteRune(r) + } + } + // Append last part. + if s := b.String(); s != "" { + list = append(list, strings.TrimSpace(s)) + } + return list +} + +// ParsePairs extracts key/value pairs from a comma-separated list of +// values as described by RFC 2068 and returns a map[key]value. The +// resulting values are unquoted. If a list element doesn't contain a +// "=", the key is the element itself and the value is an empty +// string. +// +// Lifted from https://code.google.com/p/gorilla/source/browse/http/parser/parser.go +func ParsePairs(value string) map[string]string { + m := make(map[string]string) + for _, pair := range ParseList(strings.TrimSpace(value)) { + switch i := strings.Index(pair, "="); { + case i < 0: + // No '=' in pair, treat whole string as a 'key'. + m[pair] = "" + case i == len(pair)-1: + // Malformed pair ('key=' with no value), keep key with empty value. + m[pair[:i]] = "" + default: + v := pair[i+1:] + if v[0] == '"' && v[len(v)-1] == '"' { + // Unquote it. + v = v[1 : len(v)-1] + } + m[pair[:i]] = v + } + } + return m +} + +// Headers contains header and error codes used by authenticator. +type Headers struct { + Authenticate string // WWW-Authenticate + Authorization string // Authorization + AuthInfo string // Authentication-Info + UnauthCode int // 401 + UnauthContentType string // text/plain + UnauthResponse string // Unauthorized. +} + +// V returns NormalHeaders when h is nil, or h otherwise. Allows to +// use uninitialized *Headers values in structs. +func (h *Headers) V() *Headers { + if h == nil { + return NormalHeaders + } + return h +} + +var ( + // NormalHeaders are the regular Headers used by an HTTP Server for + // request authentication. + NormalHeaders = &Headers{ + Authenticate: "WWW-Authenticate", + Authorization: "Authorization", + AuthInfo: "Authentication-Info", + UnauthCode: http.StatusUnauthorized, + UnauthContentType: "text/plain", + UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized)), + } + + // ProxyHeaders are Headers used by an HTTP Proxy server for proxy + // access authentication. + ProxyHeaders = &Headers{ + Authenticate: "Proxy-Authenticate", + Authorization: "Proxy-Authorization", + AuthInfo: "Proxy-Authentication-Info", + UnauthCode: http.StatusProxyAuthRequired, + UnauthContentType: "text/plain", + UnauthResponse: fmt.Sprintf("%d %s\n", http.StatusProxyAuthRequired, http.StatusText(http.StatusProxyAuthRequired)), + } +) + +const contentType = "Content-Type" diff --git a/vendor/github.com/abbot/go-http-auth/test.htdigest b/vendor/github.com/abbot/go-http-auth/test.htdigest new file mode 100644 index 00000000..6c8c75b4 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/test.htdigest @@ -0,0 +1 @@ +test:example.com:aa78524fceb0e50fd8ca96dd818b8cf9 diff --git a/vendor/github.com/abbot/go-http-auth/test.htpasswd b/vendor/github.com/abbot/go-http-auth/test.htpasswd new file mode 100644 index 00000000..4844a3cc --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/test.htpasswd @@ -0,0 +1,4 @@ +test:{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00= +test2:$apr1$a0j62R97$mYqFkloXH0/UOaUnAiV2b0 +test16:$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0 +test3:$2y$05$ih3C91zUBSTFcAh2mQnZYuob0UOZVEf16wl/ukgjDhjvj.xgM1WwS diff --git a/vendor/github.com/abbot/go-http-auth/users.go b/vendor/github.com/abbot/go-http-auth/users.go new file mode 100644 index 00000000..ba032fe9 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/users.go @@ -0,0 +1,151 @@ +package auth + +import ( + "encoding/csv" + "os" + "sync" +) + +// SecretProvider is used by authenticators. Takes user name and realm +// as an argument, returns secret required for authentication (HA1 for +// digest authentication, properly encrypted password for basic). +// +// Returning an empty string means failing the authentication. +type SecretProvider func(user, realm string) string + +// File handles automatic file reloading on changes. +type File struct { + Path string + Info os.FileInfo + /* must be set in inherited types during initialization */ + Reload func() + mu sync.Mutex +} + +// ReloadIfNeeded checks file Stat and calls Reload() if any changes +// were detected. File mutex is Locked for the duration of Reload() +// call. +// +// This function will panic() if Stat fails. +func (f *File) ReloadIfNeeded() { + info, err := os.Stat(f.Path) + if err != nil { + panic(err) + } + f.mu.Lock() + defer f.mu.Unlock() + if f.Info == nil || f.Info.ModTime() != info.ModTime() { + f.Info = info + f.Reload() + } +} + +// HtdigestFile is a File holding htdigest authentication data. +type HtdigestFile struct { + // File is used for automatic reloading of the authentication data. + File + // Users is a map of realms to users to HA1 digests. + Users map[string]map[string]string + mu sync.RWMutex +} + +func reloadHTDigest(hf *HtdigestFile) { + r, err := os.Open(hf.Path) + if err != nil { + panic(err) + } + reader := csv.NewReader(r) + reader.Comma = ':' + reader.Comment = '#' + reader.TrimLeadingSpace = true + + records, err := reader.ReadAll() + if err != nil { + panic(err) + } + + hf.mu.Lock() + defer hf.mu.Unlock() + hf.Users = make(map[string]map[string]string) + for _, record := range records { + _, exists := hf.Users[record[1]] + if !exists { + hf.Users[record[1]] = make(map[string]string) + } + hf.Users[record[1]][record[0]] = record[2] + } +} + +// HtdigestFileProvider is a SecretProvider implementation based on +// htdigest-formated files. It will automatically reload htdigest file +// on changes. It panics on syntax errors in htdigest files. +func HtdigestFileProvider(filename string) SecretProvider { + hf := &HtdigestFile{File: File{Path: filename}} + hf.Reload = func() { reloadHTDigest(hf) } + return func(user, realm string) string { + hf.ReloadIfNeeded() + hf.mu.RLock() + defer hf.mu.RUnlock() + _, exists := hf.Users[realm] + if !exists { + return "" + } + digest, exists := hf.Users[realm][user] + if !exists { + return "" + } + return digest + } +} + +// HtpasswdFile is a File holding basic authentication data. +type HtpasswdFile struct { + // File is used for automatic reloading of the authentication data. + File + // Users is a map of users to their secrets (salted encrypted + // passwords). + Users map[string]string + mu sync.RWMutex +} + +func reloadHTPasswd(h *HtpasswdFile) { + r, err := os.Open(h.Path) + if err != nil { + panic(err) + } + reader := csv.NewReader(r) + reader.Comma = ':' + reader.Comment = '#' + reader.TrimLeadingSpace = true + + records, err := reader.ReadAll() + if err != nil { + panic(err) + } + + h.mu.Lock() + defer h.mu.Unlock() + h.Users = make(map[string]string) + for _, record := range records { + h.Users[record[0]] = record[1] + } +} + +// HtpasswdFileProvider is a SecretProvider implementation based on +// htpasswd-formated files. It will automatically reload htpasswd file +// on changes. It panics on syntax errors in htpasswd files. Realm +// argument of the SecretProvider is ignored. +func HtpasswdFileProvider(filename string) SecretProvider { + h := &HtpasswdFile{File: File{Path: filename}} + h.Reload = func() { reloadHTPasswd(h) } + return func(user, realm string) string { + h.ReloadIfNeeded() + h.mu.RLock() + password, exists := h.Users[user] + h.mu.RUnlock() + if !exists { + return "" + } + return password + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index c411e7ce..cf48ea8a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,5 +1,8 @@ # cloud.google.com/go v0.38.0 cloud.google.com/go/compute/metadata +# github.com/abbot/go-http-auth v0.4.1-0.20220112235402-e1cee1c72f2f +## explicit +github.com/abbot/go-http-auth # github.com/aws/aws-sdk-go v1.40.41 ## explicit github.com/aws/aws-sdk-go/aws @@ -85,7 +88,7 @@ github.com/golang/protobuf/ptypes github.com/golang/protobuf/ptypes/any github.com/golang/protobuf/ptypes/duration github.com/golang/protobuf/ptypes/timestamp -# github.com/google/go-cmp v0.5.6 +# github.com/google/go-cmp v0.5.8 ## explicit # github.com/google/uuid v1.3.0 github.com/google/uuid @@ -208,7 +211,7 @@ go.opencensus.io/trace go.opencensus.io/trace/internal go.opencensus.io/trace/propagation go.opencensus.io/trace/tracestate -# golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 +# golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed ## explicit golang.org/x/crypto/acme golang.org/x/crypto/acme/autocert @@ -238,7 +241,8 @@ golang.org/x/crypto/ssh/knownhosts ## explicit golang.org/x/image/draw golang.org/x/image/math/f64 -# golang.org/x/mod v0.3.0 +# golang.org/x/mod v0.4.1 +## explicit golang.org/x/mod/semver # golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 ## explicit @@ -275,7 +279,8 @@ golang.org/x/text/unicode/norm # golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 ## explicit golang.org/x/time/rate -# golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78 +# golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963 +## explicit golang.org/x/tools/go/gcexportdata golang.org/x/tools/go/internal/gcimporter golang.org/x/tools/go/internal/packagesdriver