You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
9.3 KiB
232 lines
9.3 KiB
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"net/http/cookiejar"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"webfilebrowser/internal/auth"
|
|
)
|
|
|
|
func TestUnauthenticatedRequestsNeverReachProtectedRoutes(t *testing.T) {
|
|
_, handler := newTestApplication(t)
|
|
|
|
for _, target := range []string{"/", "/files/media", "/download/media/report.pdf", "/stream/media/video.mp4", "/admin/users"} {
|
|
t.Run(target, func(t *testing.T) {
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, target, nil))
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Errorf("GET %s status = %d, want %d", target, response.Code, http.StatusSeeOther)
|
|
}
|
|
if location := response.Header().Get("Location"); location != loginPath {
|
|
t.Errorf("GET %s Location = %q, want %q", target, location, loginPath)
|
|
}
|
|
if strings.Contains(response.Body.String(), "File browser is being prepared") {
|
|
t.Errorf("GET %s returned protected content", target)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoginForcesPasswordChangeBeforeAccessingOtherRoutes(t *testing.T) {
|
|
t.Setenv(insecureCookiesEnvironment, "")
|
|
_, handler := newTestApplication(t)
|
|
|
|
loginResponse := postForm(handler, loginPath, url.Values{
|
|
"email": {auth.SeedAdminEmail},
|
|
"password": {auth.SeedAdminPassword},
|
|
}, nil)
|
|
if loginResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("POST %s status = %d, want %d", loginPath, loginResponse.Code, http.StatusSeeOther)
|
|
}
|
|
if location := loginResponse.Header().Get("Location"); location != changePasswordPath {
|
|
t.Fatalf("POST %s Location = %q, want %q", loginPath, location, changePasswordPath)
|
|
}
|
|
cookie := responseCookie(t, loginResponse, sessionCookieName)
|
|
if !cookie.HttpOnly || !cookie.Secure || cookie.SameSite != http.SameSiteLaxMode {
|
|
t.Errorf("session cookie security attributes = HttpOnly:%t Secure:%t SameSite:%v, want true:true:Lax", cookie.HttpOnly, cookie.Secure, cookie.SameSite)
|
|
}
|
|
if cookie.Expires.IsZero() || cookie.Path != "/" {
|
|
t.Errorf("default session cookie lifetime/path = Expires:%v Path:%q, want a persistent expiry and /", cookie.Expires, cookie.Path)
|
|
}
|
|
|
|
for _, target := range []string{"/", "/files/media", "/download/media/report.pdf", "/stream/media/video.mp4", "/admin/users"} {
|
|
response := httptest.NewRecorder()
|
|
request := httptest.NewRequest(http.MethodGet, target, nil)
|
|
request.AddCookie(cookie)
|
|
handler.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther || response.Header().Get("Location") != changePasswordPath {
|
|
t.Errorf("GET %s during forced change = (%d, %q), want (%d, %q)", target, response.Code, response.Header().Get("Location"), http.StatusSeeOther, changePasswordPath)
|
|
}
|
|
}
|
|
|
|
samePasswordResponse := postForm(handler, changePasswordPath, url.Values{
|
|
"current_password": {auth.SeedAdminPassword},
|
|
"new_password": {auth.SeedAdminPassword},
|
|
}, cookie)
|
|
if samePasswordResponse.Code != http.StatusBadRequest {
|
|
t.Errorf("POST %s with unchanged password status = %d, want %d", changePasswordPath, samePasswordResponse.Code, http.StatusBadRequest)
|
|
}
|
|
|
|
changedPasswordResponse := postForm(handler, changePasswordPath, url.Values{
|
|
"current_password": {auth.SeedAdminPassword},
|
|
"new_password": {"A-new-password-123!"},
|
|
}, cookie)
|
|
if changedPasswordResponse.Code != http.StatusSeeOther || changedPasswordResponse.Header().Get("Location") != "/" {
|
|
t.Errorf("POST %s status/location = (%d, %q), want (%d, %q)", changePasswordPath, changedPasswordResponse.Code, changedPasswordResponse.Header().Get("Location"), http.StatusSeeOther, "/")
|
|
}
|
|
|
|
rootResponse := httptest.NewRecorder()
|
|
rootRequest := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
rootRequest.AddCookie(cookie)
|
|
handler.ServeHTTP(rootResponse, rootRequest)
|
|
if rootResponse.Code != http.StatusOK {
|
|
t.Errorf("GET / after changing password status = %d, want %d", rootResponse.Code, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestInsecureCookieOptInSupportsPlainHTTPLogin(t *testing.T) {
|
|
t.Setenv(insecureCookiesEnvironment, "true")
|
|
_, handler := newTestApplication(t)
|
|
server := httptest.NewServer(handler)
|
|
t.Cleanup(server.Close)
|
|
|
|
jar, err := cookiejar.New(nil)
|
|
if err != nil {
|
|
t.Fatalf("cookiejar.New() error = %v", err)
|
|
}
|
|
client := &http.Client{
|
|
Jar: jar,
|
|
CheckRedirect: func(*http.Request, []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
}
|
|
loginResponse, err := client.PostForm(server.URL+loginPath, url.Values{
|
|
"email": {auth.SeedAdminEmail},
|
|
"password": {auth.SeedAdminPassword},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("POST %s error = %v", loginPath, err)
|
|
}
|
|
defer func() { _ = loginResponse.Body.Close() }()
|
|
if loginResponse.StatusCode != http.StatusSeeOther || loginResponse.Header.Get("Location") != changePasswordPath {
|
|
t.Fatalf("plain HTTP login = (%d, %q), want (%d, %q)", loginResponse.StatusCode, loginResponse.Header.Get("Location"), http.StatusSeeOther, changePasswordPath)
|
|
}
|
|
var sessionCookie *http.Cookie
|
|
for _, cookie := range loginResponse.Cookies() {
|
|
if cookie.Name == sessionCookieName {
|
|
sessionCookie = cookie
|
|
break
|
|
}
|
|
}
|
|
if sessionCookie == nil {
|
|
t.Fatalf("plain HTTP login did not set cookie %q", sessionCookieName)
|
|
}
|
|
if sessionCookie.Secure || !sessionCookie.HttpOnly || sessionCookie.SameSite != http.SameSiteLaxMode || sessionCookie.Expires.IsZero() || sessionCookie.Path != "/" {
|
|
t.Errorf("insecure session cookie attributes = Secure:%t HttpOnly:%t SameSite:%v Expires:%v Path:%q, want false:true:Lax:non-zero:/", sessionCookie.Secure, sessionCookie.HttpOnly, sessionCookie.SameSite, sessionCookie.Expires, sessionCookie.Path)
|
|
}
|
|
|
|
forcedChangeResponse, err := client.Get(server.URL + "/")
|
|
if err != nil {
|
|
t.Fatalf("GET / after plain HTTP login error = %v", err)
|
|
}
|
|
defer func() { _ = forcedChangeResponse.Body.Close() }()
|
|
if forcedChangeResponse.StatusCode != http.StatusSeeOther || forcedChangeResponse.Header.Get("Location") != changePasswordPath {
|
|
t.Fatalf("GET / after plain HTTP login = (%d, %q), want (%d, %q)", forcedChangeResponse.StatusCode, forcedChangeResponse.Header.Get("Location"), http.StatusSeeOther, changePasswordPath)
|
|
}
|
|
|
|
changeResponse, err := client.PostForm(server.URL+changePasswordPath, url.Values{
|
|
"current_password": {auth.SeedAdminPassword},
|
|
"new_password": {"A-new-password-123!"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("POST %s error = %v", changePasswordPath, err)
|
|
}
|
|
defer func() { _ = changeResponse.Body.Close() }()
|
|
if changeResponse.StatusCode != http.StatusSeeOther || changeResponse.Header.Get("Location") != "/" {
|
|
t.Fatalf("plain HTTP password change = (%d, %q), want (%d, %q)", changeResponse.StatusCode, changeResponse.Header.Get("Location"), http.StatusSeeOther, "/")
|
|
}
|
|
|
|
rootResponse, err := client.Get(server.URL + "/")
|
|
if err != nil {
|
|
t.Fatalf("GET / after plain HTTP password change error = %v", err)
|
|
}
|
|
defer func() { _ = rootResponse.Body.Close() }()
|
|
if rootResponse.StatusCode != http.StatusOK {
|
|
t.Errorf("GET / after plain HTTP password change status = %d, want %d", rootResponse.StatusCode, http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func TestLogoutDeletesTheServerSession(t *testing.T) {
|
|
_, handler := newTestApplication(t)
|
|
cookie := loginSeedAdmin(t, handler)
|
|
|
|
logoutResponse := postForm(handler, logoutPath, nil, cookie)
|
|
if logoutResponse.Code != http.StatusSeeOther || logoutResponse.Header().Get("Location") != loginPath {
|
|
t.Fatalf("POST %s status/location = (%d, %q), want (%d, %q)", logoutPath, logoutResponse.Code, logoutResponse.Header().Get("Location"), http.StatusSeeOther, loginPath)
|
|
}
|
|
|
|
protectedResponse := httptest.NewRecorder()
|
|
protectedRequest := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
protectedRequest.AddCookie(cookie)
|
|
handler.ServeHTTP(protectedResponse, protectedRequest)
|
|
if protectedResponse.Code != http.StatusSeeOther || protectedResponse.Header().Get("Location") != loginPath {
|
|
t.Errorf("GET / after logout status/location = (%d, %q), want (%d, %q)", protectedResponse.Code, protectedResponse.Header().Get("Location"), http.StatusSeeOther, loginPath)
|
|
}
|
|
}
|
|
|
|
func newTestApplication(t *testing.T) (*auth.Store, http.Handler) {
|
|
t.Helper()
|
|
store, err := auth.Open(filepath.Join(t.TempDir(), "webfilebrowser.db"), auth.Options{})
|
|
if err != nil {
|
|
t.Fatalf("Open() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
return store, New(store)
|
|
}
|
|
|
|
func loginSeedAdmin(t *testing.T, handler http.Handler) *http.Cookie {
|
|
t.Helper()
|
|
response := postForm(handler, loginPath, url.Values{
|
|
"email": {auth.SeedAdminEmail},
|
|
"password": {auth.SeedAdminPassword},
|
|
}, nil)
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("POST %s status = %d, want %d", loginPath, response.Code, http.StatusSeeOther)
|
|
}
|
|
return responseCookie(t, response, sessionCookieName)
|
|
}
|
|
|
|
func postForm(handler http.Handler, target string, values url.Values, cookie *http.Cookie) *httptest.ResponseRecorder {
|
|
var body io.Reader
|
|
if values != nil {
|
|
body = bytes.NewBufferString(values.Encode())
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, target, body)
|
|
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
if cookie != nil {
|
|
request.AddCookie(cookie)
|
|
}
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
return response
|
|
}
|
|
|
|
func responseCookie(t *testing.T, response *httptest.ResponseRecorder, name string) *http.Cookie {
|
|
t.Helper()
|
|
for _, cookie := range response.Result().Cookies() {
|
|
if cookie.Name == name {
|
|
return cookie
|
|
}
|
|
}
|
|
t.Fatalf("response did not set cookie %q", name)
|
|
return nil
|
|
}
|
|
|