aboutsummaryrefslogtreecommitdiffstats
path: root/web
diff options
context:
space:
mode:
Diffstat (limited to 'web')
-rw-r--r--web/login_test.go68
-rw-r--r--web/static/login.html151
-rw-r--r--web/web.go2
3 files changed, 200 insertions, 21 deletions
diff --git a/web/login_test.go b/web/login_test.go
new file mode 100644
index 0000000..b48e7bc
--- /dev/null
+++ b/web/login_test.go
@@ -0,0 +1,68 @@
+package web
+
+import (
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "golang.org/x/crypto/bcrypt"
+
+ "adammathes.com/neko/config"
+)
+
+func TestCSRFLoginExclusion(t *testing.T) {
+ // Setup password
+ pw := "secret"
+ hashed, _ := bcrypt.GenerateFromPassword([]byte(pw), bcrypt.DefaultCost)
+ // We need to set the global config since loginHandler uses it
+ config.Config.DigestPassword = string(hashed) // wait, config stores the hashed password?
+ // let's check web.go loginHandler:
+ // if password == config.Config.DigestPassword {
+ // v, _ := bcrypt.GenerateFromPassword([]byte(password), 0)
+
+ // Ah, it compares PLAIN TEXT password with config.Config.DigestPassword.
+ // wait, line 113: if password == config.Config.DigestPassword {
+ // So config stores the PLAIN TEXT password? That's... odd, but okay for this test.
+ originalPw := config.Config.DigestPassword
+ defer func() { config.Config.DigestPassword = originalPw }()
+ config.Config.DigestPassword = pw
+
+ // Create a mux with login handler
+ mux := http.NewServeMux()
+ mux.HandleFunc("/login/", loginHandler)
+ mux.HandleFunc("/other", func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(http.StatusOK)
+ })
+
+ // Wrap with CSRF middleware
+ // We need to pass a pointer to Settings
+ handler := CSRFMiddleware(&config.Config, mux)
+
+ // Test 1: POST /login/ without CSRF token
+ // Should NOT return 403 Forbidden.
+ // Since we provide correct password, it should redirect (307)
+ req := httptest.NewRequest("POST", "/login/", strings.NewReader("password="+pw))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ rr := httptest.NewRecorder()
+
+ handler.ServeHTTP(rr, req)
+
+ if rr.Code == http.StatusForbidden {
+ t.Errorf("Expected /login/ POST to be allowed without CSRF token, got 403 Forbidden")
+ }
+ if rr.Code != http.StatusTemporaryRedirect {
+ t.Errorf("Expected 307 Redirect on successful login, got %d", rr.Code)
+ }
+
+ // Test 2: POST /other without CSRF token
+ // Should fail with 403 Forbidden
+ req2 := httptest.NewRequest("POST", "/other", nil)
+ rr2 := httptest.NewRecorder()
+
+ handler.ServeHTTP(rr2, req2)
+
+ if rr2.Code != http.StatusForbidden {
+ t.Errorf("Expected 403 Forbidden for POST /other, got %d", rr2.Code)
+ }
+}
diff --git a/web/static/login.html b/web/static/login.html
index ab3e781..c7d0a03 100644
--- a/web/static/login.html
+++ b/web/static/login.html
@@ -1,23 +1,134 @@
<!DOCTYPE html>
-<html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title>neko rss mode</title>
- <link rel="stylesheet" href="/static/style.css" />
- <meta name="viewport" content="width=device-width" />
- <meta name="viewport" content="initial-scale=1.0" />
- </head>
- <body>
-
- <form action="/login/" method="post">
- <h3>username</h3>
- <input type="text" name="username">
-
- <h3>password</h3>
- <input type="password" name="password">
- <input type="submit" name="login">
-
- </form>
-
- </body>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Neko Reader Login</title>
+ <style>
+ :root {
+ --bg-color: #f5f5f7;
+ --card-bg: #ffffff;
+ --text-color: #1d1d1f;
+ --border-color: #d2d2d7;
+ --primary-color: #0071e3;
+ --primary-hover: #0077ed;
+ --error-color: #ff3b30;
+ }
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100vh;
+ margin: 0;
+ padding: 20px;
+ }
+
+ .login-card {
+ background-color: var(--card-bg);
+ padding: 40px;
+ border-radius: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ width: 100%;
+ max-width: 360px;
+ text-align: center;
+ }
+
+ h1 {
+ font-size: 24px;
+ font-weight: 600;
+ margin-bottom: 24px;
+ margin-top: 0;
+ }
+
+ .input-group {
+ margin-bottom: 16px;
+ text-align: left;
+ }
+
+ label {
+ display: block;
+ margin-bottom: 8px;
+ font-size: 14px;
+ font-weight: 500;
+ color: #86868b;
+ }
+
+ input[type="text"],
+ input[type="password"] {
+ width: 100%;
+ padding: 12px;
+ border: 1px solid var(--border-color);
+ border-radius: 8px;
+ font-size: 16px;
+ box-sizing: border-box;
+ transition: border-color 0.2s ease;
+ }
+
+ input[type="text"]:focus,
+ input[type="password"]:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.2);
+ }
+
+ button {
+ width: 100%;
+ padding: 12px;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+ margin-top: 8px;
+ }
+
+ button:hover {
+ background-color: var(--primary-hover);
+ }
+
+ .footer {
+ margin-top: 24px;
+ font-size: 12px;
+ color: #86868b;
+ }
+
+ /* Dark mode support */
+ @media (prefers-color-scheme: dark) {
+ :root {
+ --bg-color: #000000;
+ --card-bg: #1c1c1e;
+ --text-color: #f5f5f7;
+ --border-color: #38383a;
+ --primary-color: #0a84ff;
+ --primary-hover: #409cff;
+ }
+ }
+ </style>
+</head>
+<body>
+ <div class="login-card">
+ <h1>Welcome Back</h1>
+ <form action="/login/" method="post">
+ <div class="input-group">
+ <label for="username">Username</label>
+ <input type="text" id="username" name="username" required autofocus autocomplete="username">
+ </div>
+ <div class="input-group">
+ <label for="password">Password</label>
+ <input type="password" id="password" name="password" required autocomplete="current-password">
+ </div>
+ <button type="submit">Sign In</button>
+ </form>
+ <div class="footer">
+ Neko RSS Reader
+ </div>
+ </div>
+</body>
</html>
diff --git a/web/web.go b/web/web.go
index a287bb9..d59d308 100644
--- a/web/web.go
+++ b/web/web.go
@@ -379,7 +379,7 @@ func CSRFMiddleware(cfg *config.Settings, next http.Handler) http.Handler {
token = cookie.Value
}
- if (r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete) && r.URL.Path != "/api/login" {
+ if (r.Method == http.MethodPost || r.Method == http.MethodPut || r.Method == http.MethodDelete) && r.URL.Path != "/api/login" && r.URL.Path != "/login/" {
headerToken := r.Header.Get("X-CSRF-Token")
if headerToken == "" || headerToken != token {
http.Error(w, "CSRF token mismatch", http.StatusForbidden)