implemented authorization

This commit is contained in:
Altair-sh
2026-11-04 17:08:55 +01:00
parent beff40ae8c
commit 7987c9e580
8 changed files with 124 additions and 11 deletions
+8 -4
View File
@@ -18,12 +18,16 @@ type listingRow struct {
func (row *listingRow) genHtml(sb *sh.StringBuilder) {
sb.AddS(`<tr>`)
sb.AddS(`<td><a href="`).AddS(row.url).AddB('"')
if !row.isDir {
sb.AddS(`<td><a href="`).AddS(row.url)
if row.isDir {
// add slash to directory url
// because browser remembers password for directory with trailing slash
sb.AddS(`/">`)
} else {
// tell browser to download file instead of opening in new tab
sb.AddS(" download")
sb.AddS(`" download>`)
}
sb.AddB('>').AddS(row.name).AddS(`</a></td>`)
sb.AddS(row.name).AddS(`</a></td>`)
sb.AddS(`<td class="size">`).AddS(row.size).AddS(`</td>`)
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
sb.AddS(`</tr>`)
+2 -2
View File
@@ -90,7 +90,7 @@ func (conn *Connection) Start() {
} else {
// valid request
conn.log.Info(req.Method, " ", req.Url, " ", req.Protocol)
conn.log.Trace("request headers: ", req.Headers)
conn.log.Trace("request headers: ", HeadersToString(req.Headers, SecretHeaders))
res = conn.callback(req)
if req.HasUnreadBody() {
res = conn.CreateErrorResponse(req, true, ErrHasUnreadBody)
@@ -99,7 +99,7 @@ func (conn *Connection) Start() {
conn.log.Info("sending response: ", res.Status,
" content-length: ", res.Headers["content-length"])
conn.log.Trace("response headers: ", res.Headers)
conn.log.Trace("response headers: ", HeadersToString(res.Headers, SecretHeaders))
err = conn.sendResponse(res)
if err != nil {
conn.logError(err)
+11
View File
@@ -122,6 +122,17 @@ func (res *Response) WithBodyReader(body io.Reader,
return res
}
func (res *Response) WithHeader(k, v string) *Response {
res.Headers[k] = v
return res
}
func (res *Response) WithHeaderI(k string, v int64) *Response {
return res.WithHeader(k, strconv.FormatInt(v, 10))
}
func (res *Response) WithHeaderU(k string, v uint64) *Response {
return res.WithHeader(k, strconv.FormatUint(v, 10))
}
func (conn *Connection) CreateErrorResponse(
req *Request, wantsClose bool, httpErr *HttpError,
) *Response {
+25
View File
@@ -8,6 +8,31 @@ import (
const HeaderTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
type SetStr map[string]struct{}
var SecretHeaders = SetStr{
"authorization": {},
}
func HeadersToString(headers HeadersMap, secretHeaders SetStr) string {
sb := sh.StringBuilder{Buf: make([]byte, 0, len(headers)*64)}
sb.AddS("{ ")
for k, v := range headers {
sb.AddB('"').AddS(k).AddS(`": "`)
if _, ok := secretHeaders[k]; ok {
sb.AddS("***")
} else {
sb.AddS(v)
}
sb.AddS(`", `)
}
if sb.Len() > 2 {
sb.Remove(2)
}
sb.AddS(" }")
return sb.StringUnsafe()
}
// search in splitted parts of header value
func HeaderValueContains(sourceSplit []string, target string) bool {
for _, v := range sourceSplit {
+13 -4
View File
@@ -11,17 +11,26 @@ import (
func (conn *TcpConnection) serveFileOrDir(req *http.Request) *http.Response {
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(req.Url)
// TODO implement authorization
_ = share
// handle authorization
if share != nil {
access := checkSharePasswords(share, req)
if access < AccessReadOnly {
return conn.createErrorResponse(req, false, http.StatusUnauthorized,
"authorization required").
WithHeader("www-authenticate",
`Basic realm="`+share.Url+`", charset="UTF-8"`)
}
}
switch urlStatus {
case UrlFile:
return conn.serveFile(req, path)
case UrlDir:
return conn.serveDirListing(req, path)
case UrlNotFound:
return conn.createErrorResponse(req, false, http.StatusNotFound, path)
return conn.createErrorResponse(req, false, http.StatusNotFound, req.Url)
case UrlForbidden:
return conn.createErrorResponse(req, false, http.StatusForbidden, path)
return conn.createErrorResponse(req, false, http.StatusForbidden, req.Url)
case UrlIncorrect:
return conn.createErrorResponse(req, false, http.StatusBadRequest,
"invalid url '"+req.Url+"'")
+58
View File
@@ -0,0 +1,58 @@
package server
import (
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"serveg/internal/http"
"strings"
)
type ShareAccess uint8
const (
AccessDenied ShareAccess = iota
AccessReadOnly
AccessReadWrite
)
// writing: share allows writing and client sent write password
// reading: share is public or client sent password (read or write)
func checkSharePasswords(share *SharedLocation, req *http.Request,
) ShareAccess {
reqPass := getBasicAuthPassword(req)
canWrite := share.WritePassword != "" &&
(reqPass != "" && passwordMatches(reqPass, share.WritePassword))
if canWrite {
return AccessReadWrite
}
canRead := share.ReadPassword == "" ||
(reqPass != "" && passwordMatches(reqPass, share.ReadPassword))
if canRead {
return AccessReadOnly
}
return AccessDenied
}
func getBasicAuthPassword(req *http.Request) string {
header := req.Headers["authorization"]
scheme, b64, ok := strings.Cut(header, " ")
if !ok || !strings.EqualFold(scheme, "Basic") {
return ""
}
decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64))
if err != nil {
return ""
}
// try get password from username:password
_, password, _ := strings.Cut(string(decoded), ":")
return password
}
func passwordMatches(got, want string) bool {
// hash first: ConstantTimeCompare returns early when lengths differ,
// which would leak the password length through timing
g := sha256.Sum256([]byte(got))
w := sha256.Sum256([]byte(want))
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
}
+6
View File
@@ -121,3 +121,9 @@ func (b *StringBuilder) String() string {
func (b *StringBuilder) StringUnsafe() string {
return UnsafeBytesToStr(b.Buf)
}
func (b *StringBuilder) Remove(n int) *StringBuilder {
l := b.Len()
b.Buf = b.Buf[:l-min(n, l)]
return b
}
+1 -1
View File
@@ -1,7 +1,7 @@
# serveg config, copy to serveg.toml
# The file contains passwords: make it readable only by the user serveg runs as (chmod 600).
address = "127.0.0.1:8080"
max_upload = 4_294_967_296
max_upload = 4_294_967_296 # 4 GB
html_title = "serveg"
# Each [[share]] makes a directory available under an URL prefix.