implemented http server

This commit is contained in:
Altair-sh
2026-11-02 21:47:34 +01:00
parent e0cf8a12a9
commit bac4ccb34e
14 changed files with 775 additions and 178 deletions
+75 -47
View File
@@ -2,6 +2,7 @@ package http
import (
"errors"
"fmt"
"io"
"net"
sh "serveg/internal/string_helper"
@@ -14,19 +15,18 @@ type responseBodyType uint8
const (
responseBodyEmpty = responseBodyType(iota)
responseBodySmall
responseBodyBig
responseBodyBuf
responseBodyReader
)
const TextPlain = "text/plain; charset=utf-8"
type Response struct {
Status StatusCode
Headers HeadersMap // keys are in lower case
omitBody bool // don't add body and content-* headers for HEAD request
bodyType responseBodyType
bodySmall []byte // small body is sent in one write() call with headers
bodyBig io.WriterTo // big body, sent as multiple write() calls to socket
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
}
const TimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
@@ -46,7 +46,7 @@ func (conn *Connection) CreateResponse(
status >= 200 &&
status != StatusNoContent &&
status != StatusNotModified {
// content-length 0 is mandatory for most responses
// content-length is mandatory for most responses
res.Headers["content-length"] = "0"
}
@@ -59,7 +59,7 @@ func (conn *Connection) CreateResponse(
return res
}
func (res *Response) AttachBodySmall(body []byte, contentType string) {
func (res *Response) AttachBodyBuf(body []byte, contentType string) {
if res.omitBody {
return
}
@@ -67,18 +67,21 @@ func (res *Response) AttachBodySmall(body []byte, contentType string) {
if res.bodyType != responseBodyEmpty {
panic("response already has body")
}
res.bodyType = responseBodySmall
res.bodySmall = body
lenStr := strconv.FormatUint(uint64(len(body)), 10)
res.bodyType = responseBodyBuf
res.bodyBuf = body
size := int64(len(body))
res.bodySize = size
lenStr := strconv.FormatUint(uint64(size), 10)
res.Headers["content-length"] = lenStr
res.Headers["content-type"] = contentType
}
func (res *Response) AttachBodySmallStr(body string, contentType string) {
res.AttachBodySmall(sh.UnsafeGetStrBytes(body), contentType)
func (res *Response) AttachBodyStr(body string, contentType string) {
res.AttachBodyBuf(sh.UnsafeGetStrBytes(body), contentType)
}
func (res *Response) AttachBodyBig(body io.WriterTo,
size uint64, contentType string) {
// body will be closed by SendResponse
func (res *Response) AttachBodyReader(body io.Reader,
size int64, contentType string) {
if res.omitBody {
return
@@ -86,23 +89,31 @@ func (res *Response) AttachBodyBig(body io.WriterTo,
if res.bodyType != responseBodyEmpty {
panic("response already has body")
}
res.bodyType = responseBodyBig
res.bodyBig = body
lenStr := strconv.FormatUint(size, 10)
res.bodyType = responseBodyReader
res.bodyReader = body
res.bodySize = size
lenStr := strconv.FormatUint(uint64(size), 10)
res.Headers["content-length"] = lenStr
res.Headers["content-type"] = contentType
}
var ErrInvalidHeader = errors.New("http: header key or value contains CR or LF")
func (conn *Connection) SendResponse(res *Response) error {
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()
}()
}
}
sb := &conn.wbuf
sb.Reset()
// first line
// space before \r\n is required by standard
sb.AddS("HTTP/1.1 ").AddU(uint64(res.Status)).AddS(" \r\n")
// headers
for k, v := range res.Headers {
// prevent response splitting / header injection
@@ -116,35 +127,52 @@ func (conn *Connection) SendResponse(res *Response) error {
switch res.bodyType {
case responseBodyEmpty:
// net.Conn.Write follows the io.Writer contract: it returns a non-nil
// error whenever n < len(p), so no loop is needed.
if _, err := conn.socket.Write(conn.wbuf.Buf); err != nil {
return err
}
case responseBodySmall:
_, err = conn.socket.Write(conn.wbuf.Buf)
case responseBodyBuf:
// write headers and body in one socket.Write() call
bbb := net.Buffers{sb.Buf, res.bodySmall}
if _, err := bbb.WriteTo(conn.socket); err != nil {
return err
bufs := net.Buffers{sb.Buf, res.bodyBuf}
_, err = bufs.WriteTo(conn.socket)
case responseBodyReader:
// write headers
if _, err = conn.socket.Write(conn.wbuf.Buf); err != nil {
return
}
case responseBodyBig:
// write headers, then write big body
if _, err := conn.socket.Write(conn.wbuf.Buf); err != nil {
return err
}
if _, err := res.bodyBig.WriteTo(conn.socket); err != nil {
return err
// Write exactly bodySize bytes from body.
// It prevents session corruption if body size changed since
// AttachBodyReader was called.
remaining := res.bodySize
for remaining > 0 {
err = conn.socket.SetWriteDeadline(time.Now().Add(conn.timeout))
if err != nil {
return
}
// don't increase or slow connections will trigger socket deadline
// 256 KB / 30 s ≈ 8.7 KB/s
const fileReadChunkSize = 256 * 1024
n := min(remaining, fileReadChunkSize)
_, err = io.CopyN(conn.socket, res.bodyReader, n)
if err != nil {
if errors.Is(err, io.EOF) {
// Body ended before bodySize bytes.
// Returning just io.EOF would look like a client discnnect
err = fmt.Errorf("response body is %d bytes shorter"+
" than content-length: %w",
remaining, io.ErrUnexpectedEOF)
}
return
}
remaining -= n
}
}
return nil
return
}
func (conn *Connection) SendErrorResponse(
httpErr *HttpError, req *Request, wantsClose bool,
) 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.AttachBodySmallStr(httpErr.Error(), TextPlain)
return conn.SendResponse(res)
res.AttachBodyStr(httpErr.Error(), TextPlain)
return res
}