implemented authorization
This commit is contained in:
@@ -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>`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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+"'")
|
||||
|
||||
@@ -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 {
|
||||
return UnsafeBytesToStr(b.Buf)
|
||||
}
|
||||
|
||||
func (b *StringBuilder) Remove(n int) *StringBuilder {
|
||||
l := b.Len()
|
||||
b.Buf = b.Buf[:l-min(n, l)]
|
||||
return b
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user