implemented authorization
This commit is contained in:
@@ -18,12 +18,16 @@ type listingRow struct {
|
|||||||
|
|
||||||
func (row *listingRow) genHtml(sb *sh.StringBuilder) {
|
func (row *listingRow) genHtml(sb *sh.StringBuilder) {
|
||||||
sb.AddS(`<tr>`)
|
sb.AddS(`<tr>`)
|
||||||
sb.AddS(`<td><a href="`).AddS(row.url).AddB('"')
|
sb.AddS(`<td><a href="`).AddS(row.url)
|
||||||
if !row.isDir {
|
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
|
// 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="size">`).AddS(row.size).AddS(`</td>`)
|
||||||
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
|
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
|
||||||
sb.AddS(`</tr>`)
|
sb.AddS(`</tr>`)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (conn *Connection) Start() {
|
|||||||
} else {
|
} else {
|
||||||
// valid request
|
// valid request
|
||||||
conn.log.Info(req.Method, " ", req.Url, " ", req.Protocol)
|
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)
|
res = conn.callback(req)
|
||||||
if req.HasUnreadBody() {
|
if req.HasUnreadBody() {
|
||||||
res = conn.CreateErrorResponse(req, true, ErrHasUnreadBody)
|
res = conn.CreateErrorResponse(req, true, ErrHasUnreadBody)
|
||||||
@@ -99,7 +99,7 @@ func (conn *Connection) Start() {
|
|||||||
|
|
||||||
conn.log.Info("sending response: ", res.Status,
|
conn.log.Info("sending response: ", res.Status,
|
||||||
" content-length: ", res.Headers["content-length"])
|
" 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)
|
err = conn.sendResponse(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.logError(err)
|
conn.logError(err)
|
||||||
|
|||||||
@@ -122,6 +122,17 @@ func (res *Response) WithBodyReader(body io.Reader,
|
|||||||
return res
|
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(
|
func (conn *Connection) CreateErrorResponse(
|
||||||
req *Request, wantsClose bool, httpErr *HttpError,
|
req *Request, wantsClose bool, httpErr *HttpError,
|
||||||
) *Response {
|
) *Response {
|
||||||
|
|||||||
@@ -8,6 +8,31 @@ import (
|
|||||||
|
|
||||||
const HeaderTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
|
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
|
// search in splitted parts of header value
|
||||||
func HeaderValueContains(sourceSplit []string, target string) bool {
|
func HeaderValueContains(sourceSplit []string, target string) bool {
|
||||||
for _, v := range sourceSplit {
|
for _, v := range sourceSplit {
|
||||||
|
|||||||
@@ -11,17 +11,26 @@ import (
|
|||||||
|
|
||||||
func (conn *TcpConnection) serveFileOrDir(req *http.Request) *http.Response {
|
func (conn *TcpConnection) serveFileOrDir(req *http.Request) *http.Response {
|
||||||
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(req.Url)
|
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(req.Url)
|
||||||
// TODO implement authorization
|
// handle authorization
|
||||||
_ = share
|
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 {
|
switch urlStatus {
|
||||||
case UrlFile:
|
case UrlFile:
|
||||||
return conn.serveFile(req, path)
|
return conn.serveFile(req, path)
|
||||||
case UrlDir:
|
case UrlDir:
|
||||||
return conn.serveDirListing(req, path)
|
return conn.serveDirListing(req, path)
|
||||||
case UrlNotFound:
|
case UrlNotFound:
|
||||||
return conn.createErrorResponse(req, false, http.StatusNotFound, path)
|
return conn.createErrorResponse(req, false, http.StatusNotFound, req.Url)
|
||||||
case UrlForbidden:
|
case UrlForbidden:
|
||||||
return conn.createErrorResponse(req, false, http.StatusForbidden, path)
|
return conn.createErrorResponse(req, false, http.StatusForbidden, req.Url)
|
||||||
case UrlIncorrect:
|
case UrlIncorrect:
|
||||||
return conn.createErrorResponse(req, false, http.StatusBadRequest,
|
return conn.createErrorResponse(req, false, http.StatusBadRequest,
|
||||||
"invalid url '"+req.Url+"'")
|
"invalid url '"+req.Url+"'")
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -121,3 +121,9 @@ func (b *StringBuilder) String() string {
|
|||||||
func (b *StringBuilder) StringUnsafe() string {
|
func (b *StringBuilder) StringUnsafe() string {
|
||||||
return UnsafeBytesToStr(b.Buf)
|
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
@@ -1,7 +1,7 @@
|
|||||||
# serveg config, copy to serveg.toml
|
# serveg config, copy to serveg.toml
|
||||||
# The file contains passwords: make it readable only by the user serveg runs as (chmod 600).
|
# The file contains passwords: make it readable only by the user serveg runs as (chmod 600).
|
||||||
address = "127.0.0.1:8080"
|
address = "127.0.0.1:8080"
|
||||||
max_upload = 4_294_967_296
|
max_upload = 4_294_967_296 # 4 GB
|
||||||
html_title = "serveg"
|
html_title = "serveg"
|
||||||
|
|
||||||
# Each [[share]] makes a directory available under an URL prefix.
|
# Each [[share]] makes a directory available under an URL prefix.
|
||||||
|
|||||||
Reference in New Issue
Block a user