implemented HTTP 1.0/1.1 server
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
/serveg.toml
|
/serveg.toml
|
||||||
/serveg
|
/serveg
|
||||||
bin/
|
bin/
|
||||||
|
__debug_bin*
|
||||||
|
|||||||
+13
-15
@@ -22,29 +22,27 @@ func run() int {
|
|||||||
|
|
||||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
configPath := *flag.String("config", "serveg.toml", "path to config file")
|
configPath := flag.String("config", "serveg.toml", "path to config file")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
conf, err := server.ParseOrCreateConfig(configPath)
|
conf, err := server.ParseOrCreateConfig(*configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Error("can't parse config: ", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := server.Server{Conf: conf}
|
||||||
|
if err := srv.StartGoroutine(); err != nil {
|
||||||
logger.Error(err)
|
logger.Error(err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
go func() { errCh <- server.Serve(conf) }()
|
|
||||||
logger.Info("server started")
|
|
||||||
|
|
||||||
select {
|
|
||||||
// server failed to start
|
|
||||||
case err := <-errCh:
|
|
||||||
logger.Error(err)
|
|
||||||
return 1
|
|
||||||
// got SIGINT/SIGTERM
|
// got SIGINT/SIGTERM
|
||||||
case <-ctx.Done():
|
<-ctx.Done()
|
||||||
stop() // restore default behaviour: a second Ctrl+C kills immediately
|
stop() // restore default behaviour: a second Ctrl+C kills immediately
|
||||||
logger.Info("shutting down")
|
logger.Info("shutting down")
|
||||||
}
|
// waits for server stop
|
||||||
|
srv.Stop()
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
sh "serveg/internal/string_helper"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxHeaderBytes = 16 * 1024
|
||||||
|
const initialWriteBufCap = 512
|
||||||
|
|
||||||
|
type mapStrStr map[string]string
|
||||||
|
type HeadersMap map[string]string
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
socket *net.TCPConn
|
||||||
|
r *bufio.Reader
|
||||||
|
wbuf sh.StringBuilder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnection(socket *net.TCPConn) *Connection {
|
||||||
|
return &Connection{
|
||||||
|
socket: socket,
|
||||||
|
r: bufio.NewReaderSize(socket, maxHeaderBytes),
|
||||||
|
wbuf: sh.StringBuilder{Buf: make([]byte, 0, initialWriteBufCap)},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func HeaderValueContains(sourceSplit []string, target string) bool {
|
||||||
|
for _, v := range sourceSplit {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if strings.EqualFold(v, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/url"
|
||||||
|
sh "serveg/internal/string_helper"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Method string
|
||||||
|
Protocol string
|
||||||
|
// url-encoded characters (%D0), query params (?v=123)
|
||||||
|
PathRaw string
|
||||||
|
Path string
|
||||||
|
QueryParams mapStrStr
|
||||||
|
// keys are in lower case
|
||||||
|
// duplicate keys are joined with ", "
|
||||||
|
Headers HeadersMap
|
||||||
|
// if true, don't close connection after response
|
||||||
|
WantsKeepAlive bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (conn *Connection) ReadRequest() (req *Request, err error) {
|
||||||
|
req = new(Request)
|
||||||
|
var readN int
|
||||||
|
// METHOD /path HTTP/ver
|
||||||
|
if err = conn.readFirstLine(req, &readN); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// key: value
|
||||||
|
// ...
|
||||||
|
// empty line
|
||||||
|
if err = conn.readHeaders(req, &readN); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// This server doesn't accept request bodies.
|
||||||
|
// Reject connection rather than desync the stream.
|
||||||
|
contentLength := req.Headers["content-length"]
|
||||||
|
transferEncoding := req.Headers["transfer-encoding"]
|
||||||
|
if (contentLength != "" && contentLength != "0") ||
|
||||||
|
transferEncoding != "" {
|
||||||
|
err = NewHttpError(StatusBadRequest,
|
||||||
|
"request bodies are not supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// set WantsKeepAlive
|
||||||
|
connectionStrSplit := strings.Split(req.Headers["connection"], ",")
|
||||||
|
if req.Protocol == "HTTP/1.0" {
|
||||||
|
req.WantsKeepAlive = HeaderValueContains(connectionStrSplit, "keep-alive")
|
||||||
|
} else {
|
||||||
|
req.WantsKeepAlive = !HeaderValueContains(connectionStrSplit, "close")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Connection) readLineBelowLimit(readN *int) (string, error) {
|
||||||
|
// read up to maxHeaderBytes
|
||||||
|
slice, notLineEnd, err := conn.r.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
// something is wrong with tcp socket
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if notLineEnd {
|
||||||
|
// buffer end was reached but no '\n' found
|
||||||
|
return "", NewHttpError(StatusBadRequest, "line end not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
*readN += len(slice) + 1 // \n
|
||||||
|
if *readN > maxHeaderBytes {
|
||||||
|
return "", NewHttpError(StatusRequestHeaderFieldsTooLarge,
|
||||||
|
"headers too large")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimRight(string(slice), " \r\n"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Connection) readFirstLine(req *Request, readN *int) error {
|
||||||
|
line, err := conn.readLineBelowLimit(readN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// skip one empty line
|
||||||
|
// some clients send it between requests
|
||||||
|
if line == "" {
|
||||||
|
line, err = conn.readLineBelowLimit(readN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, " ")
|
||||||
|
if len(parts) != 3 || !strings.HasPrefix(parts[2], "HTTP/") {
|
||||||
|
return NewHttpError(StatusBadRequest,
|
||||||
|
"malformed request line '"+line+"'")
|
||||||
|
}
|
||||||
|
req.Method = parts[0]
|
||||||
|
// url-encoded characters (%D0), query params (?v=123)
|
||||||
|
req.PathRaw = parts[1]
|
||||||
|
req.Protocol = parts[2]
|
||||||
|
|
||||||
|
if req.Protocol != "HTTP/1.0" && req.Protocol != "HTTP/1.1" {
|
||||||
|
return NewHttpError(StatusHTTPVersionNotSupported,
|
||||||
|
"request protocol not supported: "+req.Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
u, err := url.ParseRequestURI(req.PathRaw)
|
||||||
|
if err != nil {
|
||||||
|
return NewHttpError(StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Path = sh.ResolveSafePath(u.Path)
|
||||||
|
if req.Path == "" {
|
||||||
|
return NewHttpError(StatusForbidden,
|
||||||
|
"unsafe request path '"+u.Path+"'")
|
||||||
|
}
|
||||||
|
req.QueryParams = make(mapStrStr)
|
||||||
|
for key, values := range u.Query() {
|
||||||
|
req.QueryParams[key] = values[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Connection) readHeaders(req *Request, readN *int) error {
|
||||||
|
req.Headers = make(HeadersMap)
|
||||||
|
for {
|
||||||
|
line, err := conn.readLineBelowLimit(readN)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// empty line means end of headers
|
||||||
|
if line == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.IndexByte(line, ':')
|
||||||
|
if i < 1 {
|
||||||
|
return NewHttpError(StatusBadRequest,
|
||||||
|
"not a header line '"+line+"'")
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
key = strings.ToLower(key)
|
||||||
|
|
||||||
|
// val can be empty
|
||||||
|
val := strings.TrimSpace(line[i+1:])
|
||||||
|
// if header repeats, join values with ", "
|
||||||
|
existing := req.Headers[key]
|
||||||
|
if existing != "" {
|
||||||
|
val = existing + ", " + val
|
||||||
|
}
|
||||||
|
req.Headers[key] = val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
sh "serveg/internal/string_helper"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type responseBodyType uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
responseBodyEmpty = responseBodyType(iota)
|
||||||
|
responseBodySmall
|
||||||
|
responseBodyBig
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
res.omitBody = req.Method == "HEAD"
|
||||||
|
if !res.omitBody &&
|
||||||
|
status >= 200 &&
|
||||||
|
status != StatusNoContent &&
|
||||||
|
status != StatusNotModified {
|
||||||
|
// content-length 0 is mandatory for most responses
|
||||||
|
res.Headers["content-length"] = "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
if wantsClose || !req.WantsKeepAlive {
|
||||||
|
res.Headers["connection"] = "close"
|
||||||
|
} else if req.Protocol == "HTTP/1.0" {
|
||||||
|
res.Headers["connection"] = "keep-alive"
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (res *Response) AttachBodySmall(body []byte, contentType string) {
|
||||||
|
if res.omitBody {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.bodyType != responseBodyEmpty {
|
||||||
|
panic("response already has body")
|
||||||
|
}
|
||||||
|
res.bodyType = responseBodySmall
|
||||||
|
res.bodySmall = body
|
||||||
|
lenStr := strconv.FormatUint(uint64(len(body)), 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) AttachBodyBig(body io.WriterTo,
|
||||||
|
size uint64, contentType string) {
|
||||||
|
|
||||||
|
if res.omitBody {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if res.bodyType != responseBodyEmpty {
|
||||||
|
panic("response already has body")
|
||||||
|
}
|
||||||
|
res.bodyType = responseBodyBig
|
||||||
|
res.bodyBig = body
|
||||||
|
lenStr := strconv.FormatUint(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 {
|
||||||
|
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
|
||||||
|
if strings.ContainsAny(k, "\r\n:") || strings.ContainsAny(v, "\r\n") {
|
||||||
|
return ErrInvalidHeader
|
||||||
|
}
|
||||||
|
sb.AddS(k).AddS(": ").AddS(v).AddS("\r\n")
|
||||||
|
}
|
||||||
|
// empty line
|
||||||
|
sb.AddS("\r\n")
|
||||||
|
|
||||||
|
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:
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *Connection) SendErrorResponse(
|
||||||
|
httpErr *HttpError, req *Request, wantsClose bool,
|
||||||
|
) error {
|
||||||
|
|
||||||
|
res := conn.CreateResponse(httpErr.Status, req, wantsClose)
|
||||||
|
// attach error message if method allows
|
||||||
|
res.AttachBodySmallStr(httpErr.Error(), TextPlain)
|
||||||
|
return conn.SendResponse(res)
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusCode uint16
|
||||||
|
|
||||||
|
func (c StatusCode) String() string {
|
||||||
|
numStr := strconv.FormatUint(uint64(c), 10)
|
||||||
|
return statusText(c) + " (" + numStr + ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copied from /usr/lib/go/src/net/http/status.go
|
||||||
|
/// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// HTTP status codes as registered with IANA.
|
||||||
|
// See: https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
||||||
|
const (
|
||||||
|
StatusContinue StatusCode = 100 // RFC 9110, 15.2.1
|
||||||
|
StatusSwitchingProtocols StatusCode = 101 // RFC 9110, 15.2.2
|
||||||
|
StatusProcessing StatusCode = 102 // RFC 2518, 10.1
|
||||||
|
StatusEarlyHints StatusCode = 103 // RFC 8297
|
||||||
|
|
||||||
|
StatusOK StatusCode = 200 // RFC 9110, 15.3.1
|
||||||
|
StatusCreated StatusCode = 201 // RFC 9110, 15.3.2
|
||||||
|
StatusAccepted StatusCode = 202 // RFC 9110, 15.3.3
|
||||||
|
StatusNonAuthoritativeInfo StatusCode = 203 // RFC 9110, 15.3.4
|
||||||
|
StatusNoContent StatusCode = 204 // RFC 9110, 15.3.5
|
||||||
|
StatusResetContent StatusCode = 205 // RFC 9110, 15.3.6
|
||||||
|
StatusPartialContent StatusCode = 206 // RFC 9110, 15.3.7
|
||||||
|
StatusMultiStatus StatusCode = 207 // RFC 4918, 11.1
|
||||||
|
StatusAlreadyReported StatusCode = 208 // RFC 5842, 7.1
|
||||||
|
StatusIMUsed StatusCode = 226 // RFC 3229, 10.4.1
|
||||||
|
|
||||||
|
StatusMultipleChoices StatusCode = 300 // RFC 9110, 15.4.1
|
||||||
|
StatusMovedPermanently StatusCode = 301 // RFC 9110, 15.4.2
|
||||||
|
StatusFound StatusCode = 302 // RFC 9110, 15.4.3
|
||||||
|
StatusSeeOther StatusCode = 303 // RFC 9110, 15.4.4
|
||||||
|
StatusNotModified StatusCode = 304 // RFC 9110, 15.4.5
|
||||||
|
StatusUseProxy StatusCode = 305 // RFC 9110, 15.4.6
|
||||||
|
_ StatusCode = 306 // RFC 9110, 15.4.7 (Unused)
|
||||||
|
StatusTemporaryRedirect StatusCode = 307 // RFC 9110, 15.4.8
|
||||||
|
StatusPermanentRedirect StatusCode = 308 // RFC 9110, 15.4.9
|
||||||
|
|
||||||
|
StatusBadRequest StatusCode = 400 // RFC 9110, 15.5.1
|
||||||
|
StatusUnauthorized StatusCode = 401 // RFC 9110, 15.5.2
|
||||||
|
StatusPaymentRequired StatusCode = 402 // RFC 9110, 15.5.3
|
||||||
|
StatusForbidden StatusCode = 403 // RFC 9110, 15.5.4
|
||||||
|
StatusNotFound StatusCode = 404 // RFC 9110, 15.5.5
|
||||||
|
StatusMethodNotAllowed StatusCode = 405 // RFC 9110, 15.5.6
|
||||||
|
StatusNotAcceptable StatusCode = 406 // RFC 9110, 15.5.7
|
||||||
|
StatusProxyAuthRequired StatusCode = 407 // RFC 9110, 15.5.8
|
||||||
|
StatusRequestTimeout StatusCode = 408 // RFC 9110, 15.5.9
|
||||||
|
StatusConflict StatusCode = 409 // RFC 9110, 15.5.10
|
||||||
|
StatusGone StatusCode = 410 // RFC 9110, 15.5.11
|
||||||
|
StatusLengthRequired StatusCode = 411 // RFC 9110, 15.5.12
|
||||||
|
StatusPreconditionFailed StatusCode = 412 // RFC 9110, 15.5.13
|
||||||
|
StatusRequestEntityTooLarge StatusCode = 413 // RFC 9110, 15.5.14
|
||||||
|
StatusRequestURITooLong StatusCode = 414 // RFC 9110, 15.5.15
|
||||||
|
StatusUnsupportedMediaType StatusCode = 415 // RFC 9110, 15.5.16
|
||||||
|
StatusRequestedRangeNotSatisfiable StatusCode = 416 // RFC 9110, 15.5.17
|
||||||
|
StatusExpectationFailed StatusCode = 417 // RFC 9110, 15.5.18
|
||||||
|
StatusTeapot StatusCode = 418 // RFC 9110, 15.5.19 (Unused)
|
||||||
|
StatusMisdirectedRequest StatusCode = 421 // RFC 9110, 15.5.20
|
||||||
|
StatusUnprocessableEntity StatusCode = 422 // RFC 9110, 15.5.21
|
||||||
|
StatusLocked StatusCode = 423 // RFC 4918, 11.3
|
||||||
|
StatusFailedDependency StatusCode = 424 // RFC 4918, 11.4
|
||||||
|
StatusTooEarly StatusCode = 425 // RFC 8470, 5.2.
|
||||||
|
StatusUpgradeRequired StatusCode = 426 // RFC 9110, 15.5.22
|
||||||
|
StatusPreconditionRequired StatusCode = 428 // RFC 6585, 3
|
||||||
|
StatusTooManyRequests StatusCode = 429 // RFC 6585, 4
|
||||||
|
StatusRequestHeaderFieldsTooLarge StatusCode = 431 // RFC 6585, 5
|
||||||
|
StatusUnavailableForLegalReasons StatusCode = 451 // RFC 7725, 3
|
||||||
|
|
||||||
|
StatusInternalServerError StatusCode = 500 // RFC 9110, 15.6.1
|
||||||
|
StatusNotImplemented StatusCode = 501 // RFC 9110, 15.6.2
|
||||||
|
StatusBadGateway StatusCode = 502 // RFC 9110, 15.6.3
|
||||||
|
StatusServiceUnavailable StatusCode = 503 // RFC 9110, 15.6.4
|
||||||
|
StatusGatewayTimeout StatusCode = 504 // RFC 9110, 15.6.5
|
||||||
|
StatusHTTPVersionNotSupported StatusCode = 505 // RFC 9110, 15.6.6
|
||||||
|
StatusVariantAlsoNegotiates StatusCode = 506 // RFC 2295, 8.1
|
||||||
|
StatusInsufficientStorage StatusCode = 507 // RFC 4918, 11.5
|
||||||
|
StatusLoopDetected StatusCode = 508 // RFC 5842, 7.2
|
||||||
|
StatusNotExtended StatusCode = 510 // RFC 2774, 7
|
||||||
|
StatusNetworkAuthenticationRequired StatusCode = 511 // RFC 6585, 6
|
||||||
|
)
|
||||||
|
|
||||||
|
func statusText(c StatusCode) string {
|
||||||
|
switch c {
|
||||||
|
case StatusContinue:
|
||||||
|
return "Continue"
|
||||||
|
case StatusSwitchingProtocols:
|
||||||
|
return "Switching Protocols"
|
||||||
|
case StatusProcessing:
|
||||||
|
return "Processing"
|
||||||
|
case StatusEarlyHints:
|
||||||
|
return "Early Hints"
|
||||||
|
case StatusOK:
|
||||||
|
return "OK"
|
||||||
|
case StatusCreated:
|
||||||
|
return "Created"
|
||||||
|
case StatusAccepted:
|
||||||
|
return "Accepted"
|
||||||
|
case StatusNonAuthoritativeInfo:
|
||||||
|
return "Non-Authoritative Information"
|
||||||
|
case StatusNoContent:
|
||||||
|
return "No Content"
|
||||||
|
case StatusResetContent:
|
||||||
|
return "Reset Content"
|
||||||
|
case StatusPartialContent:
|
||||||
|
return "Partial Content"
|
||||||
|
case StatusMultiStatus:
|
||||||
|
return "Multi-Status"
|
||||||
|
case StatusAlreadyReported:
|
||||||
|
return "Already Reported"
|
||||||
|
case StatusIMUsed:
|
||||||
|
return "IM Used"
|
||||||
|
case StatusMultipleChoices:
|
||||||
|
return "Multiple Choices"
|
||||||
|
case StatusMovedPermanently:
|
||||||
|
return "Moved Permanently"
|
||||||
|
case StatusFound:
|
||||||
|
return "Found"
|
||||||
|
case StatusSeeOther:
|
||||||
|
return "See Other"
|
||||||
|
case StatusNotModified:
|
||||||
|
return "Not Modified"
|
||||||
|
case StatusUseProxy:
|
||||||
|
return "Use Proxy"
|
||||||
|
case StatusTemporaryRedirect:
|
||||||
|
return "Temporary Redirect"
|
||||||
|
case StatusPermanentRedirect:
|
||||||
|
return "Permanent Redirect"
|
||||||
|
case StatusBadRequest:
|
||||||
|
return "Bad Request"
|
||||||
|
case StatusUnauthorized:
|
||||||
|
return "Unauthorized"
|
||||||
|
case StatusPaymentRequired:
|
||||||
|
return "Payment Required"
|
||||||
|
case StatusForbidden:
|
||||||
|
return "Forbidden"
|
||||||
|
case StatusNotFound:
|
||||||
|
return "Not Found"
|
||||||
|
case StatusMethodNotAllowed:
|
||||||
|
return "Method Not Allowed"
|
||||||
|
case StatusNotAcceptable:
|
||||||
|
return "Not Acceptable"
|
||||||
|
case StatusProxyAuthRequired:
|
||||||
|
return "Proxy Authentication Required"
|
||||||
|
case StatusRequestTimeout:
|
||||||
|
return "Request Timeout"
|
||||||
|
case StatusConflict:
|
||||||
|
return "Conflict"
|
||||||
|
case StatusGone:
|
||||||
|
return "Gone"
|
||||||
|
case StatusLengthRequired:
|
||||||
|
return "Length Required"
|
||||||
|
case StatusPreconditionFailed:
|
||||||
|
return "Precondition Failed"
|
||||||
|
case StatusRequestEntityTooLarge:
|
||||||
|
return "Request Entity Too Large"
|
||||||
|
case StatusRequestURITooLong:
|
||||||
|
return "Request URI Too Long"
|
||||||
|
case StatusUnsupportedMediaType:
|
||||||
|
return "Unsupported Media Type"
|
||||||
|
case StatusRequestedRangeNotSatisfiable:
|
||||||
|
return "Requested Range Not Satisfiable"
|
||||||
|
case StatusExpectationFailed:
|
||||||
|
return "Expectation Failed"
|
||||||
|
case StatusTeapot:
|
||||||
|
return "I'm a teapot"
|
||||||
|
case StatusMisdirectedRequest:
|
||||||
|
return "Misdirected Request"
|
||||||
|
case StatusUnprocessableEntity:
|
||||||
|
return "Unprocessable Entity"
|
||||||
|
case StatusLocked:
|
||||||
|
return "Locked"
|
||||||
|
case StatusFailedDependency:
|
||||||
|
return "Failed Dependency"
|
||||||
|
case StatusTooEarly:
|
||||||
|
return "Too Early"
|
||||||
|
case StatusUpgradeRequired:
|
||||||
|
return "Upgrade Required"
|
||||||
|
case StatusPreconditionRequired:
|
||||||
|
return "Precondition Required"
|
||||||
|
case StatusTooManyRequests:
|
||||||
|
return "Too Many Requests"
|
||||||
|
case StatusRequestHeaderFieldsTooLarge:
|
||||||
|
return "Request Header Fields Too Large"
|
||||||
|
case StatusUnavailableForLegalReasons:
|
||||||
|
return "Unavailable For Legal Reasons"
|
||||||
|
case StatusInternalServerError:
|
||||||
|
return "Internal Server Error"
|
||||||
|
case StatusNotImplemented:
|
||||||
|
return "Not Implemented"
|
||||||
|
case StatusBadGateway:
|
||||||
|
return "Bad Gateway"
|
||||||
|
case StatusServiceUnavailable:
|
||||||
|
return "Service Unavailable"
|
||||||
|
case StatusGatewayTimeout:
|
||||||
|
return "Gateway Timeout"
|
||||||
|
case StatusHTTPVersionNotSupported:
|
||||||
|
return "HTTP Version Not Supported"
|
||||||
|
case StatusVariantAlsoNegotiates:
|
||||||
|
return "Variant Also Negotiates"
|
||||||
|
case StatusInsufficientStorage:
|
||||||
|
return "Insufficient Storage"
|
||||||
|
case StatusLoopDetected:
|
||||||
|
return "Loop Detected"
|
||||||
|
case StatusNotExtended:
|
||||||
|
return "Not Extended"
|
||||||
|
case StatusNetworkAuthenticationRequired:
|
||||||
|
return "Network Authentication Required"
|
||||||
|
default:
|
||||||
|
return "INVALID STATUS CODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
type HttpError struct {
|
||||||
|
Status StatusCode
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HttpError) Error() string {
|
||||||
|
return e.Status.String() + ": " + e.Msg
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpError(code StatusCode, msg string) *HttpError {
|
||||||
|
return &HttpError{Status: code, Msg: msg}
|
||||||
|
}
|
||||||
+3
-2
@@ -37,6 +37,7 @@ package log
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
sh "serveg/internal/string_helper"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -164,9 +165,9 @@ func buildMessage(context string, severity Severity, a ...any) message {
|
|||||||
return message{text, color}
|
return message{text, color}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msg message) print() {
|
func (msg *message) print() {
|
||||||
SetColorFg(msg.color)
|
SetColorFg(msg.color)
|
||||||
if _, err := os.Stdout.Write([]byte(msg.text)); err != nil {
|
if _, err := os.Stdout.Write(sh.UnsafeGetStrBytes(msg.text)); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: add example config file
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Port uint16 `toml:"port"`
|
Addr string `toml:"address"`
|
||||||
Ip IP `toml:"ip"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadConfig(path string) (*Config, error) {
|
func ReadConfig(path string) (*Config, error) {
|
||||||
var config = new(Config)
|
var config = DefaultConfig()
|
||||||
tomlMeta, err := toml.DecodeFile(path, config)
|
tomlMeta, err := toml.DecodeFile(path, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -33,8 +31,7 @@ func ReadConfig(path string) (*Config, error) {
|
|||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Port: 8080,
|
Addr: "127.0.0.1:8080",
|
||||||
Ip: IP{net.IPv4(127, 0, 0, 1)},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,20 +59,3 @@ func WriteConfig(config *Config, path string) (err error) {
|
|||||||
enc.Indent = " " // 4
|
enc.Indent = " " // 4
|
||||||
return enc.Encode(config)
|
return enc.Encode(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
type IP struct {
|
|
||||||
net.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ encoding.TextUnmarshaler = (*IP)(nil)
|
|
||||||
var _ encoding.TextMarshaler = IP{}
|
|
||||||
|
|
||||||
func (a *IP) UnmarshalText(text []byte) error {
|
|
||||||
s := string(text)
|
|
||||||
ip := net.ParseIP(s)
|
|
||||||
if ip == nil {
|
|
||||||
return errors.New("can't parse '" + s + "' as IP address")
|
|
||||||
}
|
|
||||||
a.IP = ip
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
+106
-26
@@ -1,59 +1,139 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
|
"serveg/internal/http"
|
||||||
"serveg/internal/log"
|
"serveg/internal/log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Serve(conf *Config) error {
|
type Server struct {
|
||||||
logger := log.LoggerContext{Name: "server"}
|
Conf *Config
|
||||||
addr := net.JoinHostPort(conf.Ip.String(), strconv.Itoa(int(conf.Port)))
|
listener net.Listener
|
||||||
|
logger log.LoggerContext
|
||||||
|
wg sync.WaitGroup
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("listening at", addr)
|
func (srv *Server) StartGoroutine() (err error) {
|
||||||
srv, err := net.Listen("tcp", addr)
|
srv.logger = log.LoggerContext{Name: "server"}
|
||||||
if err != nil {
|
srv.logger.Info("listening at ", srv.Conf.Addr)
|
||||||
return err
|
srv.listener, err = net.Listen("tcp", srv.Conf.Addr)
|
||||||
|
if err == nil {
|
||||||
|
srv.wg.Go(srv.serve)
|
||||||
}
|
}
|
||||||
defer func() { _ = srv.Close() }()
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (srv *Server) serve() {
|
||||||
|
srv.logger.Info("server started")
|
||||||
for i := int64(0); ; i++ {
|
for i := int64(0); ; i++ {
|
||||||
conn, err := srv.Accept()
|
conn, err := srv.listener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(err)
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
srv.logger.Info("listener socket closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
srv.logger.Warn(err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
go handleConnection(conn, i)
|
// TODO: add connection to map. SetDeadline(Now()) in Stop().
|
||||||
|
// srv.wg.Go(func() { handleConnection(conn.(*net.TCPConn), i) })
|
||||||
|
go handleConnection(conn.(*net.TCPConn), i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConnection(conn net.Conn, n int64) {
|
func (srv *Server) Stop() {
|
||||||
|
srv.logger.Info("stopping server")
|
||||||
|
_ = srv.listener.Close()
|
||||||
|
srv.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleConnection(socket *net.TCPConn, n int64) {
|
||||||
logger := log.LoggerContext{Name: "conn-" + strconv.FormatInt(n, 10)}
|
logger := log.LoggerContext{Name: "conn-" + strconv.FormatInt(n, 10)}
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
logger.Error("panic: ", r)
|
logger.Error("panic: ", r)
|
||||||
}
|
}
|
||||||
_ = conn.Close()
|
_ = socket.Close()
|
||||||
|
logger.Info("connection closed")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
buf := make([]byte, 4096)
|
conn := http.NewConnection(socket)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if err := conn.SetDeadline(time.Now().Add(30 * time.Second)); err != nil {
|
err := socket.SetDeadline(time.Now().Add(30 * time.Second))
|
||||||
logger.Warn(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
n, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn(err)
|
logger.Warn(err)
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
msgBytes := buf[:n]
|
|
||||||
logger.Info("msg: `", string(msgBytes), "`")
|
req, err := conn.ReadRequest()
|
||||||
if _, err := conn.Write(msgBytes); err != nil {
|
if err != nil {
|
||||||
logger.Warn(err)
|
logConnError(logger, err)
|
||||||
break
|
|
||||||
|
if httpErr, ok := errors.AsType[*http.HttpError](err); ok {
|
||||||
|
// return error code to client
|
||||||
|
logger.Info("sending error response")
|
||||||
|
// req is partially parsed, may use fields
|
||||||
|
if err = conn.SendErrorResponse(httpErr, req, true); err != nil {
|
||||||
|
logger.Warn("can't send error response")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// close the connection
|
||||||
|
logger.Info("lingering close")
|
||||||
|
lingeringClose(socket)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info(req.Method, " ", req.Path, " ", req.Protocol,
|
||||||
|
"\nHeaders: ", req.Headers)
|
||||||
|
|
||||||
|
resBody := "Hi\n" + time.Now().Format(time.DateTime)
|
||||||
|
|
||||||
|
res := conn.CreateResponse(http.StatusOK, req, false)
|
||||||
|
res.AttachBodySmallStr(resBody, http.TextPlain)
|
||||||
|
logger.Info("sending response: ", res.Status,
|
||||||
|
"\nHeaders: ", res.Headers)
|
||||||
|
err = conn.SendResponse(res)
|
||||||
|
if err != nil {
|
||||||
|
logConnError(logger, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// it is set by CreateResponse
|
||||||
|
if res.Headers["connection"] == "close" {
|
||||||
|
logger.Info("lingering close")
|
||||||
|
lingeringClose(socket)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func logConnError(logger log.LoggerContext, err error) {
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
logger.Info("time out")
|
||||||
|
} else if errors.Is(err, io.EOF) ||
|
||||||
|
errors.Is(err, syscall.EPIPE) ||
|
||||||
|
errors.Is(err, syscall.ECONNRESET) {
|
||||||
|
|
||||||
|
logger.Info("closed by client")
|
||||||
|
} else {
|
||||||
|
logger.Warn(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// lingeringClose: shut down our side, then drain the client's unread data,
|
||||||
|
// so close() doesn't send RST and destroy the response before it's read.
|
||||||
|
// Limits: 500 ms, 32 KB
|
||||||
|
func lingeringClose(socket *net.TCPConn) {
|
||||||
|
if socket.CloseWrite() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = socket.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
||||||
|
_, _ = io.Copy(io.Discard, io.LimitReader(socket, 32*1024))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package string_helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StringBuilder struct {
|
||||||
|
// allocate it with size of estimated output
|
||||||
|
Buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// compile-time interface checks
|
||||||
|
var (
|
||||||
|
_ io.Writer = (*StringBuilder)(nil)
|
||||||
|
_ io.StringWriter = (*StringBuilder)(nil)
|
||||||
|
_ io.ByteWriter = (*StringBuilder)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Write implements [io.Writer]. It never returns an error.
|
||||||
|
func (b *StringBuilder) Write(p []byte) (int, error) {
|
||||||
|
b.Buf = append(b.Buf, p...)
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString implements [io.StringWriter]. It never returns an error.
|
||||||
|
func (b *StringBuilder) WriteString(s string) (int, error) {
|
||||||
|
b.Buf = append(b.Buf, s...)
|
||||||
|
return len(s), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteByte implements [io.ByteWriter]. It never returns an error.
|
||||||
|
func (b *StringBuilder) WriteByte(c byte) error {
|
||||||
|
b.Buf = append(b.Buf, c)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset empties the builder but keeps the allocated capacity for reuse.
|
||||||
|
func (b *StringBuilder) Reset() { b.Buf = b.Buf[:0] }
|
||||||
|
|
||||||
|
// Grow ensures there is room for at least n more bytes
|
||||||
|
func (b *StringBuilder) Grow(n int) { b.Buf = slices.Grow(b.Buf, n) }
|
||||||
|
|
||||||
|
func (b *StringBuilder) Len() int { return len(b.Buf) }
|
||||||
|
|
||||||
|
func (b *StringBuilder) AddS(v string) *StringBuilder {
|
||||||
|
b.Buf = append(b.Buf, v...)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *StringBuilder) AddB(v byte) *StringBuilder {
|
||||||
|
b.Buf = append(b.Buf, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *StringBuilder) AddR(v rune) *StringBuilder {
|
||||||
|
b.Buf = utf8.AppendRune(b.Buf, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *StringBuilder) AddBool(v bool) *StringBuilder {
|
||||||
|
b.Buf = strconv.AppendBool(b.Buf, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
// numberBase: 2 <= base <= 36
|
||||||
|
func (b *StringBuilder) AddIExt(v int64, numberBase int) *StringBuilder {
|
||||||
|
b.Buf = strconv.AppendInt(b.Buf, v, numberBase)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
func (b *StringBuilder) AddI(v int64) *StringBuilder { return b.AddIExt(v, 10) }
|
||||||
|
func (b *StringBuilder) AddI32(v int32) *StringBuilder { return b.AddI(int64(v)) }
|
||||||
|
func (b *StringBuilder) AddI16(v int16) *StringBuilder { return b.AddI(int64(v)) }
|
||||||
|
func (b *StringBuilder) AddI8(v int8) *StringBuilder { return b.AddI(int64(v)) }
|
||||||
|
|
||||||
|
// numberBase: 2 <= base <= 36
|
||||||
|
func (b *StringBuilder) AddUExt(v uint64, numberBase int) *StringBuilder {
|
||||||
|
b.Buf = strconv.AppendUint(b.Buf, v, numberBase)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
func (b *StringBuilder) AddU(v uint64) *StringBuilder { return b.AddUExt(v, 10) }
|
||||||
|
func (b *StringBuilder) AddU32(v uint32) *StringBuilder { return b.AddU(uint64(v)) }
|
||||||
|
func (b *StringBuilder) AddU16(v uint16) *StringBuilder { return b.AddU(uint64(v)) }
|
||||||
|
func (b *StringBuilder) AddU8(v uint8) *StringBuilder { return b.AddU(uint64(v)) }
|
||||||
|
|
||||||
|
// fmtChar: see [strconv.FormatFloat]
|
||||||
|
// precision: number of digits after . (-1 for max precision)
|
||||||
|
func (b *StringBuilder) AddF32Ext(v float32, fmtChar byte, precision int) *StringBuilder {
|
||||||
|
b.Buf = strconv.AppendFloat(b.Buf, float64(v), fmtChar, precision, 32)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
func (b *StringBuilder) AddF32(v float32) *StringBuilder {
|
||||||
|
return b.AddF32Ext(v, 'g', -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fmtChar: see [strconv.FormatFloat]
|
||||||
|
// precision: number of digits after . (-1 for max precision)
|
||||||
|
func (b *StringBuilder) AddF64Ext(v float64, fmtChar byte, precision int) *StringBuilder {
|
||||||
|
b.Buf = strconv.AppendFloat(b.Buf, v, fmtChar, precision, 64)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
func (b *StringBuilder) AddF64(v float64) *StringBuilder {
|
||||||
|
return b.AddF64Ext(v, 'g', -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates copy of buffer
|
||||||
|
func (b *StringBuilder) String() string {
|
||||||
|
return string(b.Buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't copy buffer,
|
||||||
|
// so you shouldn't modify b.Buf before using the string
|
||||||
|
func (b *StringBuilder) StringUnsafe() string {
|
||||||
|
return UnsafeBytesToStr(b.Buf)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package string_helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Returns path cleaned from "./", "/./", "//"
|
||||||
|
// or empty string if path is not safe.
|
||||||
|
// Path is considered unsafe if it:
|
||||||
|
// - contains ".." segments
|
||||||
|
// - ends with '.'
|
||||||
|
// - contains `\:*?"<>|`
|
||||||
|
// - contains windows-restricted names COM, CON, ...
|
||||||
|
func ResolveSafePath(decodedPath string) string {
|
||||||
|
var sb StringBuilder
|
||||||
|
sb.Grow(len(decodedPath))
|
||||||
|
var seg string
|
||||||
|
for rest := decodedPath; rest != ""; {
|
||||||
|
seg, rest = nextSegment(rest)
|
||||||
|
if seg == "" || seg == "." {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isSafeSegment(seg) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
sb.AddB('/')
|
||||||
|
sb.AddS(seg)
|
||||||
|
}
|
||||||
|
if sb.Len() == 0 {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
return sb.StringUnsafe()
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextSegment splits off the first '/'-separated segment of s.
|
||||||
|
func nextSegment(s string) (seg, rest string) {
|
||||||
|
seg, rest, _ = strings.Cut(s, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSafeSegment reports whether a single non-empty path segment is safe to
|
||||||
|
// use as a file name.
|
||||||
|
func isSafeSegment(seg string) bool {
|
||||||
|
if seg == ".." {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Windows strips trailing dots "a.txt." would open "a.txt"
|
||||||
|
if last := seg[len(seg)-1]; last == '.' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(seg); i++ {
|
||||||
|
c := seg[i]
|
||||||
|
if c < 0x20 || c == 0x7F {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// invalid in Windows file names, ':' also selects NTFS streams
|
||||||
|
if strings.IndexByte(`\:*?"<>|`, c) >= 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return !isReservedWindowsName(seg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isReservedWindowsName reports whether seg names a Windows device:
|
||||||
|
// "CON", "con.txt", "COM1.tar.gz", ...
|
||||||
|
func isReservedWindowsName(seg string) bool {
|
||||||
|
base := seg
|
||||||
|
if i := strings.IndexByte(seg, '.'); i >= 0 {
|
||||||
|
base = seg[:i]
|
||||||
|
}
|
||||||
|
switch len(base) {
|
||||||
|
case 3:
|
||||||
|
return asciiEqualUpper(base, "CON") || asciiEqualUpper(base, "PRN") ||
|
||||||
|
asciiEqualUpper(base, "AUX") || asciiEqualUpper(base, "NUL")
|
||||||
|
case 4:
|
||||||
|
prefix := base[:3]
|
||||||
|
return (asciiEqualUpper(prefix, "COM") || asciiEqualUpper(prefix, "LPT")) &&
|
||||||
|
'0' <= base[3] && base[3] <= '9'
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiUpper upper-cases ASCII letters only (like std::toupper in the "C" locale).
|
||||||
|
func asciiUpper(c byte) byte {
|
||||||
|
if 'a' <= c && c <= 'z' {
|
||||||
|
return c - ('a' - 'A')
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// asciiEqualUpper compares s with upper (which must be upper-case ASCII), ignoring
|
||||||
|
// ASCII case. Unlike strings.EqualFold it does no Unicode folding, so e.g. the
|
||||||
|
// Kelvin sign 'K' (U+212A) does not match 'K'.
|
||||||
|
func asciiEqualUpper(s, upper string) bool {
|
||||||
|
if len(s) != len(upper) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if asciiUpper(s[i]) != upper[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package string_helper
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func UnsafeGetStrBytes(s string) []byte {
|
||||||
|
pointr := unsafe.StringData(s)
|
||||||
|
return unsafe.Slice(pointr, len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnsafeBytesToStr(b []byte) string {
|
||||||
|
pointr := unsafe.SliceData(b)
|
||||||
|
return unsafe.String(pointr, len(b))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user