diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67c32c1ae..0a73f5cad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -237,7 +237,7 @@ The `vendor` directory is entirely managed by the `dep` tool. To add a new dependency - dep ensure github.com/pkg/errors + dep ensure -add github.com/pkg/errors You can add constraints on that package (see the `dep` documentation), but don't unless you really need to. diff --git a/Gopkg.lock b/Gopkg.lock index a327676b7..282739bd8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -52,6 +52,12 @@ packages = ["."] revision = "cf42b1e486f0b025942a768a9ad59c9939d6ca40" +[[projects]] + name = "github.com/abbot/go-http-auth" + packages = ["."] + revision = "0ddd408d5d60ea76e320503cc7dd091992dee608" + version = "v0.4.0" + [[projects]] branch = "master" name = "github.com/aws/aws-sdk-go" @@ -321,6 +327,8 @@ branch = "master" name = "golang.org/x/crypto" packages = [ + "bcrypt", + "blowfish", "curve25519", "ed25519", "ed25519/internal/edwards25519", @@ -427,6 +435,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "ad6c96ca62ee6a305b6083c53f2f4939d8ae22ee6163ad84b1f09d8fcbcd1ff7" + inputs-digest = "bbb981a57fa3540cbfdf3f4255a8eb6b7b9980af2259cffab2f0eddcc3818bcc" solver-name = "gps-cdcl" solver-version = 1 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 000000000..112ea3951 --- /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 000000000..e454a5258 --- /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 000000000..25f208da0 --- /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 000000000..73ae98522 --- /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 000000000..05ded165e --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/auth.go @@ -0,0 +1,109 @@ +// Package auth is an implementation of HTTP Basic and HTTP Digest authentication. +package auth + +import ( + "net/http" + + "golang.org/x/net/context" +) + +/* + Request handlers must take AuthenticatedRequest instead of http.Request +*/ +type AuthenticatedRequest struct { + http.Request + /* + 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 = 0 + +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" + +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 000000000..b03dd5823 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/basic.go @@ -0,0 +1,163 @@ +package auth + +import ( + "bytes" + "crypto/sha1" + "crypto/subtle" + "encoding/base64" + "errors" + "net/http" + "strings" + + "golang.org/x/crypto/bcrypt" + "golang.org/x/net/context" +) + +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}, + } +) + +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)) + +/* + Checks the username/password combination from the request. Returns + either an empty string (authentication failed) or the name of the + authenticated user. + + Supports MD5 and SHA1 password entries +*/ +func (a *BasicAuth) CheckAuth(r *http.Request) string { + s := strings.SplitN(r.Header.Get(a.Headers.V().Authorization), " ", 2) + if len(s) != 2 || s[0] != "Basic" { + return "" + } + + b, err := base64.StdEncoding.DecodeString(s[1]) + if err != nil { + return "" + } + pair := strings.SplitN(string(b), ":", 2) + if len(pair) != 2 { + return "" + } + user, password := pair[0], pair[1] + secret := a.Secrets(user, a.Realm) + if secret == "" { + return "" + } + compare := compareFuncs[0].compare + for _, cmp := range compareFuncs[1:] { + if strings.HasPrefix(secret, cmp.prefix) { + compare = cmp.compare + break + } + } + if compare([]byte(secret), []byte(password)) != nil { + return "" + } + return pair[0] +} + +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 +} + +/* + http.Handler 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)) +} + +/* + BasicAuthenticator returns a function, which wraps an + AuthenticatedHandlerFunc converting it to http.HandlerFunc. This + wrapper function checks the authentication and either sends back + required authentication headers, or calls the wrapped function with + authenticated username in the AuthenticatedRequest. +*/ +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) +} + +func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth { + return &BasicAuth{Realm: realm, Secrets: secrets} +} diff --git a/vendor/github.com/abbot/go-http-auth/basic_test.go b/vendor/github.com/abbot/go-http-auth/basic_test.go new file mode 100644 index 000000000..99f44fc32 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/basic_test.go @@ -0,0 +1,40 @@ +package auth + +import ( + "encoding/base64" + "net/http" + "testing" +) + +func TestAuthBasic(t *testing.T) { + secrets := HtpasswdFileProvider("test.htpasswd") + a := &BasicAuth{Realm: "example.com", Secrets: secrets} + r := &http.Request{} + r.Method = "GET" + if a.CheckAuth(r) != "" { + t.Fatal("CheckAuth passed on empty headers") + } + r.Header = http.Header(make(map[string][]string)) + r.Header.Set("Authorization", "Digest blabla ololo") + if a.CheckAuth(r) != "" { + t.Fatal("CheckAuth passed on bad headers") + } + r.Header.Set("Authorization", "Basic !@#") + if a.CheckAuth(r) != "" { + t.Fatal("CheckAuth passed on bad base64 data") + } + + data := [][]string{ + {"test", "hello"}, + {"test2", "hello2"}, + {"test3", "hello3"}, + {"test16", "topsecret"}, + } + for _, tc := range data { + auth := base64.StdEncoding.EncodeToString([]byte(tc[0] + ":" + tc[1])) + r.Header.Set("Authorization", "Basic "+auth) + if a.CheckAuth(r) != tc[0] { + t.Fatalf("CheckAuth failed for user '%s'", tc[0]) + } + } +} 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 000000000..21b09334c --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/digest.go @@ -0,0 +1,274 @@ +package auth + +import ( + "crypto/subtle" + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" + + "golang.org/x/net/context" +) + +type digest_client struct { + nc uint64 + last_seen int64 +} + +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]*digest_client + mutex sync.Mutex +} + +// check that DigestAuth implements AuthenticatorInterface +var _ = (AuthenticatorInterface)((*DigestAuth)(nil)) + +type digest_cache_entry struct { + nonce string + last_seen int64 +} + +type digest_cache []digest_cache_entry + +func (c digest_cache) Less(i, j int) bool { + return c[i].last_seen < c[j].last_seen +} + +func (c digest_cache) Len() int { + return len(c) +} + +func (c digest_cache) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +/* + Remove count oldest entries from DigestAuth.clients +*/ +func (a *DigestAuth) Purge(count int) { + entries := make([]digest_cache_entry, 0, len(a.clients)) + for nonce, client := range a.clients { + entries = append(entries, digest_cache_entry{nonce, client.last_seen}) + } + cache := digest_cache(entries) + sort.Sort(cache) + for _, client := range cache[:count] { + delete(a.clients, client.nonce) + } +} + +/* + http.Handler for DigestAuth which initiates the authentication process + (or requires reauthentication). +*/ +func (a *DigestAuth) RequireAuth(w http.ResponseWriter, r *http.Request) { + if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance { + a.Purge(a.ClientCacheTolerance * 2) + } + nonce := RandomKey() + a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + w.Header().Set(contentType, a.Headers.V().UnauthContentType) + w.Header().Set(a.Headers.V().Authenticate, + fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, + a.Realm, nonce, a.Opaque)) + w.WriteHeader(a.Headers.V().UnauthCode) + w.Write([]byte(a.Headers.V().UnauthResponse)) +} + +/* + Parse 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]) +} + +/* + Check if 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.Lock() + defer da.mutex.Unlock() + 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 + } + + if client, ok := da.clients[auth["nonce"]]; !ok { + return "", nil + } else { + if client.nc != 0 && client.nc >= nc && !da.IgnoreNonceCount { + return "", nil + } + client.nc = nc + client.last_seen = time.Now().UnixNano() + } + + resp_HA2 := H(":" + auth["uri"]) + rspauth := H(strings.Join([]string{HA1, auth["nonce"], auth["nc"], auth["cnonce"], auth["qop"], resp_HA2}, ":")) + + 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 +const DefaultClientCacheTolerance = 100 + +/* + Wrap returns an Authenticator which uses HTTP Digest + authentication. Arguments: + + realm: The authentication realm. + + secrets: SecretProvider which must return HA1 digests for the same + realm as above. +*/ +func (a *DigestAuth) Wrap(wrapped AuthenticatedHandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if username, authinfo := a.CheckAuth(r); username == "" { + a.RequireAuth(w, r) + } else { + ar := &AuthenticatedRequest{Request: *r, Username: username} + if authinfo != nil { + w.Header().Set(a.Headers.V().AuthInfo, *authinfo) + } + wrapped(w, ar) + } + } +} + +/* + JustCheck returns function which converts an http.HandlerFunc into a + http.HandlerFunc which requires authentication. Username is passed as + an extra X-Authenticated-Username header. +*/ +func (a *DigestAuth) JustCheck(wrapped http.HandlerFunc) http.HandlerFunc { + return a.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 (a *DigestAuth) NewContext(ctx context.Context, r *http.Request) context.Context { + username, authinfo := a.CheckAuth(r) + info := &Info{Username: username, ResponseHeaders: make(http.Header)} + if username != "" { + info.Authenticated = true + info.ResponseHeaders.Set(a.Headers.V().AuthInfo, *authinfo) + } else { + // return back digest WWW-Authenticate header + if len(a.clients) > a.ClientCacheSize+a.ClientCacheTolerance { + a.Purge(a.ClientCacheTolerance * 2) + } + nonce := RandomKey() + a.clients[nonce] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + info.ResponseHeaders.Set(a.Headers.V().Authenticate, + fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, + a.Realm, nonce, a.Opaque)) + } + return context.WithValue(ctx, infoKey, info) +} + +func NewDigestAuthenticator(realm string, secrets SecretProvider) *DigestAuth { + da := &DigestAuth{ + Opaque: RandomKey(), + Realm: realm, + Secrets: secrets, + PlainTextSecrets: false, + ClientCacheSize: DefaultClientCacheSize, + ClientCacheTolerance: DefaultClientCacheTolerance, + clients: map[string]*digest_client{}} + return da +} diff --git a/vendor/github.com/abbot/go-http-auth/digest_test.go b/vendor/github.com/abbot/go-http-auth/digest_test.go new file mode 100644 index 000000000..ffbbd6b4a --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/digest_test.go @@ -0,0 +1,76 @@ +package auth + +import ( + "net/http" + "net/url" + "testing" + "time" +) + +func TestAuthDigest(t *testing.T) { + secrets := HtdigestFileProvider("test.htdigest") + da := &DigestAuth{Opaque: "U7H+ier3Ae8Skd/g", + Realm: "example.com", + Secrets: secrets, + clients: map[string]*digest_client{}} + r := &http.Request{} + r.Method = "GET" + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for empty request header") + } + r.Header = http.Header(make(map[string][]string)) + r.Header.Set("Authorization", "Digest blabla") + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for bad request header") + } + r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="Vb9BP/h81n3GpTTB", uri="/t2", cnonce="NjE4MTM2", nc=00000001, qop="auth", response="ffc357c4eba74773c8687e0bc724c9a3", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`) + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for unknown client") + } + + r.URL, _ = url.Parse("/t2") + da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + if u, _ := da.CheckAuth(r); u != "test" { + t.Fatal("empty auth for legitimate client") + } + + // our nc is now 0, client nc is 1 + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for outdated nc") + } + + // try again with nc checking off + da.IgnoreNonceCount = true + if u, _ := da.CheckAuth(r); u != "test" { + t.Fatal("empty auth for outdated nc even though nc checking is off") + } + da.IgnoreNonceCount = false + + r.URL, _ = url.Parse("/") + da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for bad request path") + } + + r.URL, _ = url.Parse("/t3") + da.clients["Vb9BP/h81n3GpTTB"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for bad request path") + } + + da.clients["+RbVXSbIoa1SaJk1"] = &digest_client{nc: 0, last_seen: time.Now().UnixNano()} + r.Header.Set("Authorization", `Digest username="test", realm="example.com", nonce="+RbVXSbIoa1SaJk1", uri="/", cnonce="NjE4NDkw", nc=00000001, qop="auth", response="c08918024d7faaabd5424654c4e3ad1c", opaque="U7H+ier3Ae8Skd/g", algorithm="MD5"`) + if u, _ := da.CheckAuth(r); u != "test" { + t.Fatal("empty auth for valid request in subpath") + } +} + +func TestDigestAuthParams(t *testing.T) { + const authorization = `Digest username="test", realm="", nonce="FRPnGdb8lvM1UHhi", uri="/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", algorithm=MD5, response="fdcdd78e5b306ffed343d0ec3967f2e5", opaque="lEgVjogmIar2fg/t", qop=auth, nc=00000001, cnonce="e76b05db27a3b323"` + + params := DigestAuthParams(authorization) + want := "/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro" + if params["uri"] != want { + t.Fatalf("failed to parse uri with embedded commas, got %q want %q", params["uri"], want) + } +} diff --git a/vendor/github.com/abbot/go-http-auth/examples/basic.go b/vendor/github.com/abbot/go-http-auth/examples/basic.go new file mode 100644 index 000000000..49d3989da --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/examples/basic.go @@ -0,0 +1,35 @@ +// +build ignore + +/* + Example application using Basic auth + + Build with: + + go build basic.go +*/ + +package main + +import ( + auth ".." + "fmt" + "net/http" +) + +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) +} diff --git a/vendor/github.com/abbot/go-http-auth/examples/context.go b/vendor/github.com/abbot/go-http-auth/examples/context.go new file mode 100644 index 000000000..b3ca8c6b2 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/examples/context.go @@ -0,0 +1,60 @@ +// +build ignore + +/* + Example application using NewContext/FromContext + + Build with: + + go build context.go +*/ + +package main + +import ( + "fmt" + "net/http" + + auth ".." + "golang.org/x/net/context" +) + +func Secret(user, realm string) string { + if user == "john" { + // password is "hello" + return "b98e16cbc3d01734b264adba7baa3bf9" + } + return "" +} + +type ContextHandler interface { + ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) +} + +type ContextHandlerFunc func(ctx context.Context, w http.ResponseWriter, r *http.Request) + +func (f ContextHandlerFunc) ServeHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request) { + f(ctx, w, r) +} + +func handle(ctx context.Context, w http.ResponseWriter, r *http.Request) { + authInfo := auth.FromContext(ctx) + authInfo.UpdateHeaders(w.Header()) + if authInfo == nil || !authInfo.Authenticated { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + fmt.Fprintf(w, "

