Skip to content

Commit 223d62f

Browse files
authored
Enabled HTTP Basic Auth (#16)
1 parent 569f6b8 commit 223d62f

File tree

7 files changed

+164
-0
lines changed

7 files changed

+164
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ The following configuration options are available:
151151
* `--cert` path to the ssl cert file (defaults to `cert.pem`)
152152
* `--key` path to the ssl key file (defaults to `key.pem`)
153153
* `--dir` directory path to serve (defaults to `.`, also configurable by `arg[0]`)
154+
* `--users` path to users file (defaults to `users.dat`); file should contain lines of username:password in plain text
154155

155156
## Development
156157

cmd/serve/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ func main() {
2020
flag.StringVar(&opt.CertFile, "cert", "cert.pem", "path to the ssl cert file")
2121
flag.StringVar(&opt.KeyFile, "key", "key.pem", "path to the ssl key file")
2222
flag.StringVar(&opt.Directory, "dir", "", "directory path to serve")
23+
flag.StringVar(&opt.UsersFile, "users", "users.dat", "path to users file")
2324
flag.Parse()
2425

2526
log := log.New(os.Stderr, "[serve] ", log.LstdFlags)

internal/commands/server.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
package commands
22

33
import (
4+
"bufio"
5+
"io"
46
"log"
57
"net"
68
"net/http"
9+
"os"
10+
"strings"
711
"time"
812

913
"github.com/syntaqx/serve"
@@ -30,16 +34,48 @@ func GetStdHTTPServer(addr string, h http.Handler) HTTPServer {
3034
}
3135
}
3236

37+
func GetAuthUsers(r io.Reader) map[string]string {
38+
users := make(map[string]string)
39+
40+
if r != nil {
41+
scanner := bufio.NewScanner(r)
42+
for scanner.Scan() {
43+
if line := strings.Split(scanner.Text(), ":"); len(line) == 2 { // use only if correct format
44+
users[line[0]] = line[1]
45+
}
46+
}
47+
48+
if err := scanner.Err(); err != nil {
49+
log.Fatalf("error occured during reading users file")
50+
}
51+
}
52+
53+
return users
54+
}
55+
3356
// Server implements the static http server command.
3457
func Server(log *log.Logger, opt config.Flags, dir string) error {
3558
fs := serve.NewFileServer(serve.Options{
3659
Directory: dir,
3760
})
3861

62+
// Authorization
63+
var f io.Reader
64+
if _, err := os.Stat(opt.UsersFile); !os.IsNotExist(err) {
65+
// Config file exists, load data
66+
f, err = os.Open(opt.UsersFile)
67+
if err != nil {
68+
log.Fatalf("unable to open users file %s", opt.UsersFile)
69+
}
70+
} else {
71+
log.Printf("%s does not exist, no authentication required", opt.UsersFile)
72+
}
73+
3974
fs.Use(
4075
middleware.Logger(log),
4176
middleware.Recover(),
4277
middleware.CORS(),
78+
middleware.Auth(GetAuthUsers(f)),
4379
)
4480

4581
addr := net.JoinHostPort(opt.Host, opt.Port)

internal/commands/server_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"log"
66
"net/http"
7+
"strings"
78
"testing"
89
"time"
910

@@ -74,3 +75,34 @@ func TestServerHTTPS(t *testing.T) {
7475

7576
getHTTPServerFunc = GetStdHTTPServer
7677
}
78+
79+
func TestGetAuthUsers(t *testing.T) {
80+
tests := []struct {
81+
input string
82+
output map[string]string
83+
}{
84+
{ // Single user
85+
"user1:pass1", map[string]string{
86+
"user1": "pass1",
87+
},
88+
},
89+
{ // Multiple users
90+
"user1:pass1\nuser2:pass2", map[string]string{
91+
"user1": "pass1",
92+
"user2": "pass2",
93+
},
94+
},
95+
{ // Empty file
96+
"", map[string]string{},
97+
},
98+
{ // Incorrect structure
99+
"user1:pass1:field1", map[string]string{},
100+
},
101+
}
102+
103+
for _, test := range tests {
104+
mockFile := strings.NewReader(test.input)
105+
assert.Equal(t, GetAuthUsers(mockFile), test.output)
106+
}
107+
108+
}

internal/config/flags.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type Flags struct {
1515
CertFile string
1616
KeyFile string
1717
Directory string
18+
UsersFile string
1819
}
1920

2021
// SanitizeDir allows a directory source to be set from multiple values. If any

internal/middleware/auth.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package middleware
2+
3+
import "net/http"
4+
5+
// Auth sets basic HTTP authorization
6+
func Auth(users map[string]string) func(next http.Handler) http.Handler {
7+
return func(next http.Handler) http.Handler {
8+
fn := func(w http.ResponseWriter, r *http.Request) {
9+
// Only require auth if we have any users
10+
if len(users) > 0 {
11+
authUser, authPass, ok := r.BasicAuth()
12+
if !ok {
13+
// No username/password received
14+
w.Header().Set("WWW-Authenticate", "Basic realm=Authenticate")
15+
w.WriteHeader(http.StatusUnauthorized)
16+
} else {
17+
if pass, ok := users[authUser]; ok {
18+
// User exists
19+
if pass == authPass {
20+
// Authentication successful
21+
next.ServeHTTP(w, r)
22+
} else {
23+
http.Error(w, "Incorrect login details", http.StatusUnauthorized)
24+
return
25+
}
26+
} else {
27+
http.Error(w, "Incorrect login details", http.StatusUnauthorized)
28+
return
29+
}
30+
}
31+
} else {
32+
next.ServeHTTP(w, r)
33+
}
34+
}
35+
36+
return http.HandlerFunc(fn)
37+
}
38+
}

internal/middleware/auth_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestAuth(t *testing.T) {
12+
t.Parallel()
13+
assert := assert.New(t)
14+
15+
req, err := http.NewRequest(http.MethodGet, "/", nil)
16+
assert.NoError(err)
17+
res := httptest.NewRecorder()
18+
19+
// No users
20+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
21+
Auth(nil)(testHandler).ServeHTTP(res, req)
22+
assert.Equal("", res.Header().Get("WWW-Authenticate"))
23+
24+
// Some users
25+
testUsers := map[string]string{
26+
"user1": "pass1",
27+
"user2": "pass2",
28+
}
29+
Auth(testUsers)(testHandler).ServeHTTP(res, req)
30+
assert.Equal("Basic realm=Authenticate", res.Header().Get("WWW-Authenticate"))
31+
assert.Equal(http.StatusUnauthorized, res.Result().StatusCode)
32+
33+
// Correct password
34+
// Recreate new environment
35+
testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
36+
req, err = http.NewRequest(http.MethodGet, "/", nil)
37+
assert.NoError(err)
38+
res = httptest.NewRecorder()
39+
40+
req.SetBasicAuth("user1", "pass1")
41+
Auth(testUsers)(testHandler).ServeHTTP(res, req)
42+
assert.Equal("", res.Header().Get("WWW-Authenticate"))
43+
assert.Equal(http.StatusOK, res.Result().StatusCode)
44+
45+
// Incorrect password
46+
// Recreate new environment
47+
testHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
48+
req, err = http.NewRequest(http.MethodGet, "/", nil)
49+
assert.NoError(err)
50+
res = httptest.NewRecorder()
51+
52+
req.SetBasicAuth("user1", "pass2")
53+
Auth(testUsers)(testHandler).ServeHTTP(res, req)
54+
assert.Equal(http.StatusUnauthorized, res.Result().StatusCode)
55+
}

0 commit comments

Comments
 (0)