added request body reader
This commit is contained in:
@@ -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()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package server
|
package html
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
@@ -8,30 +8,6 @@ import (
|
|||||||
sh "serveg/internal/string_helper"
|
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 {
|
type listingRow struct {
|
||||||
name string // html-escaped
|
name string // html-escaped
|
||||||
url string // url-encoded
|
url string // url-encoded
|
||||||
@@ -40,19 +16,20 @@ type listingRow struct {
|
|||||||
isDir bool
|
isDir bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *connection) serveDirListing(dirPath string) *http.Response {
|
func (row *listingRow) genHtml(sb *sh.StringBuilder) {
|
||||||
htmlBytes, err := genDirListingHtml(&conn.log, dirPath,
|
sb.AddS(`<tr>`)
|
||||||
conn.req.Url, conn.srv.Conf.HtmlTitle)
|
sb.AddS(`<td><a href="`).AddS(row.url).AddB('"')
|
||||||
if err != nil {
|
if !row.isDir {
|
||||||
return conn.createErrorResponse(false, http.StatusInternalServerError, err.Error())
|
// tell browser to download file instead of opening in new tab
|
||||||
|
sb.AddS(" download")
|
||||||
}
|
}
|
||||||
|
sb.AddB('>').AddS(row.name).AddS(`</a></td>`)
|
||||||
res := conn.http.CreateResponse(http.StatusOK, conn.req, false)
|
sb.AddS(`<td class="size">`).AddS(row.size).AddS(`</td>`)
|
||||||
res.AttachBodyBuf(htmlBytes, http.TextHtml)
|
sb.AddS(`<td class="date">`).AddS(row.mtime).AddS(`</td>`)
|
||||||
return res
|
sb.AddS(`</tr>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
dirEntries, err := os.ReadDir(dirPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -107,10 +84,10 @@ func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle str
|
|||||||
sb.AddS(`">../</a></td></tr>`)
|
sb.AddS(`">../</a></td></tr>`)
|
||||||
|
|
||||||
for i := 0; i < len(dirs); i++ {
|
for i := 0; i < len(dirs); i++ {
|
||||||
genListingRowHtml(&sb, &dirs[i])
|
dirs[i].genHtml(&sb)
|
||||||
}
|
}
|
||||||
for i := 0; i < len(files); i++ {
|
for i := 0; i < len(files); i++ {
|
||||||
genListingRowHtml(&sb, &files[i])
|
files[i].genHtml(&sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AddS(`
|
sb.AddS(`
|
||||||
@@ -125,16 +102,3 @@ func genDirListingHtml(logger *log.LoggerContext, dirPath, reqUrl, pageTitle str
|
|||||||
}
|
}
|
||||||
return sb.Buf, nil
|
return sb.Buf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func genListingRowHtml(sb *sh.StringBuilder, row *listingRow) {
|
|
||||||
sb.AddS(`<tr>`)
|
|
||||||
sb.AddS(`<td><a href="`).AddS(row.url).AddB('"')
|
|
||||||
if !row.isDir {
|
|
||||||
// tell browser to download file instead of opening in new tab
|
|
||||||
sb.AddS(" download")
|
|
||||||
}
|
|
||||||
sb.AddB('>').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>`)
|
|
||||||
}
|
|
||||||
@@ -19,6 +19,9 @@ type Connection struct {
|
|||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
r *bufio.Reader
|
r *bufio.Reader
|
||||||
wbuf sh.StringBuilder
|
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 {
|
func NewConnection(socket *net.TCPConn, timeout time.Duration) *Connection {
|
||||||
@@ -27,6 +30,13 @@ func NewConnection(socket *net.TCPConn, timeout time.Duration) *Connection {
|
|||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
r: bufio.NewReaderSize(socket, maxHeaderBytes),
|
r: bufio.NewReaderSize(socket, maxHeaderBytes),
|
||||||
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
||||||
|
req: Request{
|
||||||
|
QueryParams: make(mapStrStr),
|
||||||
|
Headers: make(HeadersMap),
|
||||||
|
},
|
||||||
|
res: Response{
|
||||||
|
Headers: make(HeadersMap),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+87
-35
@@ -1,12 +1,16 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
sh "serveg/internal/string_helper"
|
sh "serveg/internal/string_helper"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Request struct {
|
type Request struct {
|
||||||
|
needsReset bool
|
||||||
Method string
|
Method string
|
||||||
Protocol string
|
Protocol string
|
||||||
// url-encoded characters (%D0), query params (?v=123)
|
// url-encoded characters (%D0), query params (?v=123)
|
||||||
@@ -18,23 +22,45 @@ type Request struct {
|
|||||||
Headers HeadersMap
|
Headers HeadersMap
|
||||||
// if true, don't close connection after response
|
// if true, don't close connection after response
|
||||||
WantsKeepAlive bool
|
WantsKeepAlive bool
|
||||||
|
// value from header or 0
|
||||||
|
ContentLength int64
|
||||||
|
// wrapper of conn.r
|
||||||
|
bodyReader io.LimitedReader
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add request body support
|
func (req *Request) reset() {
|
||||||
// TODO: check contentLength <= MaxUpload
|
if !req.needsReset {
|
||||||
// TODO: use io.LimitReader(conn.r, contentLength)
|
return
|
||||||
// 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.
|
req.needsReset = false
|
||||||
//
|
req.Method = req.Method[:0]
|
||||||
// if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
|
req.Protocol = req.Protocol[:0]
|
||||||
// // Request is malformed, socket buffer contains unread garbage data.
|
req.UrlRaw = req.UrlRaw[:0]
|
||||||
// // Send error code and close connection.
|
req.Url = req.Url[:0]
|
||||||
// } else {
|
clear(req.QueryParams)
|
||||||
// // something bad happened with the connection, just close it
|
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) {
|
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
|
var readN int
|
||||||
// METHOD /path HTTP/ver
|
// METHOD /path HTTP/ver
|
||||||
if err = conn.readFirstLine(req, &readN); err != nil {
|
if err = conn.readFirstLine(req, &readN); err != nil {
|
||||||
@@ -47,14 +73,25 @@ func (conn *Connection) ReadRequest() (req *Request, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// This server doesn't accept request bodies.
|
// check body size and initialize req.Body
|
||||||
// Reject connection rather than desync the stream.
|
|
||||||
contentLength := req.Headers["content-length"]
|
|
||||||
transferEncoding := req.Headers["transfer-encoding"]
|
transferEncoding := req.Headers["transfer-encoding"]
|
||||||
if (contentLength != "" && contentLength != "0") ||
|
if transferEncoding != "" {
|
||||||
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,
|
err = NewHttpError(StatusBadRequest,
|
||||||
"request bodies are not supported")
|
"can't parse header: content-length: "+contentLengthStr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if contentLength > 0 {
|
||||||
|
req.ContentLength = int64(contentLength)
|
||||||
|
req.bodyReader.N = req.ContentLength
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// set WantsKeepAlive
|
// set WantsKeepAlive
|
||||||
@@ -68,6 +105,29 @@ func (conn *Connection) ReadRequest() (req *Request, err error) {
|
|||||||
return
|
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) {
|
func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
|
||||||
// read up to maxHeaderBytes
|
// read up to maxHeaderBytes
|
||||||
slice, notLineEnd, err := conn.r.ReadLine()
|
slice, notLineEnd, err := conn.r.ReadLine()
|
||||||
@@ -77,13 +137,12 @@ func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
|
|||||||
}
|
}
|
||||||
if notLineEnd {
|
if notLineEnd {
|
||||||
// buffer end was reached but no '\n' found
|
// buffer end was reached but no '\n' found
|
||||||
return "", NewHttpError(StatusBadRequest, "line end not found")
|
return "", ErrLineEndNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
*readN += len(slice) + 1 // \n
|
*readN += len(slice) + 1 // \n
|
||||||
if *readN > maxHeaderBytes {
|
if *readN > maxHeaderBytes {
|
||||||
return "", NewHttpError(StatusRequestHeaderFieldsTooLarge,
|
return "", ErrHeadersTooLarge
|
||||||
"headers too large")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.TrimRight(string(slice), " \r\n"), nil
|
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, " ")
|
parts := strings.Split(line, " ")
|
||||||
if len(parts) != 3 || !strings.HasPrefix(parts[2], "HTTP/") {
|
if len(parts) != 3 || !strings.HasPrefix(parts[2], "HTTP/") {
|
||||||
return NewHttpError(StatusBadRequest,
|
return ErrNotHttpRequest
|
||||||
"malformed request line '"+line+"'")
|
|
||||||
}
|
}
|
||||||
req.Method = parts[0]
|
req.Method = parts[0]
|
||||||
// url-encoded characters (%D0), query params (?v=123)
|
// 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]
|
req.Protocol = parts[2]
|
||||||
|
|
||||||
if req.Protocol != "HTTP/1.0" && req.Protocol != "HTTP/1.1" {
|
if req.Protocol != "HTTP/1.0" && req.Protocol != "HTTP/1.1" {
|
||||||
return NewHttpError(StatusHTTPVersionNotSupported,
|
return ErrProtocolNotSupported
|
||||||
"request protocol not supported: "+req.Protocol)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.ParseRequestURI(req.UrlRaw)
|
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)
|
req.Url = sh.ResolveSafePath(u.Path)
|
||||||
if req.Url == "" {
|
if req.Url == "" {
|
||||||
return NewHttpError(StatusForbidden,
|
return ErrUnsafeRequestPath
|
||||||
"unsafe request path '"+u.Path+"'")
|
|
||||||
}
|
}
|
||||||
req.QueryParams = make(mapStrStr)
|
|
||||||
for key, values := range u.Query() {
|
for key, values := range u.Query() {
|
||||||
req.QueryParams[key] = values[0]
|
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 {
|
func (conn *Connection) readHeaders(req *Request, readN *int) error {
|
||||||
req.Headers = make(HeadersMap)
|
|
||||||
for {
|
for {
|
||||||
line, err := conn.readLineBelowLimit(readN)
|
line, err := conn.readLineBelowLimit(readN)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -150,15 +204,13 @@ func (conn *Connection) readHeaders(req *Request, readN *int) error {
|
|||||||
|
|
||||||
i := strings.IndexByte(line, ':')
|
i := strings.IndexByte(line, ':')
|
||||||
if i < 1 {
|
if i < 1 {
|
||||||
return NewHttpError(StatusBadRequest,
|
return ErrLineNotHeader
|
||||||
"not a header line '"+line+"'")
|
|
||||||
}
|
}
|
||||||
key := line[:i]
|
key := line[:i]
|
||||||
firstB, lastB := key[0], key[i-1]
|
firstB, lastB := key[0], key[i-1]
|
||||||
if firstB == ' ' || firstB == '\t' ||
|
if firstB == ' ' || firstB == '\t' ||
|
||||||
lastB == ' ' || lastB == '\t' {
|
lastB == ' ' || lastB == '\t' {
|
||||||
return NewHttpError(StatusBadRequest,
|
return ErrHeaderNameStartsOrEndsWithSpace
|
||||||
"header name must not start or end with space")
|
|
||||||
}
|
}
|
||||||
key = strings.ToLower(key)
|
key = strings.ToLower(key)
|
||||||
|
|
||||||
|
|||||||
+58
-39
@@ -20,34 +20,58 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
|
needsReset bool
|
||||||
Status StatusCode
|
Status StatusCode
|
||||||
Headers HeadersMap // keys are in lower case
|
// keys are in lower case
|
||||||
omitBody bool // don't add body and content-* headers for HEAD request
|
Headers HeadersMap
|
||||||
|
// don't add body and content-* headers for HEAD request
|
||||||
|
omitBody bool
|
||||||
bodySize int64
|
bodySize int64
|
||||||
bodyType responseBodyType
|
bodyType responseBodyType
|
||||||
bodyBuf []byte // small body is sent in one write() call with headers
|
// small body is sent in one write() call with headers
|
||||||
bodyReader io.Reader // big body, sent as multiple write() calls to socket
|
bodyBuf []byte
|
||||||
|
// big body, sent as multiple write() calls to socket
|
||||||
|
bodyReader io.Reader
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
|
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
|
||||||
|
|
||||||
func (conn *Connection) CreateResponse(
|
func (res *Response) reset() {
|
||||||
status StatusCode, req *Request, wantsClose bool,
|
if !res.needsReset {
|
||||||
) *Response {
|
return
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.omitBody = req.Method == "HEAD"
|
res.needsReset = false
|
||||||
if !res.omitBody &&
|
res.Status = StatusNotInitialized
|
||||||
status >= 200 &&
|
clear(res.Headers)
|
||||||
status != StatusNoContent &&
|
res.omitBody = false
|
||||||
status != StatusNotModified {
|
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
|
// content-length is mandatory for most responses
|
||||||
res.Headers["content-length"] = "0"
|
res.Headers["content-length"] = "0"
|
||||||
}
|
}
|
||||||
@@ -61,9 +85,9 @@ func (conn *Connection) CreateResponse(
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
func (res *Response) AttachBodyBuf(body []byte, contentType string) {
|
func (res *Response) WithBodyBuf(body []byte, contentType string) *Response {
|
||||||
if res.omitBody {
|
if res.omitBody {
|
||||||
return
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
if res.bodyType != responseBodyEmpty {
|
if res.bodyType != responseBodyEmpty {
|
||||||
@@ -76,17 +100,18 @@ func (res *Response) AttachBodyBuf(body []byte, contentType string) {
|
|||||||
lenStr := strconv.FormatUint(uint64(size), 10)
|
lenStr := strconv.FormatUint(uint64(size), 10)
|
||||||
res.Headers["content-length"] = lenStr
|
res.Headers["content-length"] = lenStr
|
||||||
res.Headers["content-type"] = contentType
|
res.Headers["content-type"] = contentType
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
func (res *Response) AttachBodyStr(body string, contentType string) {
|
func (res *Response) WithBodyStr(body string, contentType string) *Response {
|
||||||
res.AttachBodyBuf(sh.UnsafeGetStrBytes(body), contentType)
|
return res.WithBodyBuf(sh.UnsafeGetStrBytes(body), contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// body will be closed by SendResponse
|
// body will be closed by SendResponse() or reset()
|
||||||
func (res *Response) AttachBodyReader(body io.Reader,
|
func (res *Response) WithBodyReader(body io.Reader,
|
||||||
size int64, contentType string) {
|
size int64, contentType string,
|
||||||
|
) *Response {
|
||||||
if res.omitBody {
|
if res.omitBody {
|
||||||
return
|
return res
|
||||||
}
|
}
|
||||||
if res.bodyType != responseBodyEmpty {
|
if res.bodyType != responseBodyEmpty {
|
||||||
panic("response already has body")
|
panic("response already has body")
|
||||||
@@ -97,19 +122,16 @@ func (res *Response) AttachBodyReader(body io.Reader,
|
|||||||
lenStr := strconv.FormatUint(uint64(size), 10)
|
lenStr := strconv.FormatUint(uint64(size), 10)
|
||||||
res.Headers["content-length"] = lenStr
|
res.Headers["content-length"] = lenStr
|
||||||
res.Headers["content-type"] = contentType
|
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) {
|
func (conn *Connection) SendResponse(res *Response) (err error) {
|
||||||
if res.bodyType == responseBodyReader {
|
|
||||||
if closer, ok := res.bodyReader.(io.Closer); ok {
|
if closer, ok := res.bodyReader.(io.Closer); ok {
|
||||||
// body is owned by the response
|
// body is owned by the response
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = closer.Close()
|
_ = closer.Close()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
sb := &conn.wbuf
|
sb := &conn.wbuf
|
||||||
sb.Reset()
|
sb.Reset()
|
||||||
@@ -120,7 +142,7 @@ func (conn *Connection) SendResponse(res *Response) (err error) {
|
|||||||
for k, v := range res.Headers {
|
for k, v := range res.Headers {
|
||||||
// prevent response splitting / header injection
|
// prevent response splitting / header injection
|
||||||
if strings.ContainsAny(k, "\r\n:") || strings.ContainsAny(v, "\r\n") {
|
if strings.ContainsAny(k, "\r\n:") || strings.ContainsAny(v, "\r\n") {
|
||||||
return ErrInvalidHeader
|
return ErrHeaderKeyOrValueHasCRLF
|
||||||
}
|
}
|
||||||
sb.AddS(k).AddS(": ").AddS(v).AddS("\r\n")
|
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.
|
// Write exactly bodySize bytes from body.
|
||||||
// It prevents session corruption if body size changed since
|
// It prevents session corruption if body size changed since
|
||||||
// AttachBodyReader was called.
|
// WithBodyReader was called.
|
||||||
remaining := res.bodySize
|
remaining := res.bodySize
|
||||||
for remaining > 0 {
|
for remaining > 0 {
|
||||||
err = conn.socket.SetWriteDeadline(time.Now().Add(conn.timeout))
|
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(
|
func (conn *Connection) CreateErrorResponse(
|
||||||
req *Request, wantsClose bool, httpErr *HttpError,
|
req *Request, wantsClose bool, httpErr *HttpError,
|
||||||
) *Response {
|
) *Response {
|
||||||
|
return conn.CreateResponse(httpErr.Status, req, wantsClose).
|
||||||
res := conn.CreateResponse(httpErr.Status, req, wantsClose)
|
WithBodyStr(httpErr.Error(), TextPlain)
|
||||||
// attach error message if method allows
|
|
||||||
res.AttachBodyStr(httpErr.Error(), TextPlain)
|
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func (c StatusCode) String() string {
|
|||||||
// HTTP status codes as registered with IANA.
|
// HTTP status codes as registered with IANA.
|
||||||
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||||
const (
|
const (
|
||||||
|
StatusNotInitialized StatusCode = 0 // for Response.reset
|
||||||
StatusContinue StatusCode = 100 // RFC 9110, 15.2.1
|
StatusContinue StatusCode = 100 // RFC 9110, 15.2.1
|
||||||
StatusSwitchingProtocols StatusCode = 101 // RFC 9110, 15.2.2
|
StatusSwitchingProtocols StatusCode = 101 // RFC 9110, 15.2.2
|
||||||
StatusProcessing StatusCode = 102 // RFC 2518, 10.1
|
StatusProcessing StatusCode = 102 // RFC 2518, 10.1
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
package http
|
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 {
|
type HttpError struct {
|
||||||
Status StatusCode
|
Status StatusCode
|
||||||
Msg string
|
Msg string
|
||||||
@@ -12,3 +23,35 @@ func (e *HttpError) Error() string {
|
|||||||
func NewHttpError(code StatusCode, msg string) *HttpError {
|
func NewHttpError(code StatusCode, msg string) *HttpError {
|
||||||
return &HttpError{Status: code, Msg: msg}
|
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")
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (srv *Server) serve() {
|
|||||||
srv.logger.Info("listener socket closed")
|
srv.logger.Info("listener socket closed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
srv.logger.Warn(err)
|
srv.logger.Error(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
conn := newConnection(srv, i, accepted.(*net.TCPConn))
|
conn := newConnection(srv, i, accepted.(*net.TCPConn))
|
||||||
|
|||||||
@@ -19,9 +19,13 @@ type SharedLocation struct {
|
|||||||
WritePassword string `toml:"write_password"`
|
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 {
|
func (sh *SharedLocation) normalize(configDir string) error {
|
||||||
if sh.Path == "" {
|
if sh.Path == "" {
|
||||||
return errors.New("share.path is empty")
|
return ErrSharePathEmpty
|
||||||
}
|
}
|
||||||
// get absolute path for configDir
|
// get absolute path for configDir
|
||||||
configDirAbs, err := filepath.Abs(configDir)
|
configDirAbs, err := filepath.Abs(configDir)
|
||||||
@@ -42,10 +46,10 @@ func (sh *SharedLocation) normalize(configDir string) error {
|
|||||||
sh.Path = filepath.Join(configDirAbs, shPathOsSep)
|
sh.Path = filepath.Join(configDirAbs, shPathOsSep)
|
||||||
|
|
||||||
if sh.Url == "" {
|
if sh.Url == "" {
|
||||||
return errors.New("share.url is empty")
|
return ErrShareUrlEmpty
|
||||||
}
|
}
|
||||||
if sh.Url[0] != '/' {
|
if sh.Url[0] != '/' {
|
||||||
return errors.New("share.url doesn't start with slash: '" + sh.Url + "'")
|
return ErrShareUrlDoesntStartWithSlash
|
||||||
}
|
}
|
||||||
// remove trailing slash
|
// remove trailing slash
|
||||||
sh.Url = strings.TrimRight(sh.Url, "/")
|
sh.Url = strings.TrimRight(sh.Url, "/")
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"serveg/internal/html"
|
||||||
"serveg/internal/http"
|
"serveg/internal/http"
|
||||||
"serveg/internal/log"
|
"serveg/internal/log"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -52,12 +53,11 @@ func (conn *connection) handle() {
|
|||||||
conn.req, err = conn.http.ReadRequest()
|
conn.req, err = conn.http.ReadRequest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// invalid request
|
// invalid request
|
||||||
conn.logConnError(err)
|
|
||||||
if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
|
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)
|
res = conn.http.CreateErrorResponse(conn.req, true, httpErr)
|
||||||
} else {
|
} else {
|
||||||
conn.log.Info("lingering close")
|
conn.logError(err)
|
||||||
conn.lingeringClose()
|
conn.lingeringClose()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -68,31 +68,31 @@ func (conn *connection) handle() {
|
|||||||
switch conn.req.Method {
|
switch conn.req.Method {
|
||||||
case "GET", "HEAD":
|
case "GET", "HEAD":
|
||||||
res = conn.serveFileOrDir()
|
res = conn.serveFileOrDir()
|
||||||
case "PUT":
|
case "PUT", "OPTIONS":
|
||||||
res = conn.handleUpload()
|
res = conn.handleUpload()
|
||||||
default:
|
default:
|
||||||
res = conn.createErrorResponse(false, http.StatusMethodNotAllowed, conn.req.Method)
|
res = conn.createErrorResponse(false, http.StatusMethodNotAllowed, conn.req.Method)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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: ", res.Headers)
|
||||||
err = conn.http.SendResponse(res)
|
err = conn.http.SendResponse(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.logConnError(err)
|
conn.logError(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// it is set by CreateResponse
|
// it is set by CreateResponse
|
||||||
if res.Headers["connection"] == "close" {
|
if res.Headers["connection"] == "close" {
|
||||||
conn.log.Info("lingering close")
|
|
||||||
conn.lingeringClose()
|
conn.lingeringClose()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *connection) logConnError(err error) {
|
func (conn *connection) logError(err error) {
|
||||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
conn.log.Info("time out")
|
conn.log.Info("time out")
|
||||||
} else if errors.Is(err, io.EOF) ||
|
} else if errors.Is(err, io.EOF) ||
|
||||||
@@ -101,7 +101,7 @@ func (conn *connection) logConnError(err error) {
|
|||||||
|
|
||||||
conn.log.Info("closed by client")
|
conn.log.Info("closed by client")
|
||||||
} else {
|
} 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.
|
// so close() doesn't send RST and destroy the response before it's read.
|
||||||
// Limits: 500 ms, 32 KB
|
// Limits: 500 ms, 32 KB
|
||||||
func (conn *connection) lingeringClose() {
|
func (conn *connection) lingeringClose() {
|
||||||
|
conn.log.Info("lingering close")
|
||||||
if conn.socket.CloseWrite() != nil {
|
if conn.socket.CloseWrite() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -116,6 +117,12 @@ func (conn *connection) lingeringClose() {
|
|||||||
_, _ = io.Copy(io.Discard, io.LimitReader(conn.socket, 32*1024))
|
_, _ = 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 {
|
func (conn *connection) serveFileOrDir() *http.Response {
|
||||||
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(conn.req.Url)
|
path, share, urlStatus := conn.srv.Conf.Share.MapUrl(conn.req.Url)
|
||||||
// TODO implement authorization
|
// TODO implement authorization
|
||||||
@@ -137,9 +144,15 @@ func (conn *connection) serveFileOrDir() *http.Response {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (conn *connection) createErrorResponse(wantsClose bool, status http.StatusCode, msg string,
|
func (conn *connection) serveDirListing(dirPath string) *http.Response {
|
||||||
) *http.Response {
|
htmlBytes, err := html.GenDirListingHtml(&conn.log, dirPath,
|
||||||
return conn.http.CreateErrorResponse(conn.req, wantsClose, http.NewHttpError(status, msg))
|
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 {
|
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" {
|
if conn.req.Method != "HEAD" {
|
||||||
// file will be closed by SendResponse()
|
// file will be closed by SendResponse()
|
||||||
fileOwnershipTransferred = true
|
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
|
// 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.
|
// On next request browser will send if-modified-since and let server check if file changed.
|
||||||
|
|||||||
Reference in New Issue
Block a user