diff --git a/configuration.go b/configuration.go index b1ce08464..5317e1c35 100644 --- a/configuration.go +++ b/configuration.go @@ -163,6 +163,7 @@ type EntryPoint struct { Address string TLS *TLS Redirect *Redirect + Auth *types.Auth } // Redirect configures a redirection of an entry point to another, or to an URL diff --git a/docs/toml.md b/docs/toml.md index 8cfded1ea..65ba71af2 100644 --- a/docs/toml.md +++ b/docs/toml.md @@ -110,7 +110,23 @@ # CertFile = "integration/fixtures/https/snitest.org.cert" # KeyFile = "integration/fixtures/https/snitest.org.key" # - +# To enable basic auth on an entrypoint +# with 2 user/pass: test:test and test2:test2 +# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.auth.basic] +# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# +# To enable digest auth on an entrypoint +# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 +# You can use htdigest to generate those ones +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.auth.basic] +# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] [entryPoints] [entryPoints.http] diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index ad4d9fa52..3e4465247 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -97,3 +97,21 @@ entryPoint = "https" backend = "backend2" rule = "Path:/test" ``` + +## Enable Basic authentication in an entrypoint + +With two user/pass: + +- `test`:`test` +- `test2`:`test2` + +Passwords are encoded in MD5: you can use htpasswd to generate those ones. + +``` +defaultEntryPoints = ["http"] +[entryPoints] + [entryPoints.http] + address = ":80" + [entryPoints.http.auth.basic] + users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +``` \ No newline at end of file diff --git a/glide.lock b/glide.lock index c0e265256..5a1fc5e4a 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 745d05424943c3345ff5fca5b121c6af3930f62fc13195d87d9fcce6686620ea -updated: 2016-07-22T15:14:53.608798979+02:00 +hash: 5cb432175705882247ac2cbf708c879fad8b287afe9e1e18f06dbce1e956acd2 +updated: 2016-07-28T18:20:42.864416381+02:00 imports: - name: github.com/abbot/go-http-auth version: cb4372376e1e00e9f6ab9ec142e029302c9e7140 @@ -36,7 +36,7 @@ imports: subpackages: - spew - name: github.com/docker/distribution - version: 2b72dd3927b2958160a2336f16145c0c421aa6a4 + version: 857d0f15c0a4d8037175642e0ca3660829551cb5 subpackages: - reference - digest @@ -141,7 +141,7 @@ imports: - lookup - version - name: github.com/docker/libkv - version: aabc039ad04deb721e234f99cd1b4aa28ac71a40 + version: 35d3e2084c650109e7bcc7282655b1bc8ba924ff subpackages: - store - store/boltdb @@ -157,7 +157,7 @@ imports: - name: github.com/go-check/check version: 4f90aeace3a26ad7021961c297b22c42160c7b25 - name: github.com/gogo/protobuf - version: 6abcf94fd4c97dcb423fdafd42fe9f96ca7e421b + version: e57a569e1882958f6b188cb42231d6db87701f2a subpackages: - proto - name: github.com/golang/glog @@ -169,7 +169,7 @@ imports: - name: github.com/gorilla/context version: aed02d124ae4a0e94fea4541c8effd05bf0c8296 - name: github.com/hashicorp/consul - version: fce7d75609a04eeb9d4bf41c8dc592aac18fc97d + version: 8a8271fd81cdaa1bbc20e4ced86531b90c7eaf79 subpackages: - api - name: github.com/hashicorp/go-cleanhttp @@ -220,13 +220,13 @@ imports: - name: github.com/miekg/dns version: 5d001d020961ae1c184f9f8152fdc73810481677 - name: github.com/mitchellh/mapstructure - version: 21a35fb16463dfb7c8eee579c65d995d95e64d1e + version: d2dd0262208475919e1a362f675cfc0e7c10e905 - name: github.com/moul/http2curl version: b1479103caacaa39319f75e7f57fc545287fca0d - name: github.com/ogier/pflag version: 45c278ab3607870051a2ea9040bb85fcb8557481 - name: github.com/opencontainers/runc - version: fb221651e5120cd287a76c7c1b6c877520fbd034 + version: bd1d3ac0480c5d3babac10dc32cff2886563219c subpackages: - libcontainer/user - name: github.com/parnurzeal/gorequest @@ -294,9 +294,11 @@ imports: subpackages: - acme - name: golang.org/x/crypto - version: 911fafb28f4ee7c7bd483539a6c96190bbbccc3f + version: d81fdb778bf2c40a91b24519d60cdc5767318829 subpackages: - ocsp + - bcrypt + - blowfish - name: golang.org/x/net version: b400c2eff1badec7022a8c8f5bea058b6315eed7 subpackages: diff --git a/glide.yaml b/glide.yaml index 70314fdaf..473085722 100644 --- a/glide.yaml +++ b/glide.yaml @@ -64,8 +64,6 @@ import: version: b2fad6198110326662e9e356a97199078a4a775c subpackages: - acme -- package: github.com/miekg/dns - version: 5d001d020961ae1c184f9f8152fdc73810481677 - package: golang.org/x/net subpackages: - context diff --git a/middlewares/authenticator.go b/middlewares/authenticator.go new file mode 100644 index 000000000..b5e6b0eb1 --- /dev/null +++ b/middlewares/authenticator.go @@ -0,0 +1,99 @@ +package middlewares + +import ( + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/abbot/go-http-auth" + "github.com/codegangsta/negroni" + "github.com/containous/traefik/types" + "net/http" + "strings" +) + +// Authenticator is a middleware that provides HTTP basic and digest authentication +type Authenticator struct { + handler negroni.Handler + users map[string]string +} + +// NewAuthenticator builds a new Autenticator given a config +func NewAuthenticator(authConfig *types.Auth) (*Authenticator, error) { + if authConfig == nil { + return nil, fmt.Errorf("Error creating Authenticator: auth is nil") + } + var err error + authenticator := Authenticator{} + if authConfig.Basic != nil { + authenticator.users, err = parserBasicUsers(authConfig.Basic.Users) + if err != nil { + return nil, err + } + basicAuth := auth.NewBasicAuthenticator("traefik", authenticator.secretBasic) + authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if username := basicAuth.CheckAuth(r); username == "" { + log.Debugf("Auth failed...") + basicAuth.RequireAuth(w, r) + } else { + next.ServeHTTP(w, r) + } + }) + } else if authConfig.Digest != nil { + authenticator.users, err = parserDigestUsers(authConfig.Digest.Users) + if err != nil { + return nil, err + } + digestAuth := auth.NewDigestAuthenticator("traefik", authenticator.secretDigest) + authenticator.handler = negroni.HandlerFunc(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + if username, _ := digestAuth.CheckAuth(r); username == "" { + digestAuth.RequireAuth(w, r) + } else { + next.ServeHTTP(w, r) + } + }) + } + return &authenticator, nil +} + +func parserBasicUsers(users types.Users) (map[string]string, error) { + userMap := make(map[string]string) + for _, user := range users { + split := strings.Split(user, ":") + if len(split) != 2 { + return nil, fmt.Errorf("Error parsing Authenticator user: %v", user) + } + userMap[split[0]] = split[1] + } + return userMap, nil +} + +func parserDigestUsers(users types.Users) (map[string]string, error) { + userMap := make(map[string]string) + for _, user := range users { + split := strings.Split(user, ":") + if len(split) != 3 { + return nil, fmt.Errorf("Error parsing Authenticator user: %v", user) + } + userMap[split[0]+":"+split[1]] = split[2] + } + return userMap, nil +} + +func (a *Authenticator) secretBasic(user, realm string) string { + if secret, ok := a.users[user]; ok { + return secret + } + log.Debugf("User not found: %s", user) + return "" +} + +func (a *Authenticator) secretDigest(user, realm string) string { + if secret, ok := a.users[user+":"+realm]; ok { + return secret + } + log.Debugf("User not found: %s:%s", user, realm) + return "" +} + +func (a *Authenticator) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) { + a.handler.ServeHTTP(rw, r, next) +} diff --git a/middlewares/authenticator_test.go b/middlewares/authenticator_test.go new file mode 100644 index 000000000..0d3eff016 --- /dev/null +++ b/middlewares/authenticator_test.go @@ -0,0 +1,103 @@ +package middlewares + +import ( + "fmt" + "github.com/codegangsta/negroni" + "github.com/containous/traefik/types" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" +) + +func TestBasicAuthFail(t *testing.T) { + authMiddleware, err := NewAuthenticator(&types.Auth{ + Basic: &types.Basic{ + Users: []string{"test"}, + }, + }) + assert.Contains(t, err.Error(), "Error parsing Authenticator user", "should contains") + + authMiddleware, err = NewAuthenticator(&types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:test"}, + }, + }) + assert.NoError(t, err, "there should be no error") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "traefik") + }) + n := negroni.New(authMiddleware) + n.UseHandler(handler) + ts := httptest.NewServer(n) + defer ts.Close() + + client := &http.Client{} + req, err := http.NewRequest("GET", ts.URL, nil) + req.SetBasicAuth("test", "test") + res, err := client.Do(req) + assert.NoError(t, err, "there should be no error") + assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "they should be equal") +} + +func TestBasicAuthSuccess(t *testing.T) { + authMiddleware, err := NewAuthenticator(&types.Auth{ + Basic: &types.Basic{ + Users: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"}, + }, + }) + assert.NoError(t, err, "there should be no error") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "traefik") + }) + n := negroni.New(authMiddleware) + n.UseHandler(handler) + ts := httptest.NewServer(n) + defer ts.Close() + + client := &http.Client{} + req, err := http.NewRequest("GET", ts.URL, nil) + req.SetBasicAuth("test", "test") + res, err := client.Do(req) + assert.NoError(t, err, "there should be no error") + assert.Equal(t, http.StatusOK, res.StatusCode, "they should be equal") + + body, err := ioutil.ReadAll(res.Body) + assert.NoError(t, err, "there should be no error") + assert.Equal(t, "traefik\n", string(body), "they should be equal") +} + +func TestDigestAuthFail(t *testing.T) { + authMiddleware, err := NewAuthenticator(&types.Auth{ + Digest: &types.Digest{ + Users: []string{"test"}, + }, + }) + assert.Contains(t, err.Error(), "Error parsing Authenticator user", "should contains") + + authMiddleware, err = NewAuthenticator(&types.Auth{ + Digest: &types.Digest{ + Users: []string{"test:traefik:test"}, + }, + }) + assert.NoError(t, err, "there should be no error") + assert.NotNil(t, authMiddleware, "this should not be nil") + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "traefik") + }) + n := negroni.New(authMiddleware) + n.UseHandler(handler) + ts := httptest.NewServer(n) + defer ts.Close() + + client := &http.Client{} + req, err := http.NewRequest("GET", ts.URL, nil) + req.SetBasicAuth("test", "test") + res, err := client.Do(req) + assert.NoError(t, err, "there should be no error") + assert.Equal(t, http.StatusUnauthorized, res.StatusCode, "they should be equal") +} diff --git a/script/deploy-pr.sh b/script/deploy-pr.sh index 7fd79407c..3bd4c1869 100755 --- a/script/deploy-pr.sh +++ b/script/deploy-pr.sh @@ -21,5 +21,7 @@ echo "Updating docker containous/traefik image..." docker login -e $DOCKER_EMAIL -u $DOCKER_USER -p $DOCKER_PASS docker tag containous/traefik containous/traefik:pr-${PR} docker push containous/traefik:pr-${PR} +docker tag containous/traefik containous/traefik:experimental +docker push containous/traefik:experimental echo "Deployed" diff --git a/script/deploy.sh b/script/deploy.sh index bf2d1bb7f..fe7277f5a 100755 --- a/script/deploy.sh +++ b/script/deploy.sh @@ -27,7 +27,7 @@ sudo chmod +x /usr/bin/ghr # github release and tag echo "Github release..." -ghr -t $GITHUB_TOKEN -u containous -r traefik --prerelease ${VERSION} dist/ +ghr -t $GITHUB_TOKEN -u containous -r traefik ${VERSION} dist/ # update docs.traefik.io echo "Generating and updating documentation..." diff --git a/server.go b/server.go index 4e83c0ac8..b224648fc 100644 --- a/server.go +++ b/server.go @@ -118,7 +118,15 @@ func (server *Server) Close() { func (server *Server) startHTTPServers() { server.serverEntryPoints = server.buildEntryPoints(server.globalConfiguration) for newServerEntryPointName, newServerEntryPoint := range server.serverEntryPoints { - newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, server.loggerMiddleware, metrics) + serverMiddlewares := []negroni.Handler{server.loggerMiddleware, metrics} + if server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth != nil { + authMiddleware, err := middlewares.NewAuthenticator(server.globalConfiguration.EntryPoints[newServerEntryPointName].Auth) + if err != nil { + log.Fatal("Error starting server: ", err) + } + serverMiddlewares = append(serverMiddlewares, authMiddleware) + } + newsrv, err := server.prepareServer(newServerEntryPointName, newServerEntryPoint.httpRouter, server.globalConfiguration.EntryPoints[newServerEntryPointName], nil, serverMiddlewares...) if err != nil { log.Fatal("Error preparing server: ", err) } diff --git a/traefik.sample.toml b/traefik.sample.toml index 55dd59cb5..78b3079d7 100644 --- a/traefik.sample.toml +++ b/traefik.sample.toml @@ -143,6 +143,25 @@ # [entryPoints.http.redirect] # regex = "^http://localhost/(.*)" # replacement = "http://mydomain/$1" +# +# To enable basic auth on an entrypoint +# with 2 user/pass: test:test and test2:test2 +# Passwords can be encoded in MD5, SHA1 and BCrypt: you can use htpasswd to generate those ones +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.auth.basic] +# users = ["test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"] +# +# To enable digest auth on an entrypoint +# with 2 user/realm/pass: test:traefik:test and test2:traefik:test2 +# You can use htdigest to generate those ones +# [entryPoints] +# [entryPoints.http] +# address = ":80" +# [entryPoints.http.auth.basic] +# users = ["test:traefik:a2688e031edb4be6a3797f3882655c05 ", "test2:traefik:518845800f9e2bfb1f1f740ec24f074e"] + # Enable retry sending request if network error # diff --git a/types/types.go b/types/types.go index e9c9b1c71..cc933ac79 100644 --- a/types/types.go +++ b/types/types.go @@ -183,3 +183,22 @@ func (cs *Constraints) SetValue(val interface{}) { func (cs *Constraints) Type() string { return fmt.Sprint("constraint") } + +// Auth holds authentication configuration (BASIC, DIGEST, users) +type Auth struct { + Basic *Basic + Digest *Digest +} + +// Users authentication users +type Users []string + +// Basic HTTP basic authentication +type Basic struct { + Users +} + +// Digest HTTP authentication +type Digest struct { + Users +}