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.