Skip to content

Commit

Permalink
Add http basic authentication support
Browse files Browse the repository at this point in the history
  • Loading branch information
Wei-Ning Huang committed Apr 21, 2018
1 parent 428c58b commit 60f596b
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 22 deletions.
5 changes: 4 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ func main() {
"The address that the HTTP server will bind")
flagAdmin := flag.Bool("admin", false,
"If allowing admin level requests")
flagHTPassword := flag.String("htpasswd", "htpasswd",
"specify htpasswd file for HTTP basic auth")
flag.Parse()

ctx, err := context.Open(*flagData)
Expand All @@ -32,5 +34,6 @@ func main() {
}
defer ctx.Close()

log.Panic(web.ListenAndServe(*flagAddr, *flagAdmin, getVersion(), ctx))
log.Panic(web.ListenAndServe(
*flagAddr, *flagAdmin, getVersion(), *flagHTPassword, ctx))
}
185 changes: 185 additions & 0 deletions web/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2015 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package web

import (
"bufio"
"errors"
"fmt"
"io"
"log"
"net/http"
"os"
"regexp"
"strings"
"time"

"golang.org/x/crypto/bcrypt"
)

const (
maxFailCount = 5
blockDuration = 24 * time.Hour
)

func getRequestIP(r *http.Request) string {
idx := strings.LastIndex(r.RemoteAddr, ":")
return r.RemoteAddr[:idx]
}

type basicAuthHTTPHandlerDecorator struct {
auth *BasicAuth
handler http.Handler
handlerFunc http.HandlerFunc
blockedIps map[string]time.Time
failedCount map[string]int
}

func (a *basicAuthHTTPHandlerDecorator) Unauthorized(
w http.ResponseWriter, r *http.Request, msg string, record bool) {

// Record failure
if record {
ip := getRequestIP(r)
if _, ok := a.failedCount[ip]; !ok {
a.failedCount[ip] = 0
}
if ip != "127.0.0.1" {
// Only count for non-trusted IP.
a.failedCount[ip]++
}

log.Printf("BasicAuth: IP %s failed to login, count: %d\n", ip,
a.failedCount[ip])

if a.failedCount[ip] >= maxFailCount {
a.blockedIps[ip] = time.Now()
log.Printf("BasicAuth: IP %s is blocked\n", ip)
}
}

w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%s", a.auth.Realm))
http.Error(w, fmt.Sprintf("%s: %s", http.StatusText(http.StatusUnauthorized),
msg), http.StatusUnauthorized)
}

func (a *basicAuthHTTPHandlerDecorator) IsBlocked(r *http.Request) bool {
ip := getRequestIP(r)

if t, ok := a.blockedIps[ip]; ok {
if time.Now().Sub(t) < blockDuration {
log.Printf("BasicAuth: IP %s attempted to login, blocked\n", ip)
return true
}
// Unblock the user because of timeout
delete(a.failedCount, ip)
delete(a.blockedIps, ip)
}
return false
}

func (a *basicAuthHTTPHandlerDecorator) ResetFailCount(r *http.Request) {
ip := getRequestIP(r)
delete(a.failedCount, ip)
}

func (a *basicAuthHTTPHandlerDecorator) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if a.IsBlocked(r) {
http.Error(w, fmt.Sprintf("%s: %s", http.StatusText(http.StatusUnauthorized),
"too many retries"), http.StatusUnauthorized)
return
}

username, password, ok := r.BasicAuth()
if !ok {
a.Unauthorized(w, r, "authorization failed", false)
return
}

pass, err := a.auth.Authenticate(username, password)
if !pass {
a.Unauthorized(w, r, err.Error(), true)
return
}
a.ResetFailCount(r)

if a.handler != nil {
a.handler.ServeHTTP(w, r)
} else {
a.handlerFunc(w, r)
}
}

// BasicAuth is a class that provide WrapHandler and WrapHandlerFunc, which
// turns a http.Handler to a HTTP basic-a enabled http handler.
type BasicAuth struct {
Realm string
secrets map[string]string
}

