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