diff --git a/internal/html/common.go b/internal/html/common.go
new file mode 100644
index 0000000..3336d8c
--- /dev/null
+++ b/internal/html/common.go
@@ -0,0 +1,27 @@
+package html
+
+import sh "serveg/internal/string_helper"
+
+const DateTimeTZ = "2006-01-02 15:04:05 UTC-07"
+
+func htmlEscape(s string) string {
+ sb := sh.StringBuilder{Buf: make([]byte, 0, len(s))}
+ for i := 0; i < len(s); i++ {
+ b := s[i]
+ switch b {
+ case '&':
+ sb.AddS("&")
+ case '<':
+ sb.AddS("<")
+ case '>':
+ sb.AddS(">")
+ case '"':
+ sb.AddS(""")
+ case '\'':
+ sb.AddS("'")
+ default:
+ sb.AddB(b)
+ }
+ }
+ return sb.StringUnsafe()
+}
diff --git a/internal/server/html.go b/internal/html/html.go
similarity index 69%
rename from internal/server/html.go
rename to internal/html/html.go
index de6a47b..dc03f7f 100644
--- a/internal/server/html.go
+++ b/internal/html/html.go
@@ -1,4 +1,4 @@
-package server
+package html
import (
"os"
@@ -8,30 +8,6 @@ import (
sh "serveg/internal/string_helper"
)
-const DateTimeTZ = "2006-01-02 15:04:05 UTC-07"
-
-func htmlEscape(s string) string {
- sb := sh.StringBuilder{Buf: make([]byte, 0, len(s))}
- for i := 0; i < len(s); i++ {
- b := s[i]
- switch b {
- case '&':
- sb.AddS("&")
- case '<':
- sb.AddS("<")
- case '>':
- sb.AddS(">")
- case '"':
- sb.AddS(""")
- case '\'':
- sb.AddS("'")
- default:
- sb.AddB(b)
- }
- }
- return sb.StringUnsafe()
-}
-
type listingRow struct {
name string // html-escaped
url string // url-encoded
@@ -40,19 +16,20 @@ type listingRow struct {
isDir bool
}
-func (conn *connection) serveDirListing(dirPath string) *http.Response {
- htmlBytes, err := genDirListingHtml(&conn.log, dirPath,
- conn.req.Url, conn.srv.Conf.HtmlTitle)
- if err != nil {
- return conn.createErrorResponse(false, http.StatusInternalServerError, err.Error())
+func (row *listingRow) genHtml(sb *sh.StringBuilder) {
+ sb.AddS(`
`)
+ sb.AddS(`| ').AddS(row.name).AddS(` | `)
+ sb.AddS(``).AddS(row.size).AddS(` | `)
+ sb.AddS(``).AddS(row.mtime).AddS(` | `)
+ sb.AddS(`
`)
}
-func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle string) ([]byte, error) {
+func GenDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle string) ([]byte, error) {
dirEntries, err := os.ReadDir(dirPath)
if err != nil {
return nil, err
@@ -107,10 +84,10 @@ func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle str
sb.AddS(`">../`)
for i := 0; i < len(dirs); i++ {
- genListingRowHtml(&sb, &dirs[i])
+ dirs[i].genHtml(&sb)
}
for i := 0; i < len(files); i++ {
- genListingRowHtml(&sb, &files[i])
+ files[i].genHtml(&sb)
}
sb.AddS(`
@@ -125,16 +102,3 @@ func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle str
}
return sb.Buf, nil
}
-
-func genListingRowHtml(sb *sh.StringBuilder, row *listingRow) {
- sb.AddS(``)
- sb.AddS(`| ').AddS(row.name).AddS(` | `)
- sb.AddS(``).AddS(row.size).AddS(` | `)
- sb.AddS(``).AddS(row.mtime).AddS(` | `)
- sb.AddS(`
`)
-}
diff --git a/internal/http/Connection.go b/internal/http/Connection.go
index fa34a31..bdbb9c4 100644
--- a/internal/http/Connection.go
+++ b/internal/http/Connection.go
@@ -19,6 +19,9 @@ type Connection struct {
timeout time.Duration
r *bufio.Reader
wbuf sh.StringBuilder
+ // hidden to prevent using before ReadRequest/SendResponse
+ req Request // reset by ReadRequest
+ res Response // reset by CreateResponse and ReadRequest
}
func NewConnection(socket *net.TCPConn, timeout time.Duration) *Connection {
@@ -27,6 +30,13 @@ func NewConnection(socket *net.TCPConn, timeout time.Duration) *Connection {
timeout: timeout,
r: bufio.NewReaderSize(socket, maxHeaderBytes),
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
+ req: Request{
+ QueryParams: make(mapStrStr),
+ Headers: make(HeadersMap),
+ },
+ res: Response{
+ Headers: make(HeadersMap),
+ },
}
}
diff --git a/internal/http/Request.go b/internal/http/Request.go
index 03e7379..77523b3 100644
--- a/internal/http/Request.go
+++ b/internal/http/Request.go
@@ -1,14 +1,18 @@
package http
import (
+ "fmt"
+ "io"
"net/url"
sh "serveg/internal/string_helper"
+ "strconv"
"strings"
)
type Request struct {
- Method string
- Protocol string
+ needsReset bool
+ Method string
+ Protocol string
// url-encoded characters (%D0), query params (?v=123)
UrlRaw string
Url string
@@ -18,23 +22,45 @@ type Request struct {
Headers HeadersMap
// if true, don't close connection after response
WantsKeepAlive bool
+ // value from header or 0
+ ContentLength int64
+ // wrapper of conn.r
+ bodyReader io.LimitedReader
}
-// TODO: add request body support
-// TODO: check contentLength <= MaxUpload
-// TODO: use io.LimitReader(conn.r, contentLength)
-// Read request (body not supported)
-// Returns valid request pointer. If err != nil, req is partially parsed.
-// It doesn't enforce "Host" header because i don't need it in this project.
-//
-// if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
-// // Request is malformed, socket buffer contains unread garbage data.
-// // Send error code and close connection.
-// } else {
-// // something bad happened with the connection, just close it
-// }
+func (req *Request) reset() {
+ if !req.needsReset {
+ return
+ }
+
+ req.needsReset = false
+ req.Method = req.Method[:0]
+ req.Protocol = req.Protocol[:0]
+ req.UrlRaw = req.UrlRaw[:0]
+ req.Url = req.Url[:0]
+ clear(req.QueryParams)
+ clear(req.Headers)
+ req.WantsKeepAlive = false
+ req.ContentLength = 0
+ req.bodyReader.N = 0
+}
+
+// Returns valid request pointer. If err != nil, req is partially initialized.
+// Don't forget to read body with GetBodyReader() if HasUnreadBody() or ReadRequest will return error.
+// See [HttpError]
func (conn *Connection) ReadRequest() (req *Request, err error) {
- req = new(Request)
+ req = &conn.req
+ // TODO: make this check run automatically between ReadRequest and SendResponse.
+ // This can be done by moving reading loop from server/connection to http/Connection and handling requestr in callback (req) => res
+ if req.HasUnreadBody() {
+ err = ErrHasUnreadBody
+ return
+ }
+ req.reset()
+ req.needsReset = true
+ // TODO: remove after moving to callback loop
+ conn.res.reset()
+
var readN int
// METHOD /path HTTP/ver
if err = conn.readFirstLine(req, &readN); err != nil {
@@ -47,14 +73,25 @@ func (conn *Connection) ReadRequest() (req *Request, err error) {
return
}
- // This server doesn't accept request bodies.
- // Reject connection rather than desync the stream.
- contentLength := req.Headers["content-length"]
+ // check body size and initialize req.Body
transferEncoding := req.Headers["transfer-encoding"]
- if (contentLength != "" && contentLength != "0") ||
- transferEncoding != "" {
- err = NewHttpError(StatusBadRequest,
- "request bodies are not supported")
+ if transferEncoding != "" {
+ err = ErrTransferEncodingNotSupported
+ return
+ }
+ contentLengthStr := req.Headers["content-length"]
+ if contentLengthStr != "" {
+ var contentLength uint64
+ contentLength, err = strconv.ParseUint(contentLengthStr, 10, 64)
+ if err != nil {
+ err = NewHttpError(StatusBadRequest,
+ "can't parse header: content-length: "+contentLengthStr)
+ return
+ }
+ if contentLength > 0 {
+ req.ContentLength = int64(contentLength)
+ req.bodyReader.N = req.ContentLength
+ }
}
// set WantsKeepAlive
@@ -68,6 +105,29 @@ func (conn *Connection) ReadRequest() (req *Request, err error) {
return
}
+// if true CreateResponse will return error
+func (req *Request) HasUnreadBody() bool {
+ return req.bodyReader.N > 0
+}
+
+func (conn *Connection) GetBodyReader(maxBodyLen int64) (io.Reader, error) {
+ req := &conn.req
+ if req.ContentLength > maxBodyLen {
+ return nil, NewHttpError(StatusRequestEntityTooLarge,
+ fmt.Sprintf("content-length %d > maxBodyLen %d",
+ req.ContentLength, maxBodyLen))
+ }
+ if !req.HasUnreadBody() {
+ return nil, ErrBodyHasBeenAlreadyRead
+ }
+
+ if req.ContentLength != 0 {
+ // body reader with N=0 is a valid reader that just returns EOF
+ req.bodyReader.N = min(req.bodyReader.N, maxBodyLen)
+ }
+ return &req.bodyReader, nil
+}
+
func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
// read up to maxHeaderBytes
slice, notLineEnd, err := conn.r.ReadLine()
@@ -77,13 +137,12 @@ func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
}
if notLineEnd {
// buffer end was reached but no '\n' found
- return "", NewHttpError(StatusBadRequest, "line end not found")
+ return "", ErrLineEndNotFound
}
*readN += len(slice) + 1 // \n
if *readN > maxHeaderBytes {
- return "", NewHttpError(StatusRequestHeaderFieldsTooLarge,
- "headers too large")
+ return "", ErrHeadersTooLarge
}
return strings.TrimRight(string(slice), " \r\n"), nil
@@ -105,8 +164,7 @@ func (conn *Connection) readFirstLine(req *Request, readN *int) error {
parts := strings.Split(line, " ")
if len(parts) != 3 || !strings.HasPrefix(parts[2], "HTTP/") {
- return NewHttpError(StatusBadRequest,
- "malformed request line '"+line+"'")
+ return ErrNotHttpRequest
}
req.Method = parts[0]
// url-encoded characters (%D0), query params (?v=123)
@@ -114,8 +172,7 @@ func (conn *Connection) readFirstLine(req *Request, readN *int) error {
req.Protocol = parts[2]
if req.Protocol != "HTTP/1.0" && req.Protocol != "HTTP/1.1" {
- return NewHttpError(StatusHTTPVersionNotSupported,
- "request protocol not supported: "+req.Protocol)
+ return ErrProtocolNotSupported
}
u, err := url.ParseRequestURI(req.UrlRaw)
@@ -125,10 +182,8 @@ func (conn *Connection) readFirstLine(req *Request, readN *int) error {
req.Url = sh.ResolveSafePath(u.Path)
if req.Url == "" {
- return NewHttpError(StatusForbidden,
- "unsafe request path '"+u.Path+"'")
+ return ErrUnsafeRequestPath
}
- req.QueryParams = make(mapStrStr)
for key, values := range u.Query() {
req.QueryParams[key] = values[0]
}
@@ -137,7 +192,6 @@ func (conn *Connection) readFirstLine(req *Request, readN *int) error {
}
func (conn *Connection) readHeaders(req *Request, readN *int) error {
- req.Headers = make(HeadersMap)
for {
line, err := conn.readLineBelowLimit(readN)
if err != nil {
@@ -150,15 +204,13 @@ func (conn *Connection) readHeaders(req *Request, readN *int) error {
i := strings.IndexByte(line, ':')
if i < 1 {
- return NewHttpError(StatusBadRequest,
- "not a header line '"+line+"'")
+ return ErrLineNotHeader
}
key := line[:i]
firstB, lastB := key[0], key[i-1]
if firstB == ' ' || firstB == '\t' ||
lastB == ' ' || lastB == '\t' {
- return NewHttpError(StatusBadRequest,
- "header name must not start or end with space")
+ return ErrHeaderNameStartsOrEndsWithSpace
}
key = strings.ToLower(key)
diff --git a/internal/http/Response.go b/internal/http/Response.go
index cc37427..fb02b10 100644
--- a/internal/http/Response.go
+++ b/internal/http/Response.go
@@ -20,34 +20,58 @@ const (
)
type Response struct {
+ needsReset bool
Status StatusCode
- Headers HeadersMap // keys are in lower case
- omitBody bool // don't add body and content-* headers for HEAD request
- bodySize int64
- bodyType responseBodyType
- bodyBuf []byte // small body is sent in one write() call with headers
- bodyReader io.Reader // big body, sent as multiple write() calls to socket
+ // keys are in lower case
+ Headers HeadersMap
+ // don't add body and content-* headers for HEAD request
+ omitBody bool
+ bodySize int64
+ bodyType responseBodyType
+ // small body is sent in one write() call with headers
+ bodyBuf []byte
+ // big body, sent as multiple write() calls to socket
+ bodyReader io.Reader
}
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
-func (conn *Connection) CreateResponse(
- status StatusCode, req *Request, wantsClose bool,
-) *Response {
- res := &Response{
- Status: status,
- Headers: HeadersMap{
- "date": time.Now().UTC().Format(TimeFormat),
- // without it browser may remember response and not resend it
- "cache-control": "no-store",
- },
+func (res *Response) reset() {
+ if !res.needsReset {
+ return
}
- res.omitBody = req.Method == "HEAD"
- if !res.omitBody &&
- status >= 200 &&
- status != StatusNoContent &&
- status != StatusNotModified {
+ res.needsReset = false
+ res.Status = StatusNotInitialized
+ clear(res.Headers)
+ res.omitBody = false
+ res.bodySize = 0
+ res.bodyType = responseBodyEmpty
+ res.bodyBuf = res.bodyBuf[:0]
+ if closer, ok := res.bodyReader.(io.Closer); ok {
+ // body is owned by the response
+ _ = closer.Close()
+ }
+ res.bodyReader = nil
+}
+
+func (conn *Connection) CreateResponse(status StatusCode, req *Request, wantsClose bool) *Response {
+
+ res := &conn.res
+ res.reset()
+ res.needsReset = true
+
+ res.Status = status
+ res.Headers["date"] = time.Now().UTC().Format(TimeFormat)
+ // without it browser may remember response and not resend it
+ res.Headers["cache-control"] = "no-store"
+
+ res.omitBody = req.Method == "HEAD" ||
+ status < 200 ||
+ status == StatusNoContent ||
+ status == StatusResetContent ||
+ status == StatusNotModified
+ if !res.omitBody {
// content-length is mandatory for most responses
res.Headers["content-length"] = "0"
}
@@ -61,9 +85,9 @@ func (conn *Connection) CreateResponse(
return res
}
-func (res *Response) AttachBodyBuf(body []byte, contentType string) {
+func (res *Response) WithBodyBuf(body []byte, contentType string) *Response {
if res.omitBody {
- return
+ return res
}
if res.bodyType != responseBodyEmpty {
@@ -76,17 +100,18 @@ func (res *Response) AttachBodyBuf(body []byte, contentType string) {
lenStr := strconv.FormatUint(uint64(size), 10)
res.Headers["content-length"] = lenStr
res.Headers["content-type"] = contentType
+ return res
}
-func (res *Response) AttachBodyStr(body string, contentType string) {
- res.AttachBodyBuf(sh.UnsafeGetStrBytes(body), contentType)
+func (res *Response) WithBodyStr(body string, contentType string) *Response {
+ return res.WithBodyBuf(sh.UnsafeGetStrBytes(body), contentType)
}
-// body will be closed by SendResponse
-func (res *Response) AttachBodyReader(body io.Reader,
- size int64, contentType string) {
-
+// body will be closed by SendResponse() or reset()
+func (res *Response) WithBodyReader(body io.Reader,
+ size int64, contentType string,
+) *Response {
if res.omitBody {
- return
+ return res
}
if res.bodyType != responseBodyEmpty {
panic("response already has body")
@@ -97,18 +122,15 @@ func (res *Response) AttachBodyReader(body io.Reader,
lenStr := strconv.FormatUint(uint64(size), 10)
res.Headers["content-length"] = lenStr
res.Headers["content-type"] = contentType
+ return res
}
-var ErrInvalidHeader = errors.New("http: header key or value contains CR or LF")
-
func (conn *Connection) SendResponse(res *Response) (err error) {
- if res.bodyType == responseBodyReader {
- if closer, ok := res.bodyReader.(io.Closer); ok {
- // body is owned by the response
- defer func() {
- _ = closer.Close()
- }()
- }
+ if closer, ok := res.bodyReader.(io.Closer); ok {
+ // body is owned by the response
+ defer func() {
+ _ = closer.Close()
+ }()
}
sb := &conn.wbuf
@@ -120,7 +142,7 @@ func (conn *Connection) SendResponse(res *Response) (err error) {
for k, v := range res.Headers {
// prevent response splitting / header injection
if strings.ContainsAny(k, "\r\n:") || strings.ContainsAny(v, "\r\n") {
- return ErrInvalidHeader
+ return ErrHeaderKeyOrValueHasCRLF
}
sb.AddS(k).AddS(": ").AddS(v).AddS("\r\n")
}
@@ -141,7 +163,7 @@ func (conn *Connection) SendResponse(res *Response) (err error) {
}
// Write exactly bodySize bytes from body.
// It prevents session corruption if body size changed since
- // AttachBodyReader was called.
+ // WithBodyReader was called.
remaining := res.bodySize
for remaining > 0 {
err = conn.socket.SetWriteDeadline(time.Now().Add(conn.timeout))
@@ -172,9 +194,6 @@ func (conn *Connection) SendResponse(res *Response) (err error) {
func (conn *Connection) CreateErrorResponse(
req *Request, wantsClose bool, httpErr *HttpError,
) *Response {
-
- res := conn.CreateResponse(httpErr.Status, req, wantsClose)
- // attach error message if method allows
- res.AttachBodyStr(httpErr.Error(), TextPlain)
- return res
+ return conn.CreateResponse(httpErr.Status, req, wantsClose).
+ WithBodyStr(httpErr.Error(), TextPlain)
}
diff --git a/internal/http/StatusCodes.go b/internal/http/StatusCodes.go
index cb8c410..7ae9747 100644
--- a/internal/http/StatusCodes.go
+++ b/internal/http/StatusCodes.go
@@ -19,6 +19,7 @@ func (c StatusCode) String() string {
// HTTP status codes as registered with IANA.
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
const (
+ StatusNotInitialized StatusCode = 0 // for Response.reset
StatusContinue StatusCode = 100 // RFC 9110, 15.2.1
StatusSwitchingProtocols StatusCode = 101 // RFC 9110, 15.2.2
StatusProcessing StatusCode = 102 // RFC 2518, 10.1
diff --git a/internal/http/errors.go b/internal/http/errors.go
index 788424e..c490d9c 100644
--- a/internal/http/errors.go
+++ b/internal/http/errors.go
@@ -1,5 +1,16 @@
package http
+import "errors"
+
+// Usage:
+//
+// err := conn.ReadRequest() / CreateResponse()
+// if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
+// // Request is malformed, socket buffer contains unread garbage data.
+// // Send error code and close connection.
+// } else {
+// // something bad happened with the connection, just close it
+// }
type HttpError struct {
Status StatusCode
Msg string
@@ -12,3 +23,35 @@ func (e *HttpError) Error() string {
func NewHttpError(code StatusCode, msg string) *HttpError {
return &HttpError{Status: code, Msg: msg}
}
+
+var ErrHasUnreadBody = NewHttpError(StatusRequestEntityTooLarge,
+ "can't read new request, body of previous request wasn't read")
+
+var ErrBodyHasBeenAlreadyRead = errors.New(
+ "request body has already been read")
+
+var ErrTransferEncodingNotSupported = NewHttpError(StatusBadRequest,
+ "header transfer-encoding is not supported")
+
+var ErrLineEndNotFound = NewHttpError(StatusBadRequest,
+ "line end not found")
+
+var ErrHeadersTooLarge = NewHttpError(StatusRequestHeaderFieldsTooLarge,
+ "headers too large")
+
+var ErrNotHttpRequest = NewHttpError(StatusBadRequest,
+ "not HTTP request")
+
+var ErrProtocolNotSupported = NewHttpError(StatusHTTPVersionNotSupported,
+ "request protocol not supported")
+
+var ErrUnsafeRequestPath = NewHttpError(StatusForbidden,
+ "unsafe request path")
+
+var ErrLineNotHeader = NewHttpError(StatusBadRequest,
+ "not a header line")
+
+var ErrHeaderNameStartsOrEndsWithSpace = NewHttpError(StatusBadRequest,
+ "header name must not start or end with space")
+
+var ErrHeaderKeyOrValueHasCRLF = errors.New("response header key or value contains CR or LF")
diff --git a/internal/server/Server.go b/internal/server/Server.go
index bc1c88f..e613ff1 100644
--- a/internal/server/Server.go
+++ b/internal/server/Server.go
@@ -45,7 +45,7 @@ func (srv *Server) serve() {
srv.logger.Info("listener socket closed")
return
}
- srv.logger.Warn(err)
+ srv.logger.Error(err)
continue
}
conn := newConnection(srv, i, accepted.(*net.TCPConn))
diff --git a/internal/server/SharedLocation.go b/internal/server/SharedLocation.go
index a54ed5f..c5674b6 100644
--- a/internal/server/SharedLocation.go
+++ b/internal/server/SharedLocation.go
@@ -19,9 +19,13 @@ type SharedLocation struct {
WritePassword string `toml:"write_password"`
}
+var ErrSharePathEmpty = errors.New("share.path is empty")
+var ErrShareUrlEmpty = errors.New("share.url is empty")
+var ErrShareUrlDoesntStartWithSlash = errors.New("share.url doesn't start with slash")
+
func (sh *SharedLocation) normalize(configDir string) error {
if sh.Path == "" {
- return errors.New("share.path is empty")
+ return ErrSharePathEmpty
}
// get absolute path for configDir
configDirAbs, err := filepath.Abs(configDir)
@@ -42,10 +46,10 @@ func (sh *SharedLocation) normalize(configDir string) error {
sh.Path = filepath.Join(configDirAbs, shPathOsSep)
if sh.Url == "" {
- return errors.New("share.url is empty")
+ return ErrShareUrlEmpty
}
if sh.Url[0] != '/' {
- return errors.New("share.url doesn't start with slash: '" + sh.Url + "'")
+ return ErrShareUrlDoesntStartWithSlash
}
// remove trailing slash
sh.Url = strings.TrimRight(sh.Url, "/")
diff --git a/internal/server/connection.go b/internal/server/connection.go
index bde7a56..8ec69d4 100644
--- a/internal/server/connection.go
+++ b/internal/server/connection.go
@@ -7,6 +7,7 @@ import (
"net"
"os"
"path"
+ "serveg/internal/html"
"serveg/internal/http"
"serveg/internal/log"
"strconv"
@@ -52,12 +53,11 @@ func (conn *connection) handle() {
conn.req, err = conn.http.ReadRequest()
if err != nil {
// invalid request
- conn.logConnError(err)
if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
- conn.log.Info("sending error response")
+ conn.log.Warn(err)
res = conn.http.CreateErrorResponse(conn.req, true, httpErr)
} else {
- conn.log.Info("lingering close")
+ conn.logError(err)
conn.lingeringClose()
return
}
@@ -68,31 +68,31 @@ func (conn *connection) handle() {
switch conn.req.Method {
case "GET", "HEAD":
res = conn.serveFileOrDir()
- case "PUT":
+ case "PUT", "OPTIONS":
res = conn.handleUpload()
default:
res = conn.createErrorResponse(false, http.StatusMethodNotAllowed, conn.req.Method)
}
}
+
conn.log.Info("sending response: ", res.Status,
" content-length: ", res.Headers["content-length"])
conn.log.Trace("response headers: ", res.Headers)
err = conn.http.SendResponse(res)
if err != nil {
- conn.logConnError(err)
+ conn.logError(err)
return
}
// it is set by CreateResponse
if res.Headers["connection"] == "close" {
- conn.log.Info("lingering close")
conn.lingeringClose()
return
}
}
}
-func (conn *connection) logConnError(err error) {
+func (conn *connection) logError(err error) {
if errors.Is(err, os.ErrDeadlineExceeded) {
conn.log.Info("time out")
} else if errors.Is(err, io.EOF) ||
@@ -101,7 +101,7 @@ func (conn *connection) logConnError(err error) {
conn.log.Info("closed by client")
} else {
- conn.log.Warn(err)
+ conn.log.Error(err)
}
}
@@ -109,6 +109,7 @@ func (conn *connection) logConnError(err error) {
// so close() doesn't send RST and destroy the response before it's read.
// Limits: 500 ms, 32 KB
func (conn *connection) lingeringClose() {
+ conn.log.Info("lingering close")
if conn.socket.CloseWrite() != nil {
return
}
@@ -116,6 +117,12 @@ func (conn *connection) lingeringClose() {
_, _ = io.Copy(io.Discard, io.LimitReader(conn.socket, 32*1024))
}
+func (conn *connection) createErrorResponse(
+ wantsClose bool, status http.StatusCode, msg string,
+) *http.Response {
+ return conn.http.CreateErrorResponse(conn.req, wantsClose, http.NewHttpError(status, msg))
+}
+
func (conn *connection) serveFileOrDir() *http.Response {
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(conn.req.Url)
// TODO implement authorization
@@ -137,9 +144,15 @@ func (conn *connection) serveFileOrDir() *http.Response {
}
}
-func (conn *connection) createErrorResponse(wantsClose bool, status http.StatusCode, msg string,
-) *http.Response {
- return conn.http.CreateErrorResponse(conn.req, wantsClose, http.NewHttpError(status, msg))
+func (conn *connection) serveDirListing(dirPath string) *http.Response {
+ htmlBytes, err := html.GenDirListingHtml(&conn.log, dirPath,
+ conn.req.Url, conn.srv.Conf.HtmlTitle)
+ if err != nil {
+ return conn.createErrorResponse(false, http.StatusInternalServerError, err.Error())
+ }
+
+ return conn.http.CreateResponse(http.StatusOK, conn.req, false).
+ WithBodyBuf(htmlBytes, http.TextHtml)
}
func (conn *connection) serveFile(filePath string) *http.Response {
@@ -176,7 +189,7 @@ func (conn *connection) serveFile(filePath string) *http.Response {
if conn.req.Method != "HEAD" {
// file will be closed by SendResponse()
fileOwnershipTransferred = true
- res.AttachBodyReader(f, size, http.GetMimeType(fileName))
+ res.WithBodyReader(f, size, http.GetMimeType(fileName))
}
// Tell browser to not use its own caching, remember last-modified
// On next request browser will send if-modified-since and let server check if file changed.