// NewBasicAuth creates a BasicAuth object
func NewBasicAuth(realm, htpasswd string) *BasicAuth {
secrets := make(map[string]string)

f, err := os.Open(htpasswd)
if err != nil {
return &BasicAuth{realm, secrets}
}

b := bufio.NewReader(f)
for {
line, _, err := b.ReadLine()
if err == io.EOF {
break
}
if line[0] == '#' {
continue
}
parts := strings.Split(string(line), ":")
if len(parts) != 2 {
continue
}
matched, err := regexp.Match("^\\$2[ay]\\$.*$", []byte(parts[1]))
if err != nil {
panic(err)
}
if !matched {
log.Printf("BasicAuth: user %s: password encryption scheme "+
"not supported, ignored.\n", parts[0])
continue
}
secrets[parts[0]] = parts[1]
}

return &BasicAuth{realm, secrets}
}

// WrapHandler wraps an http.Hanlder and provide HTTP basic-a.
func (a *BasicAuth) WrapHandler(h http.Handler) http.Handler {
return &basicAuthHTTPHandlerDecorator{a, h, nil,
make(map[string]time.Time), make(map[string]int)}
}

// WrapHandlerFunc wraps an http.HanlderFunc and provide HTTP basic-a.
func (a *BasicAuth) WrapHandlerFunc(h http.HandlerFunc) http.Handler {
return &basicAuthHTTPHandlerDecorator{a, nil, h,
make(map[string]time.Time), make(map[string]int)}
}

// Authenticate authenticate an user with the provided user and passwd.
func (a *BasicAuth) Authenticate(user, passwd string) (bool, error) {
deniedError := errors.New("permission denied")

passwdHash, ok := a.secrets[user]
if !ok {
return false, deniedError
}

if bcrypt.CompareHashAndPassword([]byte(passwdHash), []byte(passwd)) != nil {
return false, deniedError
}

return true, nil
}
51 changes: 30 additions & 21 deletions web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,32 +81,41 @@ func getLinks(ctx *context.Context, w http.ResponseWriter, r *http.Request) {

// ListenAndServe sets up all web routes, binds the port and handles incoming
// web requests.
func ListenAndServe(addr string, admin bool, version string, ctx *context.Context) error {
func ListenAndServe(
addr string, admin bool, version string,
htpasswd string, ctx *context.Context) error {

mux := http.NewServeMux()

mux.HandleFunc("/api/url/", func(w http.ResponseWriter, r *http.Request) {
apiURL(ctx, w, r)
})
mux.HandleFunc("/api/urls/", func(w http.ResponseWriter, r *http.Request) {
apiURLs(ctx, w, r)
})
auth := NewBasicAuth("cobinhood", htpasswd)

mux.Handle("/api/url/", auth.WrapHandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
apiURL(ctx, w, r)
}))
mux.Handle("/api/urls/", auth.WrapHandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
apiURLs(ctx, w, r)
}))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
getDefault(ctx, w, r)
})
mux.HandleFunc("/edit/", func(w http.ResponseWriter, r *http.Request) {
p := parseName("/edit/", r.URL.Path)

// if this is a banned name, just redirect to the local URI. That'll show em.
if isBannedName(p) {
http.Redirect(w, r, fmt.Sprintf("/%s", p), http.StatusTemporaryRedirect)
return
}

serveAsset(w, r, "edit.html")
})
mux.HandleFunc("/links/", func(w http.ResponseWriter, r *http.Request) {
getLinks(ctx, w, r)
})
mux.Handle("/edit/", auth.WrapHandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
p := parseName("/edit/", r.URL.Path)

// if this is a banned name, just redirect to the local URI. That'll show em.
if isBannedName(p) {
http.Redirect(w, r, fmt.Sprintf("/%s", p), http.StatusTemporaryRedirect)
return
}

serveAsset(w, r, "edit.html")
}))
mux.Handle("/links/", auth.WrapHandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
getLinks(ctx, w, r)
}))
mux.HandleFunc("/s/", func(w http.ResponseWriter, r *http.Request) {
serveAsset(w, r, r.URL.Path[len("/s/"):])
})
Expand Down

0 comments on commit 60f596b

Please sign in to comment.