From 6798f193fdf13484f32f5d538d87828b0253d7b8 Mon Sep 17 00:00:00 2001 From: Dipankar Sarkar Date: Thu, 11 Dec 2014 17:25:43 +0530 Subject: [PATCH] Added HTTP Auth and HTTP Digest authentication #302 --- Godeps/Godeps.json | 4 + .../github.com/abbot/go-http-auth/.gitignore | 5 + .../src/github.com/abbot/go-http-auth/LICENSE | 178 ++++++++++++++ .../github.com/abbot/go-http-auth/Makefile | 12 + .../github.com/abbot/go-http-auth/README.md | 70 ++++++ .../src/github.com/abbot/go-http-auth/auth.go | 48 ++++ .../github.com/abbot/go-http-auth/basic.go | 88 +++++++ .../abbot/go-http-auth/basic_test.go | 38 +++ .../github.com/abbot/go-http-auth/digest.go | 226 ++++++++++++++++++ .../abbot/go-http-auth/digest_test.go | 57 +++++ .../abbot/go-http-auth/examples/basic.go | 35 +++ .../abbot/go-http-auth/examples/digest.go | 35 +++ .../abbot/go-http-auth/examples/wrapped.go | 36 +++ .../github.com/abbot/go-http-auth/md5crypt.go | 92 +++++++ .../abbot/go-http-auth/md5crypt_test.go | 18 ++ .../src/github.com/abbot/go-http-auth/misc.go | 30 +++ .../abbot/go-http-auth/misc_test.go | 12 + .../abbot/go-http-auth/test.htdigest | 1 + .../abbot/go-http-auth/test.htpasswd | 2 + .../github.com/abbot/go-http-auth/users.go | 136 +++++++++++ .../abbot/go-http-auth/users_test.go | 33 +++ cadvisor.go | 61 ++++- pages/pages.go | 82 ++++++- test.htdigest | 1 + test.htpasswd | 1 + 25 files changed, 1276 insertions(+), 25 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/.gitignore create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/LICENSE create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/Makefile create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/README.md create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/auth.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/basic.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/basic_test.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/digest.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/digest_test.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/basic.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/digest.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/wrapped.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt_test.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/misc.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/misc_test.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htdigest create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htpasswd create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/users.go create mode 100644 Godeps/_workspace/src/github.com/abbot/go-http-auth/users_test.go create mode 100644 test.htdigest create mode 100644 test.htpasswd diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 6c3f4d67..97686a06 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -97,6 +97,10 @@ { "ImportPath": "github.com/stretchr/testify/mock", "Rev": "8ce79b9f0b77745113f82c17d0756771456ccbd3" + }, + { + "ImportPath": "github.com/abbot/go-http-auth", + "Rev": "c0ef4539dfab4d21c8ef20ba2924f9fc6f186d35" } ] } diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/.gitignore b/Godeps/_workspace/src/github.com/abbot/go-http-auth/.gitignore new file mode 100644 index 00000000..112ea395 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/.gitignore @@ -0,0 +1,5 @@ +*~ +*.a +*.6 +*.out +_testmain.go diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/LICENSE b/Godeps/_workspace/src/github.com/abbot/go-http-auth/LICENSE new file mode 100644 index 00000000..e454a525 --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/Makefile b/Godeps/_workspace/src/github.com/abbot/go-http-auth/Makefile new file mode 100644 index 00000000..25f208da --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/README.md b/Godeps/_workspace/src/github.com/abbot/go-http-auth/README.md new file mode 100644 index 00000000..8a26f10f --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/README.md @@ -0,0 +1,70 @@ +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 and SHA1 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 ( + auth "github.com/abbot/go-http-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) + } + +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/Godeps/_workspace/src/github.com/abbot/go-http-auth/auth.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/auth.go new file mode 100644 index 00000000..c4eb5639 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/auth.go @@ -0,0 +1,48 @@ +package auth + +import "net/http" + +/* + 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 + +type AuthenticatorInterface interface { + Wrap(AuthenticatedHandlerFunc) http.HandlerFunc +} + +func JustCheck(auth AuthenticatorInterface, wrapped http.HandlerFunc) http.HandlerFunc { + return auth.Wrap(func(w http.ResponseWriter, ar *AuthenticatedRequest) { + ar.Header.Set("X-Authenticated-Username", ar.Username) + wrapped(w, &ar.Request) + }) +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic.go new file mode 100644 index 00000000..b705c83e --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic.go @@ -0,0 +1,88 @@ +package auth + +import ( + "crypto/sha1" + "encoding/base64" + "net/http" + "strings" +) + +type BasicAuth struct { + Realm string + Secrets SecretProvider +} + +/* + 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("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 "" + } + passwd := a.Secrets(pair[0], a.Realm) + if passwd == "" { + return "" + } + if strings.HasPrefix(passwd, "{SHA}") { + d := sha1.New() + d.Write([]byte(pair[1])) + if passwd[5:] != base64.StdEncoding.EncodeToString(d.Sum(nil)) { + return "" + } + } else { + e := NewMD5Entry(passwd) + if e == nil { + return "" + } + if passwd != string(MD5Crypt([]byte(pair[1]), e.Salt, e.Magic)) { + return "" + } + } + return pair[0] +} + +/* + 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("WWW-Authenticate", `Basic realm="`+a.Realm+`"`) + w.WriteHeader(401) + w.Write([]byte("401 Unauthorized\n")) +} + +/* + 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) + } + } +} + +func NewBasicAuthenticator(realm string, secrets SecretProvider) *BasicAuth { + return &BasicAuth{Realm: realm, Secrets: secrets} +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic_test.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic_test.go new file mode 100644 index 00000000..522e00ff --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/basic_test.go @@ -0,0 +1,38 @@ +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"}, + } + 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/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest.go new file mode 100644 index 00000000..b3225ee4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest.go @@ -0,0 +1,226 @@ +package auth + +import ( + "fmt" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "sync" + "time" +) + +type digest_client struct { + nc uint64 + last_seen int64 +} + +type DigestAuth struct { + Realm string + Opaque string + Secrets SecretProvider + PlainTextSecrets bool + + /* + 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 +} + +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("WWW-Authenticate", + fmt.Sprintf(`Digest realm="%s", nonce="%s", opaque="%s", algorithm="MD5", qop="auth"`, + a.Realm, nonce, a.Opaque)) + w.WriteHeader(401) + w.Write([]byte("401 Unauthorized\n")) +} + +/* + 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(r *http.Request) map[string]string { + s := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + if len(s) != 2 || s[0] != "Digest" { + return nil + } + + result := map[string]string{} + for _, kv := range strings.Split(s[1], ",") { + parts := strings.SplitN(kv, "=", 2) + if len(parts) != 2 { + continue + } + result[strings.Trim(parts[0], "\" ")] = strings.Trim(parts[1], "\" ") + } + return result +} + +/* + 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) + if auth == nil || da.Opaque != auth["opaque"] || auth["algorithm"] != "MD5" || auth["qop"] != "auth" { + return + } + + // Check if the requested URI matches auth header + switch u, err := url.Parse(auth["uri"]); { + case err != nil: + return + case r.URL == nil: + return + case len(u.Path) > len(r.URL.Path): + return + case !strings.HasPrefix(r.URL.Path, u.Path): + return + } + + 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 KD != auth["response"] { + return + } + + // 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 + } + + if client, ok := da.clients[auth["nonce"]]; !ok { + return + } else { + if client.nc != 0 && client.nc >= nc { + return + } + 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("Authentication-Info", *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("X-Authenticated-Username", ar.Username) + wrapped(w, &ar.Request) + }) +} + +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/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest_test.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest_test.go new file mode 100644 index 00000000..b4da0814 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/digest_test.go @@ -0,0 +1,57 @@ +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") + } + if u, _ := da.CheckAuth(r); u != "" { + t.Fatal("non-empty auth for outdated nc") + } + + 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") + } +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/basic.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/basic.go new file mode 100644 index 00000000..49d3989d --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/digest.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/digest.go new file mode 100644 index 00000000..38598933 --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/wrapped.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/examples/wrapped.go new file mode 100644 index 00000000..aa95ec38 --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt.go new file mode 100644 index 00000000..a7a031c4 --- /dev/null +++ b/Godeps/_workspace/src/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/Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt_test.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt_test.go new file mode 100644 index 00000000..65738ece --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/md5crypt_test.go @@ -0,0 +1,18 @@ +package auth + +import "testing" + +func Test_MD5Crypt(t *testing.T) { + test_cases := [][]string{ + {"apache", "$apr1$J.w5a/..$IW9y6DR0oO/ADuhlMF5/X1"}, + {"pass", "$1$YeNsbWdH$wvOF8JdqsoiLix754LTW90"}, + } + 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/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc.go new file mode 100644 index 00000000..277a6859 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc.go @@ -0,0 +1,30 @@ +package auth + +import "encoding/base64" +import "crypto/md5" +import "crypto/rand" +import "fmt" + +/* + Return 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)) +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc_test.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc_test.go new file mode 100644 index 00000000..089524c7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/misc_test.go @@ -0,0 +1,12 @@ +package auth + +import "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) + } +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htdigest b/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htdigest new file mode 100644 index 00000000..6c8c75b4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htdigest @@ -0,0 +1 @@ +test:example.com:aa78524fceb0e50fd8ca96dd818b8cf9 diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htpasswd b/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htpasswd new file mode 100644 index 00000000..7b069898 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/test.htpasswd @@ -0,0 +1,2 @@ +test:{SHA}qvTGHdzF6KLavt4PO0gs2a6pQ00= +test2:$apr1$a0j62R97$mYqFkloXH0/UOaUnAiV2b0 diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/users.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/users.go new file mode 100644 index 00000000..5e7d0b8d --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/users.go @@ -0,0 +1,136 @@ +package auth + +import "encoding/csv" +import "os" + +/* + 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). +*/ +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() +} + +func (f *File) ReloadIfNeeded() { + info, err := os.Stat(f.Path) + if err != nil { + panic(err) + } + 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 +} + +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.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() + _, 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 +} + +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.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() + password, exists := h.Users[user] + if !exists { + return "" + } + return password + } +} diff --git a/Godeps/_workspace/src/github.com/abbot/go-http-auth/users_test.go b/Godeps/_workspace/src/github.com/abbot/go-http-auth/users_test.go new file mode 100644 index 00000000..4fbe8875 --- /dev/null +++ b/Godeps/_workspace/src/github.com/abbot/go-http-auth/users_test.go @@ -0,0 +1,33 @@ +package auth + +import ( + "testing" +) + +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("test3", "blah") + if passwd != "" { + t.Fatal("Got passwd for non-existant user:", passwd) + } +} diff --git a/cadvisor.go b/cadvisor.go index db05a719..7321f9cd 100644 --- a/cadvisor.go +++ b/cadvisor.go @@ -33,6 +33,7 @@ import ( "github.com/google/cadvisor/manager" "github.com/google/cadvisor/pages" "github.com/google/cadvisor/pages/static" + auth "github.com/abbot/go-http-auth" ) var argIp = flag.String("listen_ip", "", "IP to listen on, defaults to all IPs") @@ -42,6 +43,11 @@ var maxProcs = flag.Int("max_procs", 0, "max number of CPUs that can be used sim var argDbDriver = flag.String("storage_driver", "", "storage driver to use. Data is always cached shortly in memory, this controls where data is pushed besides the local cache. Empty means none. Options are: (default), bigquery, and influxdb") var versionFlag = flag.Bool("version", false, "print cAdvisor version and exit") +var httpAuthFile = flag.String("http_auth_file", "", "Htpasswd auth file for the web UI") +var httpAuthRealm = flag.String("http_auth_realm", "localhost", "HTTP auth realm for the web UI") +var httpDigestFile = flag.String("http_digest_file", "", "HTTP digest file for the web UI") +var httpDigestRealm = flag.String("http_digest_realm", "localhost", "HTTP digest file for the web UI") + func main() { defer glog.Flush() flag.Parse() @@ -78,25 +84,42 @@ func main() { glog.Fatalf("Failed to register healthz handler: %s", err) } - // Handler for static content. - http.HandleFunc(static.StaticResource, func(w http.ResponseWriter, r *http.Request) { - err := static.HandleRequest(w, r.URL) - if err != nil { - fmt.Fprintf(w, "%s", err) - } - }) - // Register API handler. if err := api.RegisterHandlers(containerManager); err != nil { glog.Fatalf("Failed to register API handlers: %s", err) } - + // Redirect / to containers page. http.Handle("/", http.RedirectHandler(pages.ContainersPage, http.StatusTemporaryRedirect)) - if err := pages.RegisterHandlers(containerManager); err != nil { - glog.Fatalf("Failed to register pages handlers: %s", err) - } + var authenticated bool = false + // Setup the authenticator object + if *httpAuthFile!="" { + secrets := auth.HtpasswdFileProvider(*httpAuthFile) + authenticator := auth.NewBasicAuthenticator(*httpAuthRealm, secrets) + http.HandleFunc(static.StaticResource, authenticator.Wrap(staticHandler)) + if err := pages.RegisterHandlersBasic(containerManager,authenticator); err != nil { + glog.Fatalf("Failed to register pages handlers: %s", err) + } + authenticated = true + } + if *httpDigestFile!="" { + secrets := auth.HtdigestFileProvider(*httpDigestFile) + authenticator := auth.NewDigestAuthenticator(*httpDigestRealm, secrets) + http.HandleFunc(static.StaticResource, authenticator.Wrap(staticHandler)) + if err := pages.RegisterHandlersDigest(containerManager,authenticator); err != nil { + glog.Fatalf("Failed to register pages handlers: %s", err) + } + authenticated = true + } + + // Change handler based on authenticator initalization + if !authenticated { + http.HandleFunc(static.StaticResource, staticHandlerNoAuth) + if err := pages.RegisterHandlersBasic(containerManager,nil); err != nil { + glog.Fatalf("Failed to register pages handlers: %s", err) + } + } // Start the manager. if err := containerManager.Start(); err != nil { @@ -144,3 +167,17 @@ func installSignalHandler(containerManager manager.Manager) { os.Exit(0) }() } + +func staticHandlerNoAuth(w http.ResponseWriter, r *http.Request) { + err := static.HandleRequest(w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } +} + +func staticHandler(w http.ResponseWriter, r *auth.AuthenticatedRequest) { + err := static.HandleRequest(w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } +} diff --git a/pages/pages.go b/pages/pages.go index 743a3cdd..462710aa 100644 --- a/pages/pages.go +++ b/pages/pages.go @@ -8,6 +8,7 @@ import ( "github.com/golang/glog" "github.com/google/cadvisor/info" "github.com/google/cadvisor/manager" + auth "github.com/abbot/go-http-auth" ) var pageTemplate *template.Template @@ -44,27 +45,82 @@ func init() { } } +func containerHandlerNoAuth(containerManager manager.Manager) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := serveContainersPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + } +} + +func containerHandler(containerManager manager.Manager) auth.AuthenticatedHandlerFunc { + return func(w http.ResponseWriter, r *auth.AuthenticatedRequest) { + err := serveContainersPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + } +} + +func dockerHandlerNoAuth(containerManager manager.Manager) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + err := serveDockerPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + } +} + +func dockerHandler(containerManager manager.Manager) auth.AuthenticatedHandlerFunc { + return func(w http.ResponseWriter, r *auth.AuthenticatedRequest) { + err := serveDockerPage(containerManager, w, r.URL) + if err != nil { + fmt.Fprintf(w, "%s", err) + } + } +} + // Register http handlers for pages. -func RegisterHandlers(containerManager manager.Manager) error { +func RegisterHandlersDigest(containerManager manager.Manager,authenticator *auth.DigestAuth) error { + // Register the handler for the containers page. - http.HandleFunc(ContainersPage, func(w http.ResponseWriter, r *http.Request) { - err := serveContainersPage(containerManager, w, r.URL) - if err != nil { - fmt.Fprintf(w, "%s", err) - } - }) + if authenticator!=nil { + http.HandleFunc(ContainersPage, authenticator.Wrap(containerHandler(containerManager))) + } else { + http.HandleFunc(ContainersPage, containerHandlerNoAuth(containerManager)) + } // Register the handler for the docker page. - http.HandleFunc(DockerPage, func(w http.ResponseWriter, r *http.Request) { - err := serveDockerPage(containerManager, w, r.URL) - if err != nil { - fmt.Fprintf(w, "%s", err) - } - }) + if authenticator!=nil { + http.HandleFunc(DockerPage, authenticator.Wrap(dockerHandler(containerManager))) + } else { + http.HandleFunc(ContainersPage, dockerHandlerNoAuth(containerManager)) + } return nil } +func RegisterHandlersBasic(containerManager manager.Manager,authenticator *auth.BasicAuth) error { + + // Register the handler for the containers page. + if authenticator!=nil { + http.HandleFunc(ContainersPage, authenticator.Wrap(containerHandler(containerManager))) + } else { + http.HandleFunc(ContainersPage, containerHandlerNoAuth(containerManager)) + } + + // Register the handler for the docker page. + if authenticator!=nil { + http.HandleFunc(DockerPage, authenticator.Wrap(dockerHandler(containerManager))) + } else { + http.HandleFunc(DockerPage, dockerHandlerNoAuth(containerManager)) + } + + return nil +} + + func getContainerDisplayName(cont info.ContainerReference) string { // Pick the shortest name of the container as the display name. displayName := cont.Name diff --git a/test.htdigest b/test.htdigest new file mode 100644 index 00000000..73aae909 --- /dev/null +++ b/test.htdigest @@ -0,0 +1 @@ +admin:localhost:70f2631dded4ce5ad0ebbea5faa6ad6e diff --git a/test.htpasswd b/test.htpasswd new file mode 100644 index 00000000..b4dabcf5 --- /dev/null +++ b/test.htpasswd @@ -0,0 +1 @@ +admin:$apr1$WVO0Bsre$VrmWGDbcBV1fdAkvgQwdk0