Hello, %s!

", authInfo.Username) +} + +func authenticatedHandler(a auth.AuthenticatorInterface, h ContextHandler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := a.NewContext(context.Background(), r) + h.ServeHTTP(ctx, w, r) + }) +} + +func main() { + authenticator := auth.NewDigestAuthenticator("example.com", Secret) + http.Handle("/", authenticatedHandler(authenticator, ContextHandlerFunc(handle))) + http.ListenAndServe(":8080", nil) +} diff --git a/vendor/github.com/abbot/go-http-auth/examples/digest.go b/vendor/github.com/abbot/go-http-auth/examples/digest.go new file mode 100644 index 000000000..385989338 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/examples/digest.go @@ -0,0 +1,35 @@ +// +build ignore + +/* + Example application using Digest auth + + Build with: + + go build digest.go +*/ + +package main + +import ( + auth ".." + "fmt" + "net/http" +) + +func Secret(user, realm string) string { + if user == "john" { + // password is "hello" + return "b98e16cbc3d01734b264adba7baa3bf9" + } + return "" +} + +func handle(w http.ResponseWriter, r *auth.AuthenticatedRequest) { + fmt.Fprintf(w, "

Hello, %s!

", r.Username) +} + +func main() { + authenticator := auth.NewDigestAuthenticator("example.com", Secret) + http.HandleFunc("/", authenticator.Wrap(handle)) + http.ListenAndServe(":8080", nil) +} diff --git a/vendor/github.com/abbot/go-http-auth/examples/wrapped.go b/vendor/github.com/abbot/go-http-auth/examples/wrapped.go new file mode 100644 index 000000000..aa95ec38f --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/examples/wrapped.go @@ -0,0 +1,36 @@ +// +build ignore + +/* + Example demonstrating how to wrap an application which is unaware of + authenticated requests with a "pass-through" authentication + + Build with: + + go build wrapped.go +*/ + +package main + +import ( + auth ".." + "fmt" + "net/http" +) + +func Secret(user, realm string) string { + if user == "john" { + // password is "hello" + return "$1$dlPL2MqE$oQmn16q49SqdmhenQuNgs1" + } + return "" +} + +func regular_handler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "

This application is unaware of authentication

") +} + +func main() { + authenticator := auth.NewBasicAuthenticator("example.com", Secret) + http.HandleFunc("/", auth.JustCheck(authenticator, regular_handler)) + http.ListenAndServe(":8080", nil) +} 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 000000000..a7a031c41 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/md5crypt.go @@ -0,0 +1,92 @@ +package auth + +import "crypto/md5" +import "strings" + +const itoa64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +var md5_crypt_swaps = [16]int{12, 6, 0, 13, 7, 1, 14, 8, 2, 15, 9, 3, 5, 10, 4, 11} + +type MD5Entry struct { + Magic, Salt, Hash []byte +} + +func NewMD5Entry(e string) *MD5Entry { + parts := strings.SplitN(e, "$", 4) + if len(parts) != 4 { + return nil + } + return &MD5Entry{ + Magic: []byte("$" + parts[1] + "$"), + Salt: []byte(parts[2]), + Hash: []byte(parts[3]), + } +} + +/* + 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 md5_crypt_swaps { + 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/md5crypt_test.go b/vendor/github.com/abbot/go-http-auth/md5crypt_test.go new file mode 100644 index 000000000..2fc9bc6dc --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/md5crypt_test.go @@ -0,0 +1,19 @@ +package auth + +import "testing" + +func Test_MD5Crypt(t *testing.T) { + test_cases := [][]string{ + {"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"}, + {"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"}, + {"topsecret", "$apr1$JI4wh3am$AmhephVqLTUyAVpFQeHZC0"}, + } + for _, tc := range test_cases { + e := NewMD5Entry(tc[1]) + result := MD5Crypt([]byte(tc[0]), e.Salt, e.Magic) + if string(result) != tc[1] { + t.Fatalf("MD5Crypt returned '%s' instead of '%s'", string(result), tc[1]) + } + t.Logf("MD5Crypt: '%s' (%s%s$) -> %s", tc[0], e.Magic, e.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 000000000..4536ce673 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/misc.go @@ -0,0 +1,141 @@ +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)) { + if i := strings.Index(pair, "="); i < 0 { + m[pair] = "" + } else { + 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/misc_test.go b/vendor/github.com/abbot/go-http-auth/misc_test.go new file mode 100644 index 000000000..31a920f0c --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/misc_test.go @@ -0,0 +1,37 @@ +package auth + +import ( + "reflect" + "testing" +) + +func TestH(t *testing.T) { + const hello = "Hello, world!" + const hello_md5 = "6cd3556deb0da54bca060b4c39479839" + h := H(hello) + if h != hello_md5 { + t.Fatal("Incorrect digest for test string:", h, "instead of", hello_md5) + } +} + +func TestParsePairs(t *testing.T) { + const header = `username="\test", realm="a \"quoted\" string", nonce="FRPnGdb8lvM1UHhi", uri="/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", algorithm=MD5, response="fdcdd78e5b306ffed343d0ec3967f2e5", opaque="lEgVjogmIar2fg/t", qop=auth, nc=00000001, cnonce="e76b05db27a3b323"` + + want := map[string]string{ + "username": "test", + "realm": `a "quoted" string`, + "nonce": "FRPnGdb8lvM1UHhi", + "uri": "/css?family=Source+Sans+Pro:400,700,400italic,700italic|Source+Code+Pro", + "algorithm": "MD5", + "response": "fdcdd78e5b306ffed343d0ec3967f2e5", + "opaque": "lEgVjogmIar2fg/t", + "qop": "auth", + "nc": "00000001", + "cnonce": "e76b05db27a3b323", + } + got := ParsePairs(header) + + if !reflect.DeepEqual(got, want) { + t.Fatalf("failed to correctly parse pairs, got %v, want %v\ndiff: %s", got, want) + } +} 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 000000000..6c8c75b4e --- /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 000000000..4844a3ccb --- /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 000000000..377181243 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/users.go @@ -0,0 +1,154 @@ +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 + +/* + Common functions for file auto-reloading +*/ +type File struct { + Path string + Info os.FileInfo + /* must be set in inherited types during initialization */ + Reload func() + mu sync.Mutex +} + +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() + } +} + +/* + Structure used for htdigest file authentication. Users map realms to + maps of users to their HA1 digests. +*/ +type HtdigestFile struct { + File + Users map[string]map[string]string + mu sync.RWMutex +} + +func reload_htdigest(hf *HtdigestFile) { + r, err := os.Open(hf.Path) + if err != nil { + panic(err) + } + csv_reader := csv.NewReader(r) + csv_reader.Comma = ':' + csv_reader.Comment = '#' + csv_reader.TrimLeadingSpace = true + + records, err := csv_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] + } +} + +/* + SecretProvider implementation based on htdigest-formated files. Will + reload htdigest file on changes. Will panic on syntax errors in + htdigest files. +*/ +func HtdigestFileProvider(filename string) SecretProvider { + hf := &HtdigestFile{File: File{Path: filename}} + hf.Reload = func() { reload_htdigest(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 + } +} + +/* + Structure used for htdigest file authentication. Users map users to + their salted encrypted password +*/ +type HtpasswdFile struct { + File + Users map[string]string + mu sync.RWMutex +} + +func reload_htpasswd(h *HtpasswdFile) { + r, err := os.Open(h.Path) + if err != nil { + panic(err) + } + csv_reader := csv.NewReader(r) + csv_reader.Comma = ':' + csv_reader.Comment = '#' + csv_reader.TrimLeadingSpace = true + + records, err := csv_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] + } +} + +/* + SecretProvider implementation based on htpasswd-formated files. Will + reload htpasswd file on changes. Will panic 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() { reload_htpasswd(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/github.com/abbot/go-http-auth/users_test.go b/vendor/github.com/abbot/go-http-auth/users_test.go new file mode 100644 index 000000000..784ad7f22 --- /dev/null +++ b/vendor/github.com/abbot/go-http-auth/users_test.go @@ -0,0 +1,45 @@ +package auth + +import ( + "os" + "testing" + "time" +) + +func TestHtdigestFile(t *testing.T) { + secrets := HtdigestFileProvider("test.htdigest") + digest := secrets("test", "example.com") + if digest != "aa78524fceb0e50fd8ca96dd818b8cf9" { + t.Fatal("Incorrect digest for test user:", digest) + } + digest = secrets("test", "example1.com") + if digest != "" { + t.Fatal("Got digest for user in non-existant realm:", digest) + } + digest = secrets("test1", "example.com") + if digest != "" { + t.Fatal("Got digest for non-existant user:", digest) + } +} + +func TestHtpasswdFile(t *testing.T) { + secrets := HtpasswdFileProvider("test.htpasswd") + passwd := secrets("test", "blah") + if passwd != "{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00=" { + t.Fatal("Incorrect passwd for test user:", passwd) + } + passwd = secrets("nosuchuser", "blah") + if passwd != "" { + t.Fatal("Got passwd for non-existant user:", passwd) + } +} + +// TestConcurrent verifies potential race condition in users reading logic +func TestConcurrent(t *testing.T) { + secrets := HtpasswdFileProvider("test.htpasswd") + os.Chtimes("test.htpasswd", time.Now(), time.Now()) + go func() { + secrets("test", "blah") + }() + secrets("test", "blah") +}