mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-22 14:25:45 +00:00
.
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
tags
|
||||
*.pprof
|
||||
*.fasthttp.gz
|
||||
*.fasthttp.br
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
vendor/
|
||||
testdata/fuzz
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# This file contains configuration options for golangci-lint.
|
||||
# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||
|
||||
run:
|
||||
# Timeout for analysis.
|
||||
timeout: 5m
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
- cyclop
|
||||
- depguard
|
||||
- dupl
|
||||
- errname
|
||||
- errorlint
|
||||
- exhaustive
|
||||
- exhaustruct
|
||||
- forcetypeassert
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
- gocognit
|
||||
- goconst
|
||||
- gocyclo
|
||||
- goerr113
|
||||
- gomnd
|
||||
- gosec
|
||||
- inamedparam
|
||||
- ireturn
|
||||
- maintidx
|
||||
- nakedret
|
||||
- nestif
|
||||
- nlreturn
|
||||
- noctx
|
||||
- nonamedreturns
|
||||
- paralleltest
|
||||
- testableexamples
|
||||
- testpackage
|
||||
- thelper
|
||||
- tparallel
|
||||
- unparam
|
||||
- usestdlibvars
|
||||
- varnamelen
|
||||
- wrapcheck
|
||||
- wsl
|
||||
|
||||
# Deprecated linters
|
||||
- deadcode
|
||||
- exhaustivestruct
|
||||
- golint
|
||||
- ifshort
|
||||
- interfacer
|
||||
- maligned
|
||||
- nosnakecase
|
||||
- scopelint
|
||||
- structcheck
|
||||
- varcheck
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md
|
||||
rules:
|
||||
- name: use-any
|
||||
lll:
|
||||
line-length: 130
|
||||
stylecheck:
|
||||
checks: [
|
||||
"all",
|
||||
"-ST1000", # at least one file in a package should have a package comment
|
||||
]
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- experimental
|
||||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- deferInLoop
|
||||
- importShadow
|
||||
- sloppyReassign
|
||||
- unnamedResult
|
||||
- whyNoLint
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
- shadow
|
||||
|
||||
issues:
|
||||
# Show all issues from a linter.
|
||||
max-issues-per-linter: 0
|
||||
|
||||
# Show all issues with the same text.
|
||||
max-same-issues: 0
|
||||
|
||||
include:
|
||||
- EXC0011 # include issues about comments from `stylecheck`
|
||||
|
||||
exclude-rules:
|
||||
# Exclude some linters from running on tests files.
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- lll
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+638
@@ -0,0 +1,638 @@
|
||||
# fasthttp [](https://pkg.go.dev/github.com/valyala/fasthttp) [](https://goreportcard.com/report/github.com/valyala/fasthttp)
|
||||
|
||||

|
||||
|
||||
Fast HTTP implementation for Go.
|
||||
|
||||
# fasthttp might not be for you!
|
||||
fasthttp was designed for some high performance edge cases. **Unless** your server/client needs to handle **thousands of small to medium requests per second** and needs a consistent low millisecond response time fasthttp might not be for you. **For most cases `net/http` is much better** as it's easier to use and can handle more cases. For most cases you won't even notice the performance difference.
|
||||
|
||||
|
||||
## General info and links
|
||||
|
||||
Currently fasthttp is successfully used by [VertaMedia](https://vertamedia.com/)
|
||||
in a production serving up to 200K rps from more than 1.5M concurrent keep-alive
|
||||
connections per physical server.
|
||||
|
||||
[TechEmpower Benchmark round 19 results](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=plaintext)
|
||||
|
||||
[Server Benchmarks](#http-server-performance-comparison-with-nethttp)
|
||||
|
||||
[Client Benchmarks](#http-client-comparison-with-nethttp)
|
||||
|
||||
[Install](#install)
|
||||
|
||||
[Documentation](https://pkg.go.dev/github.com/valyala/fasthttp)
|
||||
|
||||
[Examples from docs](https://pkg.go.dev/github.com/valyala/fasthttp#pkg-examples)
|
||||
|
||||
[Code examples](examples)
|
||||
|
||||
[Awesome fasthttp tools](https://github.com/fasthttp)
|
||||
|
||||
[Switching from net/http to fasthttp](#switching-from-nethttp-to-fasthttp)
|
||||
|
||||
[Fasthttp best practices](#fasthttp-best-practices)
|
||||
|
||||
[Tricks with byte buffers](#tricks-with-byte-buffers)
|
||||
|
||||
[Related projects](#related-projects)
|
||||
|
||||
[FAQ](#faq)
|
||||
|
||||
## HTTP server performance comparison with [net/http](https://pkg.go.dev/net/http)
|
||||
|
||||
In short, fasthttp server is up to 10 times faster than net/http.
|
||||
Below are benchmark results.
|
||||
|
||||
*GOMAXPROCS=1*
|
||||
|
||||
net/http server:
|
||||
```
|
||||
$ GOMAXPROCS=1 go test -bench=NetHTTPServerGet -benchmem -benchtime=10s
|
||||
BenchmarkNetHTTPServerGet1ReqPerConn 1000000 12052 ns/op 2297 B/op 29 allocs/op
|
||||
BenchmarkNetHTTPServerGet2ReqPerConn 1000000 12278 ns/op 2327 B/op 24 allocs/op
|
||||
BenchmarkNetHTTPServerGet10ReqPerConn 2000000 8903 ns/op 2112 B/op 19 allocs/op
|
||||
BenchmarkNetHTTPServerGet10KReqPerConn 2000000 8451 ns/op 2058 B/op 18 allocs/op
|
||||
BenchmarkNetHTTPServerGet1ReqPerConn10KClients 500000 26733 ns/op 3229 B/op 29 allocs/op
|
||||
BenchmarkNetHTTPServerGet2ReqPerConn10KClients 1000000 23351 ns/op 3211 B/op 24 allocs/op
|
||||
BenchmarkNetHTTPServerGet10ReqPerConn10KClients 1000000 13390 ns/op 2483 B/op 19 allocs/op
|
||||
BenchmarkNetHTTPServerGet100ReqPerConn10KClients 1000000 13484 ns/op 2171 B/op 18 allocs/op
|
||||
```
|
||||
|
||||
fasthttp server:
|
||||
```
|
||||
$ GOMAXPROCS=1 go test -bench=kServerGet -benchmem -benchtime=10s
|
||||
BenchmarkServerGet1ReqPerConn 10000000 1559 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet2ReqPerConn 10000000 1248 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10ReqPerConn 20000000 797 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10KReqPerConn 20000000 716 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet1ReqPerConn10KClients 10000000 1974 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet2ReqPerConn10KClients 10000000 1352 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10ReqPerConn10KClients 20000000 789 ns/op 2 B/op 0 allocs/op
|
||||
BenchmarkServerGet100ReqPerConn10KClients 20000000 604 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
*GOMAXPROCS=4*
|
||||
|
||||
net/http server:
|
||||
```
|
||||
$ GOMAXPROCS=4 go test -bench=NetHTTPServerGet -benchmem -benchtime=10s
|
||||
BenchmarkNetHTTPServerGet1ReqPerConn-4 3000000 4529 ns/op 2389 B/op 29 allocs/op
|
||||
BenchmarkNetHTTPServerGet2ReqPerConn-4 5000000 3896 ns/op 2418 B/op 24 allocs/op
|
||||
BenchmarkNetHTTPServerGet10ReqPerConn-4 5000000 3145 ns/op 2160 B/op 19 allocs/op
|
||||
BenchmarkNetHTTPServerGet10KReqPerConn-4 5000000 3054 ns/op 2065 B/op 18 allocs/op
|
||||
BenchmarkNetHTTPServerGet1ReqPerConn10KClients-4 1000000 10321 ns/op 3710 B/op 30 allocs/op
|
||||
BenchmarkNetHTTPServerGet2ReqPerConn10KClients-4 2000000 7556 ns/op 3296 B/op 24 allocs/op
|
||||
BenchmarkNetHTTPServerGet10ReqPerConn10KClients-4 5000000 3905 ns/op 2349 B/op 19 allocs/op
|
||||
BenchmarkNetHTTPServerGet100ReqPerConn10KClients-4 5000000 3435 ns/op 2130 B/op 18 allocs/op
|
||||
```
|
||||
|
||||
fasthttp server:
|
||||
```
|
||||
$ GOMAXPROCS=4 go test -bench=kServerGet -benchmem -benchtime=10s
|
||||
BenchmarkServerGet1ReqPerConn-4 10000000 1141 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet2ReqPerConn-4 20000000 707 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10ReqPerConn-4 30000000 341 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10KReqPerConn-4 50000000 310 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet1ReqPerConn10KClients-4 10000000 1119 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet2ReqPerConn10KClients-4 20000000 644 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet10ReqPerConn10KClients-4 30000000 346 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkServerGet100ReqPerConn10KClients-4 50000000 282 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
## HTTP client comparison with net/http
|
||||
|
||||
In short, fasthttp client is up to 10 times faster than net/http.
|
||||
Below are benchmark results.
|
||||
|
||||
*GOMAXPROCS=1*
|
||||
|
||||
net/http client:
|
||||
```
|
||||
$ GOMAXPROCS=1 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
|
||||
BenchmarkNetHTTPClientDoFastServer 1000000 12567 ns/op 2616 B/op 35 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1TCP 200000 67030 ns/op 5028 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd10TCP 300000 51098 ns/op 5031 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd100TCP 300000 45096 ns/op 5026 B/op 55 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1Inmemory 500000 24779 ns/op 5035 B/op 57 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd10Inmemory 1000000 26425 ns/op 5035 B/op 57 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd100Inmemory 500000 28515 ns/op 5045 B/op 57 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1000Inmemory 500000 39511 ns/op 5096 B/op 56 allocs/op
|
||||
```
|
||||
|
||||
fasthttp client:
|
||||
```
|
||||
$ GOMAXPROCS=1 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
|
||||
BenchmarkClientDoFastServer 20000000 865 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1TCP 1000000 18711 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd10TCP 1000000 14664 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd100TCP 1000000 14043 ns/op 1 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1Inmemory 5000000 3965 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd10Inmemory 3000000 4060 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd100Inmemory 5000000 3396 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1000Inmemory 5000000 3306 ns/op 2 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
*GOMAXPROCS=4*
|
||||
|
||||
net/http client:
|
||||
```
|
||||
$ GOMAXPROCS=4 go test -bench='HTTPClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
|
||||
BenchmarkNetHTTPClientDoFastServer-4 2000000 8774 ns/op 2619 B/op 35 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1TCP-4 500000 22951 ns/op 5047 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd10TCP-4 1000000 19182 ns/op 5037 B/op 55 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd100TCP-4 1000000 16535 ns/op 5031 B/op 55 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1Inmemory-4 1000000 14495 ns/op 5038 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd10Inmemory-4 1000000 10237 ns/op 5034 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd100Inmemory-4 1000000 10125 ns/op 5045 B/op 56 allocs/op
|
||||
BenchmarkNetHTTPClientGetEndToEnd1000Inmemory-4 1000000 11132 ns/op 5136 B/op 56 allocs/op
|
||||
```
|
||||
|
||||
fasthttp client:
|
||||
```
|
||||
$ GOMAXPROCS=4 go test -bench='kClient(Do|GetEndToEnd)' -benchmem -benchtime=10s
|
||||
BenchmarkClientDoFastServer-4 50000000 397 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1TCP-4 2000000 7388 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd10TCP-4 2000000 6689 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd100TCP-4 3000000 4927 ns/op 1 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1Inmemory-4 10000000 1604 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd10Inmemory-4 10000000 1458 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd100Inmemory-4 10000000 1329 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkClientGetEndToEnd1000Inmemory-4 10000000 1316 ns/op 5 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go get -u github.com/valyala/fasthttp
|
||||
```
|
||||
|
||||
|
||||
## Switching from net/http to fasthttp
|
||||
|
||||
Unfortunately, fasthttp doesn't provide API identical to net/http.
|
||||
See the [FAQ](#faq) for details.
|
||||
There is [net/http -> fasthttp handler converter](https://pkg.go.dev/github.com/valyala/fasthttp/fasthttpadaptor),
|
||||
but it is better to write fasthttp request handlers by hand in order to use
|
||||
all of the fasthttp advantages (especially high performance :) ).
|
||||
|
||||
Important points:
|
||||
|
||||
* Fasthttp works with [RequestHandler functions](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler)
|
||||
instead of objects implementing [Handler interface](https://pkg.go.dev/net/http#Handler).
|
||||
Fortunately, it is easy to pass bound struct methods to fasthttp:
|
||||
|
||||
```go
|
||||
type MyHandler struct {
|
||||
foobar string
|
||||
}
|
||||
|
||||
// request handler in net/http style, i.e. method bound to MyHandler struct.
|
||||
func (h *MyHandler) HandleFastHTTP(ctx *fasthttp.RequestCtx) {
|
||||
// notice that we may access MyHandler properties here - see h.foobar.
|
||||
fmt.Fprintf(ctx, "Hello, world! Requested path is %q. Foobar is %q",
|
||||
ctx.Path(), h.foobar)
|
||||
}
|
||||
|
||||
// request handler in fasthttp style, i.e. just plain function.
|
||||
func fastHTTPHandler(ctx *fasthttp.RequestCtx) {
|
||||
fmt.Fprintf(ctx, "Hi there! RequestURI is %q", ctx.RequestURI())
|
||||
}
|
||||
|
||||
// pass bound struct method to fasthttp
|
||||
myHandler := &MyHandler{
|
||||
foobar: "foobar",
|
||||
}
|
||||
fasthttp.ListenAndServe(":8080", myHandler.HandleFastHTTP)
|
||||
|
||||
// pass plain function to fasthttp
|
||||
fasthttp.ListenAndServe(":8081", fastHTTPHandler)
|
||||
```
|
||||
|
||||
* The [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler)
|
||||
accepts only one argument - [RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx).
|
||||
It contains all the functionality required for http request processing
|
||||
and response writing. Below is an example of a simple request handler conversion
|
||||
from net/http to fasthttp.
|
||||
|
||||
```go
|
||||
// net/http request handler
|
||||
requestHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/foo":
|
||||
fooHandler(w, r)
|
||||
case "/bar":
|
||||
barHandler(w, r)
|
||||
default:
|
||||
http.Error(w, "Unsupported path", http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
// the corresponding fasthttp request handler
|
||||
requestHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
switch string(ctx.Path()) {
|
||||
case "/foo":
|
||||
fooHandler(ctx)
|
||||
case "/bar":
|
||||
barHandler(ctx)
|
||||
default:
|
||||
ctx.Error("Unsupported path", fasthttp.StatusNotFound)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* Fasthttp allows setting response headers and writing response body
|
||||
in an arbitrary order. There is no 'headers first, then body' restriction
|
||||
like in net/http. The following code is valid for fasthttp:
|
||||
|
||||
```go
|
||||
requestHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
// set some headers and status code first
|
||||
ctx.SetContentType("foo/bar")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
|
||||
// then write the first part of body
|
||||
fmt.Fprintf(ctx, "this is the first part of body\n")
|
||||
|
||||
// then set more headers
|
||||
ctx.Response.Header.Set("Foo-Bar", "baz")
|
||||
|
||||
// then write more body
|
||||
fmt.Fprintf(ctx, "this is the second part of body\n")
|
||||
|
||||
// then override already written body
|
||||
ctx.SetBody([]byte("this is completely new body contents"))
|
||||
|
||||
// then update status code
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
|
||||
// basically, anything may be updated many times before
|
||||
// returning from RequestHandler.
|
||||
//
|
||||
// Unlike net/http fasthttp doesn't put response to the wire until
|
||||
// returning from RequestHandler.
|
||||
}
|
||||
```
|
||||
|
||||
* Fasthttp doesn't provide [ServeMux](https://pkg.go.dev/net/http#ServeMux),
|
||||
but there are more powerful third-party routers and web frameworks
|
||||
with fasthttp support:
|
||||
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing)
|
||||
* [router](https://github.com/fasthttp/router)
|
||||
* [lu](https://github.com/vincentLiuxiang/lu)
|
||||
* [atreugo](https://github.com/savsgio/atreugo)
|
||||
* [Fiber](https://github.com/gofiber/fiber)
|
||||
* [Gearbox](https://github.com/gogearbox/gearbox)
|
||||
|
||||
Net/http code with simple ServeMux is trivially converted to fasthttp code:
|
||||
|
||||
```go
|
||||
// net/http code
|
||||
|
||||
m := &http.ServeMux{}
|
||||
m.HandleFunc("/foo", fooHandlerFunc)
|
||||
m.HandleFunc("/bar", barHandlerFunc)
|
||||
m.Handle("/baz", bazHandler)
|
||||
|
||||
http.ListenAndServe(":80", m)
|
||||
```
|
||||
|
||||
```go
|
||||
// the corresponding fasthttp code
|
||||
m := func(ctx *fasthttp.RequestCtx) {
|
||||
switch string(ctx.Path()) {
|
||||
case "/foo":
|
||||
fooHandlerFunc(ctx)
|
||||
case "/bar":
|
||||
barHandlerFunc(ctx)
|
||||
case "/baz":
|
||||
bazHandler.HandlerFunc(ctx)
|
||||
default:
|
||||
ctx.Error("not found", fasthttp.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
fasthttp.ListenAndServe(":80", m)
|
||||
```
|
||||
|
||||
* Because creating a new channel for every request is just too expensive, so the channel returned by RequestCtx.Done() is only closed when the server is shutting down.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
fasthttp.ListenAndServe(":8080", fasthttp.TimeoutHandler(func(ctx *fasthttp.RequestCtx) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// ctx.Done() is only closed when the server is shutting down.
|
||||
log.Println("context cancelled")
|
||||
return
|
||||
case <-time.After(10 * time.Second):
|
||||
log.Println("process finished ok")
|
||||
}
|
||||
}, time.Second*2, "timeout"))
|
||||
}
|
||||
```
|
||||
|
||||
* net/http -> fasthttp conversion table:
|
||||
|
||||
* All the pseudocode below assumes w, r and ctx have these types:
|
||||
```go
|
||||
var (
|
||||
w http.ResponseWriter
|
||||
r *http.Request
|
||||
ctx *fasthttp.RequestCtx
|
||||
)
|
||||
```
|
||||
* r.Body -> [ctx.PostBody()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.PostBody)
|
||||
* r.URL.Path -> [ctx.Path()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Path)
|
||||
* r.URL -> [ctx.URI()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.URI)
|
||||
* r.Method -> [ctx.Method()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Method)
|
||||
* r.Header -> [ctx.Request.Header](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHeader)
|
||||
* r.Header.Get() -> [ctx.Request.Header.Peek()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHeader.Peek)
|
||||
* r.Host -> [ctx.Host()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Host)
|
||||
* r.Form -> [ctx.QueryArgs()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.QueryArgs) +
|
||||
[ctx.PostArgs()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.PostArgs)
|
||||
* r.PostForm -> [ctx.PostArgs()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.PostArgs)
|
||||
* r.FormValue() -> [ctx.FormValue()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormValue)
|
||||
* r.FormFile() -> [ctx.FormFile()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.FormFile)
|
||||
* r.MultipartForm -> [ctx.MultipartForm()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.MultipartForm)
|
||||
* r.RemoteAddr -> [ctx.RemoteAddr()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RemoteAddr)
|
||||
* r.RequestURI -> [ctx.RequestURI()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.RequestURI)
|
||||
* r.TLS -> [ctx.IsTLS()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.IsTLS)
|
||||
* r.Cookie() -> [ctx.Request.Header.Cookie()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHeader.Cookie)
|
||||
* r.Referer() -> [ctx.Referer()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Referer)
|
||||
* r.UserAgent() -> [ctx.UserAgent()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.UserAgent)
|
||||
* w.Header() -> [ctx.Response.Header](https://pkg.go.dev/github.com/valyala/fasthttp#ResponseHeader)
|
||||
* w.Header().Set() -> [ctx.Response.Header.Set()](https://pkg.go.dev/github.com/valyala/fasthttp#ResponseHeader.Set)
|
||||
* w.Header().Set("Content-Type") -> [ctx.SetContentType()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.SetContentType)
|
||||
* w.Header().Set("Set-Cookie") -> [ctx.Response.Header.SetCookie()](https://pkg.go.dev/github.com/valyala/fasthttp#ResponseHeader.SetCookie)
|
||||
* w.Write() -> [ctx.Write()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Write),
|
||||
[ctx.SetBody()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.SetBody),
|
||||
[ctx.SetBodyStream()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.SetBodyStream),
|
||||
[ctx.SetBodyStreamWriter()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.SetBodyStreamWriter)
|
||||
* w.WriteHeader() -> [ctx.SetStatusCode()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.SetStatusCode)
|
||||
* w.(http.Hijacker).Hijack() -> [ctx.Hijack()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Hijack)
|
||||
* http.Error() -> [ctx.Error()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Error)
|
||||
* http.FileServer() -> [fasthttp.FSHandler()](https://pkg.go.dev/github.com/valyala/fasthttp#FSHandler),
|
||||
[fasthttp.FS](https://pkg.go.dev/github.com/valyala/fasthttp#FS)
|
||||
* http.ServeFile() -> [fasthttp.ServeFile()](https://pkg.go.dev/github.com/valyala/fasthttp#ServeFile)
|
||||
* http.Redirect() -> [ctx.Redirect()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Redirect)
|
||||
* http.NotFound() -> [ctx.NotFound()](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.NotFound)
|
||||
* http.StripPrefix() -> [fasthttp.PathRewriteFunc](https://pkg.go.dev/github.com/valyala/fasthttp#PathRewriteFunc)
|
||||
|
||||
* *VERY IMPORTANT!* Fasthttp disallows holding references
|
||||
to [RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx) or to its'
|
||||
members after returning from [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler).
|
||||
Otherwise [data races](http://go.dev/blog/race-detector) are inevitable.
|
||||
Carefully inspect all the net/http request handlers converted to fasthttp whether
|
||||
they retain references to RequestCtx or to its' members after returning.
|
||||
RequestCtx provides the following _band aids_ for this case:
|
||||
|
||||
* Wrap RequestHandler into [TimeoutHandler](https://pkg.go.dev/github.com/valyala/fasthttp#TimeoutHandler).
|
||||
* Call [TimeoutError](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from RequestHandler if there are references to RequestCtx or to its' members.
|
||||
See [the example](https://pkg.go.dev/github.com/valyala/fasthttp#example-RequestCtx-TimeoutError)
|
||||
for more details.
|
||||
|
||||
Use this brilliant tool - [race detector](http://go.dev/blog/race-detector) -
|
||||
for detecting and eliminating data races in your program. If you detected
|
||||
data race related to fasthttp in your program, then there is high probability
|
||||
you forgot calling [TimeoutError](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler).
|
||||
|
||||
* Blind switching from net/http to fasthttp won't give you performance boost.
|
||||
While fasthttp is optimized for speed, its' performance may be easily saturated
|
||||
by slow [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler).
|
||||
So [profile](http://go.dev/blog/pprof) and optimize your
|
||||
code after switching to fasthttp. For instance, use [quicktemplate](https://github.com/valyala/quicktemplate)
|
||||
instead of [html/template](https://pkg.go.dev/html/template).
|
||||
|
||||
* See also [fasthttputil](https://pkg.go.dev/github.com/valyala/fasthttp/fasthttputil),
|
||||
[fasthttpadaptor](https://pkg.go.dev/github.com/valyala/fasthttp/fasthttpadaptor) and
|
||||
[expvarhandler](https://pkg.go.dev/github.com/valyala/fasthttp/expvarhandler).
|
||||
|
||||
|
||||
## Performance optimization tips for multi-core systems
|
||||
|
||||
* Use [reuseport](https://pkg.go.dev/github.com/valyala/fasthttp/reuseport) listener.
|
||||
* Run a separate server instance per CPU core with GOMAXPROCS=1.
|
||||
* Pin each server instance to a separate CPU core using [taskset](http://linux.die.net/man/1/taskset).
|
||||
* Ensure the interrupts of multiqueue network card are evenly distributed between CPU cores.
|
||||
See [this article](https://blog.cloudflare.com/how-to-achieve-low-latency/) for details.
|
||||
* Use the latest version of Go as each version contains performance improvements.
|
||||
|
||||
|
||||
## Fasthttp best practices
|
||||
|
||||
* Do not allocate objects and `[]byte` buffers - just reuse them as much
|
||||
as possible. Fasthttp API design encourages this.
|
||||
* [sync.Pool](https://pkg.go.dev/sync#Pool) is your best friend.
|
||||
* [Profile your program](http://go.dev/blog/pprof)
|
||||
in production.
|
||||
`go tool pprof --alloc_objects your-program mem.pprof` usually gives better
|
||||
insights for optimization opportunities than `go tool pprof your-program cpu.pprof`.
|
||||
* Write [tests and benchmarks](https://pkg.go.dev/testing) for hot paths.
|
||||
* Avoid conversion between `[]byte` and `string`, since this may result in memory
|
||||
allocation+copy. Fasthttp API provides functions for both `[]byte` and `string` -
|
||||
use these functions instead of converting manually between `[]byte` and `string`.
|
||||
There are some exceptions - see [this wiki page](https://github.com/golang/go/wiki/CompilerOptimizations#string-and-byte)
|
||||
for more details.
|
||||
* Verify your tests and production code under
|
||||
[race detector](https://go.dev/doc/articles/race_detector.html) on a regular basis.
|
||||
* Prefer [quicktemplate](https://github.com/valyala/quicktemplate) instead of
|
||||
[html/template](https://pkg.go.dev/html/template) in your webserver.
|
||||
|
||||
|
||||
## Tricks with `[]byte` buffers
|
||||
|
||||
The following tricks are used by fasthttp. Use them in your code too.
|
||||
|
||||
* Standard Go functions accept nil buffers
|
||||
```go
|
||||
var (
|
||||
// both buffers are uninitialized
|
||||
dst []byte
|
||||
src []byte
|
||||
)
|
||||
dst = append(dst, src...) // is legal if dst is nil and/or src is nil
|
||||
copy(dst, src) // is legal if dst is nil and/or src is nil
|
||||
(string(src) == "") // is true if src is nil
|
||||
(len(src) == 0) // is true if src is nil
|
||||
src = src[:0] // works like a charm with nil src
|
||||
|
||||
// this for loop doesn't panic if src is nil
|
||||
for i, ch := range src {
|
||||
doSomething(i, ch)
|
||||
}
|
||||
```
|
||||
|
||||
So throw away nil checks for `[]byte` buffers from you code. For example,
|
||||
```go
|
||||
srcLen := 0
|
||||
if src != nil {
|
||||
srcLen = len(src)
|
||||
}
|
||||
```
|
||||
|
||||
becomes
|
||||
|
||||
```go
|
||||
srcLen := len(src)
|
||||
```
|
||||
|
||||
* String may be appended to `[]byte` buffer with `append`
|
||||
```go
|
||||
dst = append(dst, "foobar"...)
|
||||
```
|
||||
|
||||
* `[]byte` buffer may be extended to its' capacity.
|
||||
```go
|
||||
buf := make([]byte, 100)
|
||||
a := buf[:10] // len(a) == 10, cap(a) == 100.
|
||||
b := a[:100] // is valid, since cap(a) == 100.
|
||||
```
|
||||
|
||||
* All fasthttp functions accept nil `[]byte` buffer
|
||||
```go
|
||||
statusCode, body, err := fasthttp.Get(nil, "http://google.com/")
|
||||
uintBuf := fasthttp.AppendUint(nil, 1234)
|
||||
```
|
||||
|
||||
* String and `[]byte` buffers may converted without memory allocations
|
||||
```go
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
func s2b(s string) (b []byte) {
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh.Data = sh.Data
|
||||
bh.Cap = sh.Len
|
||||
bh.Len = sh.Len
|
||||
return b
|
||||
}
|
||||
```
|
||||
|
||||
### Warning:
|
||||
This is an **unsafe** way, the result string and `[]byte` buffer share the same bytes.
|
||||
|
||||
**Please make sure not to modify the bytes in the `[]byte` buffer if the string still survives!**
|
||||
|
||||
## Related projects
|
||||
|
||||
* [fasthttp](https://github.com/fasthttp) - various useful
|
||||
helpers for projects based on fasthttp.
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing) - fast and
|
||||
powerful routing package for fasthttp servers.
|
||||
* [http2](https://github.com/dgrr/http2) - HTTP/2 implementation for fasthttp.
|
||||
* [router](https://github.com/fasthttp/router) - a high
|
||||
performance fasthttp request router that scales well.
|
||||
* [fastws](https://github.com/fasthttp/fastws) - Bloatless WebSocket package made for fasthttp
|
||||
to handle Read/Write operations concurrently.
|
||||
* [gramework](https://github.com/gramework/gramework) - a web framework made by one of fasthttp maintainers
|
||||
* [lu](https://github.com/vincentLiuxiang/lu) - a high performance
|
||||
go middleware web framework which is based on fasthttp.
|
||||
* [websocket](https://github.com/fasthttp/websocket) - Gorilla-based
|
||||
websocket implementation for fasthttp.
|
||||
* [websocket](https://github.com/dgrr/websocket) - Event-based high-performance WebSocket library for zero-allocation
|
||||
websocket servers and clients.
|
||||
* [fasthttpsession](https://github.com/phachon/fasthttpsession) - a fast and powerful session package for fasthttp servers.
|
||||
* [atreugo](https://github.com/savsgio/atreugo) - High performance and extensible micro web framework with zero memory allocations in hot paths.
|
||||
* [kratgo](https://github.com/savsgio/kratgo) - Simple, lightweight and ultra-fast HTTP Cache to speed up your websites.
|
||||
* [kit-plugins](https://github.com/wencan/kit-plugins/tree/master/transport/fasthttp) - go-kit transport implementation for fasthttp.
|
||||
* [Fiber](https://github.com/gofiber/fiber) - An Expressjs inspired web framework running on Fasthttp
|
||||
* [Gearbox](https://github.com/gogearbox/gearbox) - :gear: gearbox is a web framework written in Go with a focus on high performance and memory optimization
|
||||
* [http2curl](https://github.com/li-jin-gou/http2curl) - A tool to convert fasthttp requests to curl command line
|
||||
|
||||
## FAQ
|
||||
|
||||
* *Why creating yet another http package instead of optimizing net/http?*
|
||||
|
||||
Because net/http API limits many optimization opportunities.
|
||||
For example:
|
||||
* net/http Request object lifetime isn't limited by request handler execution
|
||||
time. So the server must create a new request object per each request instead
|
||||
of reusing existing objects like fasthttp does.
|
||||
* net/http headers are stored in a `map[string][]string`. So the server
|
||||
must parse all the headers, convert them from `[]byte` to `string` and put
|
||||
them into the map before calling user-provided request handler.
|
||||
This all requires unnecessary memory allocations avoided by fasthttp.
|
||||
* net/http client API requires creating a new response object per each request.
|
||||
|
||||
* *Why fasthttp API is incompatible with net/http?*
|
||||
|
||||
Because net/http API limits many optimization opportunities. See the answer
|
||||
above for more details. Also certain net/http API parts are suboptimal
|
||||
for use:
|
||||
* Compare [net/http connection hijacking](https://pkg.go.dev/net/http#Hijacker)
|
||||
to [fasthttp connection hijacking](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Hijack).
|
||||
* Compare [net/http Request.Body reading](https://pkg.go.dev/net/http#Request)
|
||||
to [fasthttp request body reading](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.PostBody).
|
||||
|
||||
* *Why fasthttp doesn't support HTTP/2.0 and WebSockets?*
|
||||
|
||||
[HTTP/2.0 support](https://github.com/fasthttp/http2) is in progress. [WebSockets](https://github.com/fasthttp/websockets) has been done already.
|
||||
Third parties also may use [RequestCtx.Hijack](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.Hijack)
|
||||
for implementing these goodies.
|
||||
|
||||
* *Are there known net/http advantages comparing to fasthttp?*
|
||||
|
||||
Yes:
|
||||
* net/http supports [HTTP/2.0 starting from go1.6](https://pkg.go.dev/golang.org/x/net/http2).
|
||||
* net/http API is stable, while fasthttp API constantly evolves.
|
||||
* net/http handles more HTTP corner cases.
|
||||
* net/http can stream both request and response bodies
|
||||
* net/http can handle bigger bodies as it doesn't read the whole body into memory
|
||||
* net/http should contain less bugs, since it is used and tested by much
|
||||
wider audience.
|
||||
|
||||
* *Why fasthttp API prefers returning `[]byte` instead of `string`?*
|
||||
|
||||
Because `[]byte` to `string` conversion isn't free - it requires memory
|
||||
allocation and copy. Feel free wrapping returned `[]byte` result into
|
||||
`string()` if you prefer working with strings instead of byte slices.
|
||||
But be aware that this has non-zero overhead.
|
||||
|
||||
* *Which GO versions are supported by fasthttp?*
|
||||
|
||||
Go 1.18.x. Older versions won't be supported.
|
||||
|
||||
* *Please provide real benchmark data and server information*
|
||||
|
||||
See [this issue](https://github.com/valyala/fasthttp/issues/4).
|
||||
|
||||
* *Are there plans to add request routing to fasthttp?*
|
||||
|
||||
There are no plans to add request routing into fasthttp.
|
||||
Use third-party routers and web frameworks with fasthttp support:
|
||||
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing)
|
||||
* [router](https://github.com/fasthttp/router)
|
||||
* [gramework](https://github.com/gramework/gramework)
|
||||
* [lu](https://github.com/vincentLiuxiang/lu)
|
||||
* [atreugo](https://github.com/savsgio/atreugo)
|
||||
* [Fiber](https://github.com/gofiber/fiber)
|
||||
* [Gearbox](https://github.com/gogearbox/gearbox)
|
||||
|
||||
See also [this issue](https://github.com/valyala/fasthttp/issues/9) for more info.
|
||||
|
||||
* *I detected data race in fasthttp!*
|
||||
|
||||
Cool! [File a bug](https://github.com/valyala/fasthttp/issues/new). But before
|
||||
doing this check the following in your code:
|
||||
|
||||
* Make sure there are no references to [RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx)
|
||||
or to its' members after returning from [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler).
|
||||
* Make sure you call [TimeoutError](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from [RequestHandler](https://pkg.go.dev/github.com/valyala/fasthttp#RequestHandler)
|
||||
if there are references to [RequestCtx](https://pkg.go.dev/github.com/valyala/fasthttp#RequestCtx)
|
||||
or to its' members, which may be accessed by other goroutines.
|
||||
|
||||
* *I didn't find an answer for my question here*
|
||||
|
||||
Try exploring [these questions](https://github.com/valyala/fasthttp/issues?q=label%3Aquestion).
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
### TL;DR
|
||||
|
||||
We use a simplified version of [Golang Security Policy](https://go.dev/security).
|
||||
For example, for now we skip CVE assignment.
|
||||
|
||||
### Reporting a Security Bug
|
||||
|
||||
Please report to us any issues you find. This document explains how to do that and what to expect in return.
|
||||
|
||||
All security bugs in our releases should be reported by email to erik@dubbelboer.com
|
||||
Your email will be acknowledged within 24 hours, and you'll receive a more detailed response
|
||||
to your email within 72 hours indicating the next steps in handling your report.
|
||||
Please use a descriptive subject line for your report email.
|
||||
|
||||
### Flagging Existing Issues as Security-related
|
||||
|
||||
If you believe that an existing issue is security-related, we ask that you send an email to erik@dubbelboer.com
|
||||
The email should include the issue ID and a short description of why it should be handled according to this security policy.
|
||||
|
||||
### Disclosure Process
|
||||
|
||||
Our project uses the following disclosure process:
|
||||
|
||||
- Once the security report is received it is assigned a primary handler. This person coordinates the fix and release process.
|
||||
- The issue is confirmed and a list of affected software is determined.
|
||||
- Code is audited to find any potential similar problems.
|
||||
- Fixes are prepared for the two most recent major releases and the head/master revision. These fixes are not yet committed to the public repository.
|
||||
- To notify users, a new issue without security details is submitted to our GitHub repository.
|
||||
- Three working days following this notification, the fixes are applied to the public repository and a new release is issued.
|
||||
- On the date that the fixes are applied, announcement is published in the issue.
|
||||
|
||||
This process can take some time, especially when coordination is required with maintainers of other projects.
|
||||
Every effort will be made to handle the bug in as timely a manner as possible, however it's important that we follow
|
||||
the process described above to ensure that disclosures are handled consistently.
|
||||
|
||||
### Receiving Security Updates
|
||||
The best way to receive security announcements is to subscribe ("Watch") to our repository.
|
||||
Any GitHub issues pertaining to a security issue will be prefixed with [security].
|
||||
|
||||
### Comments on This Policy
|
||||
If you have any suggestions to improve this policy, please send an email to erik@dubbelboer.com for discussion.
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
- SessionClient with referer and cookies support.
|
||||
- ProxyHandler similar to FSHandler.
|
||||
- WebSockets. See https://tools.ietf.org/html/rfc6455 .
|
||||
- HTTP/2.0. See https://tools.ietf.org/html/rfc7540 .
|
||||
+644
@@ -0,0 +1,644 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
const (
|
||||
argsNoValue = true
|
||||
argsHasValue = false
|
||||
)
|
||||
|
||||
// AcquireArgs returns an empty Args object from the pool.
|
||||
//
|
||||
// The returned Args may be returned to the pool with ReleaseArgs
|
||||
// when no longer needed. This allows reducing GC load.
|
||||
func AcquireArgs() *Args {
|
||||
return argsPool.Get().(*Args)
|
||||
}
|
||||
|
||||
// ReleaseArgs returns the object acquired via AcquireArgs to the pool.
|
||||
//
|
||||
// Do not access the released Args object, otherwise data races may occur.
|
||||
func ReleaseArgs(a *Args) {
|
||||
a.Reset()
|
||||
argsPool.Put(a)
|
||||
}
|
||||
|
||||
var argsPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &Args{}
|
||||
},
|
||||
}
|
||||
|
||||
// Args represents query arguments.
|
||||
//
|
||||
// It is forbidden copying Args instances. Create new instances instead
|
||||
// and use CopyTo().
|
||||
//
|
||||
// Args instance MUST NOT be used from concurrently running goroutines.
|
||||
type Args struct {
|
||||
noCopy noCopy
|
||||
|
||||
args []argsKV
|
||||
buf []byte
|
||||
}
|
||||
|
||||
type argsKV struct {
|
||||
key []byte
|
||||
value []byte
|
||||
noValue bool
|
||||
}
|
||||
|
||||
// Reset clears query args.
|
||||
func (a *Args) Reset() {
|
||||
a.args = a.args[:0]
|
||||
}
|
||||
|
||||
// CopyTo copies all args to dst.
|
||||
func (a *Args) CopyTo(dst *Args) {
|
||||
dst.args = copyArgs(dst.args, a.args)
|
||||
}
|
||||
|
||||
// VisitAll calls f for each existing arg.
|
||||
//
|
||||
// f must not retain references to key and value after returning.
|
||||
// Make key and/or value copies if you need storing them after returning.
|
||||
func (a *Args) VisitAll(f func(key, value []byte)) {
|
||||
visitArgs(a.args, f)
|
||||
}
|
||||
|
||||
// Len returns the number of query args.
|
||||
func (a *Args) Len() int {
|
||||
return len(a.args)
|
||||
}
|
||||
|
||||
// Parse parses the given string containing query args.
|
||||
func (a *Args) Parse(s string) {
|
||||
a.buf = append(a.buf[:0], s...)
|
||||
a.ParseBytes(a.buf)
|
||||
}
|
||||
|
||||
// ParseBytes parses the given b containing query args.
|
||||
func (a *Args) ParseBytes(b []byte) {
|
||||
a.Reset()
|
||||
|
||||
var s argsScanner
|
||||
s.b = b
|
||||
|
||||
var kv *argsKV
|
||||
a.args, kv = allocArg(a.args)
|
||||
for s.next(kv) {
|
||||
if len(kv.key) > 0 || len(kv.value) > 0 {
|
||||
a.args, kv = allocArg(a.args)
|
||||
}
|
||||
}
|
||||
a.args = releaseArg(a.args)
|
||||
}
|
||||
|
||||
// String returns string representation of query args.
|
||||
func (a *Args) String() string {
|
||||
return string(a.QueryString())
|
||||
}
|
||||
|
||||
// QueryString returns query string for the args.
|
||||
//
|
||||
// The returned value is valid until the Args is reused or released (ReleaseArgs).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (a *Args) QueryString() []byte {
|
||||
a.buf = a.AppendBytes(a.buf[:0])
|
||||
return a.buf
|
||||
}
|
||||
|
||||
// Sort sorts Args by key and then value using 'f' as comparison function.
|
||||
//
|
||||
// For example args.Sort(bytes.Compare).
|
||||
func (a *Args) Sort(f func(x, y []byte) int) {
|
||||
sort.SliceStable(a.args, func(i, j int) bool {
|
||||
n := f(a.args[i].key, a.args[j].key)
|
||||
if n == 0 {
|
||||
return f(a.args[i].value, a.args[j].value) == -1
|
||||
}
|
||||
return n == -1
|
||||
})
|
||||
}
|
||||
|
||||
// AppendBytes appends query string to dst and returns the extended dst.
|
||||
func (a *Args) AppendBytes(dst []byte) []byte {
|
||||
for i, n := 0, len(a.args); i < n; i++ {
|
||||
kv := &a.args[i]
|
||||
dst = AppendQuotedArg(dst, kv.key)
|
||||
if !kv.noValue {
|
||||
dst = append(dst, '=')
|
||||
if len(kv.value) > 0 {
|
||||
dst = AppendQuotedArg(dst, kv.value)
|
||||
}
|
||||
}
|
||||
if i+1 < n {
|
||||
dst = append(dst, '&')
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// WriteTo writes query string to w.
|
||||
//
|
||||
// WriteTo implements io.WriterTo interface.
|
||||
func (a *Args) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(a.QueryString())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Del deletes argument with the given key from query args.
|
||||
func (a *Args) Del(key string) {
|
||||
a.args = delAllArgs(a.args, key)
|
||||
}
|
||||
|
||||
// DelBytes deletes argument with the given key from query args.
|
||||
func (a *Args) DelBytes(key []byte) {
|
||||
a.args = delAllArgs(a.args, b2s(key))
|
||||
}
|
||||
|
||||
// Add adds 'key=value' argument.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) Add(key, value string) {
|
||||
a.args = appendArg(a.args, key, value, argsHasValue)
|
||||
}
|
||||
|
||||
// AddBytesK adds 'key=value' argument.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) AddBytesK(key []byte, value string) {
|
||||
a.args = appendArg(a.args, b2s(key), value, argsHasValue)
|
||||
}
|
||||
|
||||
// AddBytesV adds 'key=value' argument.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) AddBytesV(key string, value []byte) {
|
||||
a.args = appendArg(a.args, key, b2s(value), argsHasValue)
|
||||
}
|
||||
|
||||
// AddBytesKV adds 'key=value' argument.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) AddBytesKV(key, value []byte) {
|
||||
a.args = appendArg(a.args, b2s(key), b2s(value), argsHasValue)
|
||||
}
|
||||
|
||||
// AddNoValue adds only 'key' as argument without the '='.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) AddNoValue(key string) {
|
||||
a.args = appendArg(a.args, key, "", argsNoValue)
|
||||
}
|
||||
|
||||
// AddBytesKNoValue adds only 'key' as argument without the '='.
|
||||
//
|
||||
// Multiple values for the same key may be added.
|
||||
func (a *Args) AddBytesKNoValue(key []byte) {
|
||||
a.args = appendArg(a.args, b2s(key), "", argsNoValue)
|
||||
}
|
||||
|
||||
// Set sets 'key=value' argument.
|
||||
func (a *Args) Set(key, value string) {
|
||||
a.args = setArg(a.args, key, value, argsHasValue)
|
||||
}
|
||||
|
||||
// SetBytesK sets 'key=value' argument.
|
||||
func (a *Args) SetBytesK(key []byte, value string) {
|
||||
a.args = setArg(a.args, b2s(key), value, argsHasValue)
|
||||
}
|
||||
|
||||
// SetBytesV sets 'key=value' argument.
|
||||
func (a *Args) SetBytesV(key string, value []byte) {
|
||||
a.args = setArg(a.args, key, b2s(value), argsHasValue)
|
||||
}
|
||||
|
||||
// SetBytesKV sets 'key=value' argument.
|
||||
func (a *Args) SetBytesKV(key, value []byte) {
|
||||
a.args = setArgBytes(a.args, key, value, argsHasValue)
|
||||
}
|
||||
|
||||
// SetNoValue sets only 'key' as argument without the '='.
|
||||
//
|
||||
// Only key in argument, like key1&key2.
|
||||
func (a *Args) SetNoValue(key string) {
|
||||
a.args = setArg(a.args, key, "", argsNoValue)
|
||||
}
|
||||
|
||||
// SetBytesKNoValue sets 'key' argument.
|
||||
func (a *Args) SetBytesKNoValue(key []byte) {
|
||||
a.args = setArg(a.args, b2s(key), "", argsNoValue)
|
||||
}
|
||||
|
||||
// Peek returns query arg value for the given key.
|
||||
//
|
||||
// The returned value is valid until the Args is reused or released (ReleaseArgs).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (a *Args) Peek(key string) []byte {
|
||||
return peekArgStr(a.args, key)
|
||||
}
|
||||
|
||||
// PeekBytes returns query arg value for the given key.
|
||||
//
|
||||
// The returned value is valid until the Args is reused or released (ReleaseArgs).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (a *Args) PeekBytes(key []byte) []byte {
|
||||
return peekArgBytes(a.args, key)
|
||||
}
|
||||
|
||||
// PeekMulti returns all the arg values for the given key.
|
||||
func (a *Args) PeekMulti(key string) [][]byte {
|
||||
var values [][]byte
|
||||
a.VisitAll(func(k, v []byte) {
|
||||
if string(k) == key {
|
||||
values = append(values, v)
|
||||
}
|
||||
})
|
||||
return values
|
||||
}
|
||||
|
||||
// PeekMultiBytes returns all the arg values for the given key.
|
||||
func (a *Args) PeekMultiBytes(key []byte) [][]byte {
|
||||
return a.PeekMulti(b2s(key))
|
||||
}
|
||||
|
||||
// Has returns true if the given key exists in Args.
|
||||
func (a *Args) Has(key string) bool {
|
||||
return hasArg(a.args, key)
|
||||
}
|
||||
|
||||
// HasBytes returns true if the given key exists in Args.
|
||||
func (a *Args) HasBytes(key []byte) bool {
|
||||
return hasArg(a.args, b2s(key))
|
||||
}
|
||||
|
||||
// ErrNoArgValue is returned when Args value with the given key is missing.
|
||||
var ErrNoArgValue = errors.New("no Args value for the given key")
|
||||
|
||||
// GetUint returns uint value for the given key.
|
||||
func (a *Args) GetUint(key string) (int, error) {
|
||||
value := a.Peek(key)
|
||||
if len(value) == 0 {
|
||||
return -1, ErrNoArgValue
|
||||
}
|
||||
return ParseUint(value)
|
||||
}
|
||||
|
||||
// SetUint sets uint value for the given key.
|
||||
func (a *Args) SetUint(key string, value int) {
|
||||
bb := bytebufferpool.Get()
|
||||
bb.B = AppendUint(bb.B[:0], value)
|
||||
a.SetBytesV(key, bb.B)
|
||||
bytebufferpool.Put(bb)
|
||||
}
|
||||
|
||||
// SetUintBytes sets uint value for the given key.
|
||||
func (a *Args) SetUintBytes(key []byte, value int) {
|
||||
a.SetUint(b2s(key), value)
|
||||
}
|
||||
|
||||
// GetUintOrZero returns uint value for the given key.
|
||||
//
|
||||
// Zero (0) is returned on error.
|
||||
func (a *Args) GetUintOrZero(key string) int {
|
||||
n, err := a.GetUint(key)
|
||||
if err != nil {
|
||||
n = 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// GetUfloat returns ufloat value for the given key.
|
||||
func (a *Args) GetUfloat(key string) (float64, error) {
|
||||
value := a.Peek(key)
|
||||
if len(value) == 0 {
|
||||
return -1, ErrNoArgValue
|
||||
}
|
||||
return ParseUfloat(value)
|
||||
}
|
||||
|
||||
// GetUfloatOrZero returns ufloat value for the given key.
|
||||
//
|
||||
// Zero (0) is returned on error.
|
||||
func (a *Args) GetUfloatOrZero(key string) float64 {
|
||||
f, err := a.GetUfloat(key)
|
||||
if err != nil {
|
||||
f = 0
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
// GetBool returns boolean value for the given key.
|
||||
//
|
||||
// true is returned for "1", "t", "T", "true", "TRUE", "True", "y", "yes", "Y", "YES", "Yes",
|
||||
// otherwise false is returned.
|
||||
func (a *Args) GetBool(key string) bool {
|
||||
switch string(a.Peek(key)) {
|
||||
// Support the same true cases as strconv.ParseBool
|
||||
// See: https://github.com/golang/go/blob/4e1b11e2c9bdb0ddea1141eed487be1a626ff5be/src/strconv/atob.go#L12
|
||||
// and Y and Yes versions.
|
||||
case "1", "t", "T", "true", "TRUE", "True", "y", "yes", "Y", "YES", "Yes":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func visitArgs(args []argsKV, f func(k, v []byte)) {
|
||||
for i, n := 0, len(args); i < n; i++ {
|
||||
kv := &args[i]
|
||||
f(kv.key, kv.value)
|
||||
}
|
||||
}
|
||||
|
||||
func visitArgsKey(args []argsKV, f func(k []byte)) {
|
||||
for i, n := 0, len(args); i < n; i++ {
|
||||
kv := &args[i]
|
||||
f(kv.key)
|
||||
}
|
||||
}
|
||||
|
||||
func copyArgs(dst, src []argsKV) []argsKV {
|
||||
if cap(dst) < len(src) {
|
||||
tmp := make([]argsKV, len(src))
|
||||
dstLen := len(dst)
|
||||
dst = dst[:cap(dst)] // copy all of dst.
|
||||
copy(tmp, dst)
|
||||
for i := dstLen; i < len(tmp); i++ {
|
||||
// Make sure nothing is nil.
|
||||
tmp[i].key = []byte{}
|
||||
tmp[i].value = []byte{}
|
||||
}
|
||||
dst = tmp
|
||||
}
|
||||
n := len(src)
|
||||
dst = dst[:n]
|
||||
for i := 0; i < n; i++ {
|
||||
dstKV := &dst[i]
|
||||
srcKV := &src[i]
|
||||
dstKV.key = append(dstKV.key[:0], srcKV.key...)
|
||||
if srcKV.noValue {
|
||||
dstKV.value = dstKV.value[:0]
|
||||
} else {
|
||||
dstKV.value = append(dstKV.value[:0], srcKV.value...)
|
||||
}
|
||||
dstKV.noValue = srcKV.noValue
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func delAllArgsBytes(args []argsKV, key []byte) []argsKV {
|
||||
return delAllArgs(args, b2s(key))
|
||||
}
|
||||
|
||||
func delAllArgs(args []argsKV, key string) []argsKV {
|
||||
for i, n := 0, len(args); i < n; i++ {
|
||||
kv := &args[i]
|
||||
if key == string(kv.key) {
|
||||
tmp := *kv
|
||||
copy(args[i:], args[i+1:])
|
||||
n--
|
||||
i--
|
||||
args[n] = tmp
|
||||
args = args[:n]
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func setArgBytes(h []argsKV, key, value []byte, noValue bool) []argsKV {
|
||||
return setArg(h, b2s(key), b2s(value), noValue)
|
||||
}
|
||||
|
||||
func setArg(h []argsKV, key, value string, noValue bool) []argsKV {
|
||||
n := len(h)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &h[i]
|
||||
if key == string(kv.key) {
|
||||
if noValue {
|
||||
kv.value = kv.value[:0]
|
||||
} else {
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
}
|
||||
kv.noValue = noValue
|
||||
return h
|
||||
}
|
||||
}
|
||||
return appendArg(h, key, value, noValue)
|
||||
}
|
||||
|
||||
func appendArgBytes(h []argsKV, key, value []byte, noValue bool) []argsKV {
|
||||
return appendArg(h, b2s(key), b2s(value), noValue)
|
||||
}
|
||||
|
||||
func appendArg(args []argsKV, key, value string, noValue bool) []argsKV {
|
||||
var kv *argsKV
|
||||
args, kv = allocArg(args)
|
||||
kv.key = append(kv.key[:0], key...)
|
||||
if noValue {
|
||||
kv.value = kv.value[:0]
|
||||
} else {
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
}
|
||||
kv.noValue = noValue
|
||||
return args
|
||||
}
|
||||
|
||||
func allocArg(h []argsKV) ([]argsKV, *argsKV) {
|
||||
n := len(h)
|
||||
if cap(h) > n {
|
||||
h = h[:n+1]
|
||||
} else {
|
||||
h = append(h, argsKV{
|
||||
value: []byte{},
|
||||
})
|
||||
}
|
||||
return h, &h[n]
|
||||
}
|
||||
|
||||
func releaseArg(h []argsKV) []argsKV {
|
||||
return h[:len(h)-1]
|
||||
}
|
||||
|
||||
func hasArg(h []argsKV, key string) bool {
|
||||
for i, n := 0, len(h); i < n; i++ {
|
||||
kv := &h[i]
|
||||
if key == string(kv.key) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func peekArgBytes(h []argsKV, k []byte) []byte {
|
||||
for i, n := 0, len(h); i < n; i++ {
|
||||
kv := &h[i]
|
||||
if bytes.Equal(kv.key, k) {
|
||||
return kv.value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func peekArgStr(h []argsKV, k string) []byte {
|
||||
for i, n := 0, len(h); i < n; i++ {
|
||||
kv := &h[i]
|
||||
if string(kv.key) == k {
|
||||
return kv.value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type argsScanner struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (s *argsScanner) next(kv *argsKV) bool {
|
||||
if len(s.b) == 0 {
|
||||
return false
|
||||
}
|
||||
kv.noValue = argsHasValue
|
||||
|
||||
isKey := true
|
||||
k := 0
|
||||
for i, c := range s.b {
|
||||
switch c {
|
||||
case '=':
|
||||
if isKey {
|
||||
isKey = false
|
||||
kv.key = decodeArgAppend(kv.key[:0], s.b[:i])
|
||||
k = i + 1
|
||||
}
|
||||
case '&':
|
||||
if isKey {
|
||||
kv.key = decodeArgAppend(kv.key[:0], s.b[:i])
|
||||
kv.value = kv.value[:0]
|
||||
kv.noValue = argsNoValue
|
||||
} else {
|
||||
kv.value = decodeArgAppend(kv.value[:0], s.b[k:i])
|
||||
}
|
||||
s.b = s.b[i+1:]
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isKey {
|
||||
kv.key = decodeArgAppend(kv.key[:0], s.b)
|
||||
kv.value = kv.value[:0]
|
||||
kv.noValue = argsNoValue
|
||||
} else {
|
||||
kv.value = decodeArgAppend(kv.value[:0], s.b[k:])
|
||||
}
|
||||
s.b = s.b[len(s.b):]
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeArgAppend(dst, src []byte) []byte {
|
||||
idxPercent := bytes.IndexByte(src, '%')
|
||||
idxPlus := bytes.IndexByte(src, '+')
|
||||
if idxPercent == -1 && idxPlus == -1 {
|
||||
// fast path: src doesn't contain encoded chars
|
||||
return append(dst, src...)
|
||||
}
|
||||
|
||||
var idx int
|
||||
switch {
|
||||
case idxPercent == -1:
|
||||
idx = idxPlus
|
||||
case idxPlus == -1:
|
||||
idx = idxPercent
|
||||
case idxPercent > idxPlus:
|
||||
idx = idxPlus
|
||||
default:
|
||||
idx = idxPercent
|
||||
}
|
||||
|
||||
dst = append(dst, src[:idx]...)
|
||||
|
||||
// slow path
|
||||
for i := idx; i < len(src); i++ {
|
||||
c := src[i]
|
||||
switch c {
|
||||
case '%':
|
||||
if i+2 >= len(src) {
|
||||
return append(dst, src[i:]...)
|
||||
}
|
||||
x2 := hex2intTable[src[i+2]]
|
||||
x1 := hex2intTable[src[i+1]]
|
||||
if x1 == 16 || x2 == 16 {
|
||||
dst = append(dst, '%')
|
||||
} else {
|
||||
dst = append(dst, x1<<4|x2)
|
||||
i += 2
|
||||
}
|
||||
case '+':
|
||||
dst = append(dst, ' ')
|
||||
default:
|
||||
dst = append(dst, c)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// decodeArgAppendNoPlus is almost identical to decodeArgAppend, but it doesn't
|
||||
// substitute '+' with ' '.
|
||||
//
|
||||
// The function is copy-pasted from decodeArgAppend due to the performance
|
||||
// reasons only.
|
||||
func decodeArgAppendNoPlus(dst, src []byte) []byte {
|
||||
idx := bytes.IndexByte(src, '%')
|
||||
if idx < 0 {
|
||||
// fast path: src doesn't contain encoded chars
|
||||
return append(dst, src...)
|
||||
}
|
||||
dst = append(dst, src[:idx]...)
|
||||
|
||||
// slow path
|
||||
for i := idx; i < len(src); i++ {
|
||||
c := src[i]
|
||||
if c == '%' {
|
||||
if i+2 >= len(src) {
|
||||
return append(dst, src[i:]...)
|
||||
}
|
||||
x2 := hex2intTable[src[i+2]]
|
||||
x1 := hex2intTable[src[i+1]]
|
||||
if x1 == 16 || x2 == 16 {
|
||||
dst = append(dst, '%')
|
||||
} else {
|
||||
dst = append(dst, x1<<4|x2)
|
||||
i += 2
|
||||
}
|
||||
} else {
|
||||
dst = append(dst, c)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func peekAllArgBytesToDst(dst [][]byte, h []argsKV, k []byte) [][]byte {
|
||||
for i, n := 0, len(h); i < n; i++ {
|
||||
kv := &h[i]
|
||||
if bytes.Equal(kv.key, k) {
|
||||
dst = append(dst, kv.value)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func peekArgsKeys(dst [][]byte, h []argsKV) [][]byte {
|
||||
for i, n := 0, len(h); i < n; i++ {
|
||||
kv := &h[i]
|
||||
dst = append(dst, kv.key)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
//go:build go1.20
|
||||
|
||||
package fasthttp
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
func b2s(b []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(b), len(b))
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
//go:build !go1.20
|
||||
|
||||
package fasthttp
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// b2s converts byte slice to a string without memory allocation.
|
||||
// See https://groups.google.com/forum/#!msg/Golang-Nuts/ENgbUzYvCuU/90yGx7GUAgAJ .
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func b2s(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"github.com/valyala/fasthttp/stackless"
|
||||
)
|
||||
|
||||
// Supported compression levels.
|
||||
const (
|
||||
CompressBrotliNoCompression = 0
|
||||
CompressBrotliBestSpeed = brotli.BestSpeed
|
||||
CompressBrotliBestCompression = brotli.BestCompression
|
||||
|
||||
// Choose a default brotli compression level comparable to
|
||||
// CompressDefaultCompression (gzip 6)
|
||||
// See: https://github.com/valyala/fasthttp/issues/798#issuecomment-626293806
|
||||
CompressBrotliDefaultCompression = 4
|
||||
)
|
||||
|
||||
func acquireBrotliReader(r io.Reader) (*brotli.Reader, error) {
|
||||
v := brotliReaderPool.Get()
|
||||
if v == nil {
|
||||
return brotli.NewReader(r), nil
|
||||
}
|
||||
zr := v.(*brotli.Reader)
|
||||
if err := zr.Reset(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
func releaseBrotliReader(zr *brotli.Reader) {
|
||||
brotliReaderPool.Put(zr)
|
||||
}
|
||||
|
||||
var brotliReaderPool sync.Pool
|
||||
|
||||
func acquireStacklessBrotliWriter(w io.Writer, level int) stackless.Writer {
|
||||
nLevel := normalizeBrotliCompressLevel(level)
|
||||
p := stacklessBrotliWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
return stackless.NewWriter(w, func(w io.Writer) stackless.Writer {
|
||||
return acquireRealBrotliWriter(w, level)
|
||||
})
|
||||
}
|
||||
sw := v.(stackless.Writer)
|
||||
sw.Reset(w)
|
||||
return sw
|
||||
}
|
||||
|
||||
func releaseStacklessBrotliWriter(sw stackless.Writer, level int) {
|
||||
sw.Close()
|
||||
nLevel := normalizeBrotliCompressLevel(level)
|
||||
p := stacklessBrotliWriterPoolMap[nLevel]
|
||||
p.Put(sw)
|
||||
}
|
||||
|
||||
func acquireRealBrotliWriter(w io.Writer, level int) *brotli.Writer {
|
||||
nLevel := normalizeBrotliCompressLevel(level)
|
||||
p := realBrotliWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
zw := brotli.NewWriterLevel(w, level)
|
||||
return zw
|
||||
}
|
||||
zw := v.(*brotli.Writer)
|
||||
zw.Reset(w)
|
||||
return zw
|
||||
}
|
||||
|
||||
func releaseRealBrotliWriter(zw *brotli.Writer, level int) {
|
||||
zw.Close()
|
||||
nLevel := normalizeBrotliCompressLevel(level)
|
||||
p := realBrotliWriterPoolMap[nLevel]
|
||||
p.Put(zw)
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessBrotliWriterPoolMap = newCompressWriterPoolMap()
|
||||
realBrotliWriterPoolMap = newCompressWriterPoolMap()
|
||||
)
|
||||
|
||||
// AppendBrotliBytesLevel appends brotlied src to dst using the given
|
||||
// compression level and returns the resulting dst.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressBrotliNoCompression
|
||||
// - CompressBrotliBestSpeed
|
||||
// - CompressBrotliBestCompression
|
||||
// - CompressBrotliDefaultCompression
|
||||
func AppendBrotliBytesLevel(dst, src []byte, level int) []byte {
|
||||
w := &byteSliceWriter{dst}
|
||||
WriteBrotliLevel(w, src, level) //nolint:errcheck
|
||||
return w.b
|
||||
}
|
||||
|
||||
// WriteBrotliLevel writes brotlied p to w using the given compression level
|
||||
// and returns the number of compressed bytes written to w.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressBrotliNoCompression
|
||||
// - CompressBrotliBestSpeed
|
||||
// - CompressBrotliBestCompression
|
||||
// - CompressBrotliDefaultCompression
|
||||
func WriteBrotliLevel(w io.Writer, p []byte, level int) (int, error) {
|
||||
switch w.(type) {
|
||||
case *byteSliceWriter,
|
||||
*bytes.Buffer,
|
||||
*bytebufferpool.ByteBuffer:
|
||||
// These writers don't block, so we can just use stacklessWriteBrotli
|
||||
ctx := &compressCtx{
|
||||
w: w,
|
||||
p: p,
|
||||
level: level,
|
||||
}
|
||||
stacklessWriteBrotli(ctx)
|
||||
return len(p), nil
|
||||
default:
|
||||
zw := acquireStacklessBrotliWriter(w, level)
|
||||
n, err := zw.Write(p)
|
||||
releaseStacklessBrotliWriter(zw, level)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessWriteBrotliOnce sync.Once
|
||||
stacklessWriteBrotliFunc func(ctx any) bool
|
||||
)
|
||||
|
||||
func stacklessWriteBrotli(ctx any) {
|
||||
stacklessWriteBrotliOnce.Do(func() {
|
||||
stacklessWriteBrotliFunc = stackless.NewFunc(nonblockingWriteBrotli)
|
||||
})
|
||||
stacklessWriteBrotliFunc(ctx)
|
||||
}
|
||||
|
||||
func nonblockingWriteBrotli(ctxv any) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealBrotliWriter(ctx.w, ctx.level)
|
||||
|
||||
zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway
|
||||
|
||||
releaseRealBrotliWriter(zw, ctx.level)
|
||||
}
|
||||
|
||||
// WriteBrotli writes brotlied p to w and returns the number of compressed
|
||||
// bytes written to w.
|
||||
func WriteBrotli(w io.Writer, p []byte) (int, error) {
|
||||
return WriteBrotliLevel(w, p, CompressBrotliDefaultCompression)
|
||||
}
|
||||
|
||||
// AppendBrotliBytes appends brotlied src to dst and returns the resulting dst.
|
||||
func AppendBrotliBytes(dst, src []byte) []byte {
|
||||
return AppendBrotliBytesLevel(dst, src, CompressBrotliDefaultCompression)
|
||||
}
|
||||
|
||||
// WriteUnbrotli writes unbrotlied p to w and returns the number of uncompressed
|
||||
// bytes written to w.
|
||||
func WriteUnbrotli(w io.Writer, p []byte) (int, error) {
|
||||
r := &byteSliceReader{p}
|
||||
zr, err := acquireBrotliReader(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := copyZeroAlloc(w, zr)
|
||||
releaseBrotliReader(zr)
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0, fmt.Errorf("too much data unbrotlied: %d", n)
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
|
||||
// AppendUnbrotliBytes appends unbrotlied src to dst and returns the resulting dst.
|
||||
func AppendUnbrotliBytes(dst, src []byte) ([]byte, error) {
|
||||
w := &byteSliceWriter{dst}
|
||||
_, err := WriteUnbrotli(w, src)
|
||||
return w.b, err
|
||||
}
|
||||
|
||||
// normalizes compression level into [0..11], so it could be used as an index
|
||||
// in *PoolMap.
|
||||
func normalizeBrotliCompressLevel(level int) int {
|
||||
// -2 is the lowest compression level - CompressHuffmanOnly
|
||||
// 9 is the highest compression level - CompressBestCompression
|
||||
if level < 0 || level > 11 {
|
||||
level = CompressBrotliDefaultCompression
|
||||
}
|
||||
return level
|
||||
}
|
||||
+353
@@ -0,0 +1,353 @@
|
||||
//go:generate go run bytesconv_table_gen.go
|
||||
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AppendHTMLEscape appends html-escaped s to dst and returns the extended dst.
|
||||
func AppendHTMLEscape(dst []byte, s string) []byte {
|
||||
var (
|
||||
prev int
|
||||
sub string
|
||||
)
|
||||
|
||||
for i, n := 0, len(s); i < n; i++ {
|
||||
sub = ""
|
||||
switch s[i] {
|
||||
case '&':
|
||||
sub = "&"
|
||||
case '<':
|
||||
sub = "<"
|
||||
case '>':
|
||||
sub = ">"
|
||||
case '"':
|
||||
sub = """ // """ is shorter than """.
|
||||
case '\'':
|
||||
sub = "'" // "'" is shorter than "'" and apos was not in HTML until HTML5.
|
||||
}
|
||||
if sub != "" {
|
||||
dst = append(dst, s[prev:i]...)
|
||||
dst = append(dst, sub...)
|
||||
prev = i + 1
|
||||
}
|
||||
}
|
||||
return append(dst, s[prev:]...)
|
||||
}
|
||||
|
||||
// AppendHTMLEscapeBytes appends html-escaped s to dst and returns
|
||||
// the extended dst.
|
||||
func AppendHTMLEscapeBytes(dst, s []byte) []byte {
|
||||
return AppendHTMLEscape(dst, b2s(s))
|
||||
}
|
||||
|
||||
// AppendIPv4 appends string representation of the given ip v4 to dst
|
||||
// and returns the extended dst.
|
||||
func AppendIPv4(dst []byte, ip net.IP) []byte {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
return append(dst, "non-v4 ip passed to AppendIPv4"...)
|
||||
}
|
||||
|
||||
dst = AppendUint(dst, int(ip[0]))
|
||||
for i := 1; i < 4; i++ {
|
||||
dst = append(dst, '.')
|
||||
dst = AppendUint(dst, int(ip[i]))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
var errEmptyIPStr = errors.New("empty ip address string")
|
||||
|
||||
// ParseIPv4 parses ip address from ipStr into dst and returns the extended dst.
|
||||
func ParseIPv4(dst net.IP, ipStr []byte) (net.IP, error) {
|
||||
if len(ipStr) == 0 {
|
||||
return dst, errEmptyIPStr
|
||||
}
|
||||
if len(dst) < net.IPv4len || len(dst) > net.IPv4len {
|
||||
dst = make([]byte, net.IPv4len)
|
||||
}
|
||||
copy(dst, net.IPv4zero)
|
||||
dst = dst.To4() // dst is always non-nil here
|
||||
|
||||
b := ipStr
|
||||
for i := 0; i < 3; i++ {
|
||||
n := bytes.IndexByte(b, '.')
|
||||
if n < 0 {
|
||||
return dst, fmt.Errorf("cannot find dot in ipStr %q", ipStr)
|
||||
}
|
||||
v, err := ParseUint(b[:n])
|
||||
if err != nil {
|
||||
return dst, fmt.Errorf("cannot parse ipStr %q: %w", ipStr, err)
|
||||
}
|
||||
if v > 255 {
|
||||
return dst, fmt.Errorf("cannot parse ipStr %q: ip part cannot exceed 255: parsed %d", ipStr, v)
|
||||
}
|
||||
dst[i] = byte(v)
|
||||
b = b[n+1:]
|
||||
}
|
||||
v, err := ParseUint(b)
|
||||
if err != nil {
|
||||
return dst, fmt.Errorf("cannot parse ipStr %q: %w", ipStr, err)
|
||||
}
|
||||
if v > 255 {
|
||||
return dst, fmt.Errorf("cannot parse ipStr %q: ip part cannot exceed 255: parsed %d", ipStr, v)
|
||||
}
|
||||
dst[3] = byte(v)
|
||||
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// AppendHTTPDate appends HTTP-compliant (RFC1123) representation of date
|
||||
// to dst and returns the extended dst.
|
||||
func AppendHTTPDate(dst []byte, date time.Time) []byte {
|
||||
dst = date.In(time.UTC).AppendFormat(dst, time.RFC1123)
|
||||
copy(dst[len(dst)-3:], strGMT)
|
||||
return dst
|
||||
}
|
||||
|
||||
// ParseHTTPDate parses HTTP-compliant (RFC1123) date.
|
||||
func ParseHTTPDate(date []byte) (time.Time, error) {
|
||||
return time.Parse(time.RFC1123, b2s(date))
|
||||
}
|
||||
|
||||
// AppendUint appends n to dst and returns the extended dst.
|
||||
func AppendUint(dst []byte, n int) []byte {
|
||||
if n < 0 {
|
||||
// developer sanity-check
|
||||
panic("BUG: int must be positive")
|
||||
}
|
||||
|
||||
var b [20]byte
|
||||
buf := b[:]
|
||||
i := len(buf)
|
||||
var q int
|
||||
for n >= 10 {
|
||||
i--
|
||||
q = n / 10
|
||||
buf[i] = '0' + byte(n-q*10)
|
||||
n = q
|
||||
}
|
||||
i--
|
||||
buf[i] = '0' + byte(n)
|
||||
|
||||
dst = append(dst, buf[i:]...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// ParseUint parses uint from buf.
|
||||
func ParseUint(buf []byte) (int, error) {
|
||||
v, n, err := parseUintBuf(buf)
|
||||
if n != len(buf) {
|
||||
return -1, errUnexpectedTrailingChar
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyInt = errors.New("empty integer")
|
||||
errUnexpectedFirstChar = errors.New("unexpected first char found. Expecting 0-9")
|
||||
errUnexpectedTrailingChar = errors.New("unexpected trailing char found. Expecting 0-9")
|
||||
errTooLongInt = errors.New("too long int")
|
||||
)
|
||||
|
||||
func parseUintBuf(b []byte) (int, int, error) {
|
||||
n := len(b)
|
||||
if n == 0 {
|
||||
return -1, 0, errEmptyInt
|
||||
}
|
||||
v := 0
|
||||
for i := 0; i < n; i++ {
|
||||
c := b[i]
|
||||
k := c - '0'
|
||||
if k > 9 {
|
||||
if i == 0 {
|
||||
return -1, i, errUnexpectedFirstChar
|
||||
}
|
||||
return v, i, nil
|
||||
}
|
||||
vNew := 10*v + int(k)
|
||||
// Test for overflow.
|
||||
if vNew < v {
|
||||
return -1, i, errTooLongInt
|
||||
}
|
||||
v = vNew
|
||||
}
|
||||
return v, n, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyFloat = errors.New("empty float number")
|
||||
errDuplicateFloatPoint = errors.New("duplicate point found in float number")
|
||||
errUnexpectedFloatEnd = errors.New("unexpected end of float number")
|
||||
errInvalidFloatExponent = errors.New("invalid float number exponent")
|
||||
errUnexpectedFloatChar = errors.New("unexpected char found in float number")
|
||||
)
|
||||
|
||||
// ParseUfloat parses unsigned float from buf.
|
||||
func ParseUfloat(buf []byte) (float64, error) {
|
||||
if len(buf) == 0 {
|
||||
return -1, errEmptyFloat
|
||||
}
|
||||
b := buf
|
||||
var v uint64
|
||||
offset := 1.0
|
||||
var pointFound bool
|
||||
for i, c := range b {
|
||||
if c < '0' || c > '9' {
|
||||
if c == '.' {
|
||||
if pointFound {
|
||||
return -1, errDuplicateFloatPoint
|
||||
}
|
||||
pointFound = true
|
||||
continue
|
||||
}
|
||||
if c == 'e' || c == 'E' {
|
||||
if i+1 >= len(b) {
|
||||
return -1, errUnexpectedFloatEnd
|
||||
}
|
||||
b = b[i+1:]
|
||||
minus := -1
|
||||
switch b[0] {
|
||||
case '+':
|
||||
b = b[1:]
|
||||
minus = 1
|
||||
case '-':
|
||||
b = b[1:]
|
||||
default:
|
||||
minus = 1
|
||||
}
|
||||
vv, err := ParseUint(b)
|
||||
if err != nil {
|
||||
return -1, errInvalidFloatExponent
|
||||
}
|
||||
return float64(v) * offset * math.Pow10(minus*vv), nil
|
||||
}
|
||||
return -1, errUnexpectedFloatChar
|
||||
}
|
||||
v = 10*v + uint64(c-'0')
|
||||
if pointFound {
|
||||
offset /= 10
|
||||
}
|
||||
}
|
||||
return float64(v) * offset, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errEmptyHexNum = errors.New("empty hex number")
|
||||
errTooLargeHexNum = errors.New("too large hex number")
|
||||
)
|
||||
|
||||
func readHexInt(r *bufio.Reader) (int, error) {
|
||||
var k, i, n int
|
||||
for {
|
||||
c, err := r.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF && i > 0 {
|
||||
return n, nil
|
||||
}
|
||||
return -1, err
|
||||
}
|
||||
k = int(hex2intTable[c])
|
||||
if k == 16 {
|
||||
if i == 0 {
|
||||
return -1, errEmptyHexNum
|
||||
}
|
||||
if err := r.UnreadByte(); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
if i >= maxHexIntChars {
|
||||
return -1, errTooLargeHexNum
|
||||
}
|
||||
n = (n << 4) | k
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
var hexIntBufPool sync.Pool
|
||||
|
||||
func writeHexInt(w *bufio.Writer, n int) error {
|
||||
if n < 0 {
|
||||
// developer sanity-check
|
||||
panic("BUG: int must be positive")
|
||||
}
|
||||
|
||||
v := hexIntBufPool.Get()
|
||||
if v == nil {
|
||||
v = make([]byte, maxHexIntChars+1)
|
||||
}
|
||||
buf := v.([]byte)
|
||||
i := len(buf) - 1
|
||||
for {
|
||||
buf[i] = lowerhex[n&0xf]
|
||||
n >>= 4
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
i--
|
||||
}
|
||||
_, err := w.Write(buf[i:])
|
||||
hexIntBufPool.Put(v)
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
upperhex = "0123456789ABCDEF"
|
||||
lowerhex = "0123456789abcdef"
|
||||
)
|
||||
|
||||
func lowercaseBytes(b []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
p := &b[i]
|
||||
*p = toLowerTable[*p]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendUnquotedArg appends url-decoded src to dst and returns appended dst.
|
||||
//
|
||||
// dst may point to src. In this case src will be overwritten.
|
||||
func AppendUnquotedArg(dst, src []byte) []byte {
|
||||
return decodeArgAppend(dst, src)
|
||||
}
|
||||
|
||||
// AppendQuotedArg appends url-encoded src to dst and returns appended dst.
|
||||
func AppendQuotedArg(dst, src []byte) []byte {
|
||||
for _, c := range src {
|
||||
switch {
|
||||
case c == ' ':
|
||||
dst = append(dst, '+')
|
||||
case quotedArgShouldEscapeTable[int(c)] != 0:
|
||||
dst = append(dst, '%', upperhex[c>>4], upperhex[c&0xf])
|
||||
default:
|
||||
dst = append(dst, c)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendQuotedPath(dst, src []byte) []byte {
|
||||
// Fix issue in https://github.com/golang/go/issues/11202
|
||||
if len(src) == 1 && src[0] == '*' {
|
||||
return append(dst, '*')
|
||||
}
|
||||
|
||||
for _, c := range src {
|
||||
if quotedPathShouldEscapeTable[int(c)] != 0 {
|
||||
dst = append(dst, '%', upperhex[c>>4], upperhex[c&0xf])
|
||||
} else {
|
||||
dst = append(dst, c)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x
|
||||
|
||||
package fasthttp
|
||||
|
||||
const (
|
||||
maxHexIntChars = 7
|
||||
)
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
//go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x
|
||||
|
||||
package fasthttp
|
||||
|
||||
const (
|
||||
maxHexIntChars = 15
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fasthttp
|
||||
|
||||
// Code generated by go run bytesconv_table_gen.go; DO NOT EDIT.
|
||||
// See bytesconv_table_gen.go for more information about these tables.
|
||||
|
||||
const hex2intTable = "\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x00\x01\x02\x03\x04\x05\x06\a\b\t\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\n\v\f\r\x0e\x0f\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10\x10"
|
||||
const toLowerTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@abcdefghijklmnopqrstuvwxyz[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
|
||||
const toUpperTable = "\x00\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`ABCDEFGHIJKLMNOPQRSTUVWXYZ{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
|
||||
const quotedArgShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
|
||||
const quotedPathShouldEscapeTable = "\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01"
|
||||
const validHeaderFieldByteTable = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x01\x01\x01\x01\x01\x00\x00\x01\x01\x00\x01\x01\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x00\x00\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x01\x00\x01\x00\x01\x00"
|
||||
+3006
File diff suppressed because it is too large
Load Diff
+13
@@ -0,0 +1,13 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// CoarseTimeNow returns the current time truncated to the nearest second.
|
||||
//
|
||||
// Deprecated: This is slower than calling time.Now() directly.
|
||||
// This is now time.Now().Truncate(time.Second) shortcut.
|
||||
func CoarseTimeNow() time.Time {
|
||||
return time.Now().Truncate(time.Second)
|
||||
}
|
||||
+480
@@ -0,0 +1,480 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/compress/flate"
|
||||
"github.com/klauspost/compress/gzip"
|
||||
"github.com/klauspost/compress/zlib"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"github.com/valyala/fasthttp/stackless"
|
||||
)
|
||||
|
||||
// Supported compression levels.
|
||||
const (
|
||||
CompressNoCompression = flate.NoCompression
|
||||
CompressBestSpeed = flate.BestSpeed
|
||||
CompressBestCompression = flate.BestCompression
|
||||
CompressDefaultCompression = 6 // flate.DefaultCompression
|
||||
CompressHuffmanOnly = -2 // flate.HuffmanOnly
|
||||
)
|
||||
|
||||
func acquireGzipReader(r io.Reader) (*gzip.Reader, error) {
|
||||
v := gzipReaderPool.Get()
|
||||
if v == nil {
|
||||
return gzip.NewReader(r)
|
||||
}
|
||||
zr := v.(*gzip.Reader)
|
||||
if err := zr.Reset(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
func releaseGzipReader(zr *gzip.Reader) {
|
||||
zr.Close()
|
||||
gzipReaderPool.Put(zr)
|
||||
}
|
||||
|
||||
var gzipReaderPool sync.Pool
|
||||
|
||||
func acquireFlateReader(r io.Reader) (io.ReadCloser, error) {
|
||||
v := flateReaderPool.Get()
|
||||
if v == nil {
|
||||
zr, err := zlib.NewReader(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
zr := v.(io.ReadCloser)
|
||||
if err := resetFlateReader(zr, r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
func releaseFlateReader(zr io.ReadCloser) {
|
||||
zr.Close()
|
||||
flateReaderPool.Put(zr)
|
||||
}
|
||||
|
||||
func resetFlateReader(zr io.ReadCloser, r io.Reader) error {
|
||||
zrr, ok := zr.(zlib.Resetter)
|
||||
if !ok {
|
||||
// sanity check. should only be called with a zlib.Reader
|
||||
panic("BUG: zlib.Reader doesn't implement zlib.Resetter???")
|
||||
}
|
||||
return zrr.Reset(r, nil)
|
||||
}
|
||||
|
||||
var flateReaderPool sync.Pool
|
||||
|
||||
func acquireStacklessGzipWriter(w io.Writer, level int) stackless.Writer {
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := stacklessGzipWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
return stackless.NewWriter(w, func(w io.Writer) stackless.Writer {
|
||||
return acquireRealGzipWriter(w, level)
|
||||
})
|
||||
}
|
||||
sw := v.(stackless.Writer)
|
||||
sw.Reset(w)
|
||||
return sw
|
||||
}
|
||||
|
||||
func releaseStacklessGzipWriter(sw stackless.Writer, level int) {
|
||||
sw.Close()
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := stacklessGzipWriterPoolMap[nLevel]
|
||||
p.Put(sw)
|
||||
}
|
||||
|
||||
func acquireRealGzipWriter(w io.Writer, level int) *gzip.Writer {
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := realGzipWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
zw, err := gzip.NewWriterLevel(w, level)
|
||||
if err != nil {
|
||||
// gzip.NewWriterLevel only errors for invalid
|
||||
// compression levels. Clamp it to be min or max.
|
||||
if level < gzip.HuffmanOnly {
|
||||
level = gzip.HuffmanOnly
|
||||
} else {
|
||||
level = gzip.BestCompression
|
||||
}
|
||||
zw, _ = gzip.NewWriterLevel(w, level)
|
||||
}
|
||||
return zw
|
||||
}
|
||||
zw := v.(*gzip.Writer)
|
||||
zw.Reset(w)
|
||||
return zw
|
||||
}
|
||||
|
||||
func releaseRealGzipWriter(zw *gzip.Writer, level int) {
|
||||
zw.Close()
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := realGzipWriterPoolMap[nLevel]
|
||||
p.Put(zw)
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessGzipWriterPoolMap = newCompressWriterPoolMap()
|
||||
realGzipWriterPoolMap = newCompressWriterPoolMap()
|
||||
)
|
||||
|
||||
// AppendGzipBytesLevel appends gzipped src to dst using the given
|
||||
// compression level and returns the resulting dst.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressNoCompression
|
||||
// - CompressBestSpeed
|
||||
// - CompressBestCompression
|
||||
// - CompressDefaultCompression
|
||||
// - CompressHuffmanOnly
|
||||
func AppendGzipBytesLevel(dst, src []byte, level int) []byte {
|
||||
w := &byteSliceWriter{dst}
|
||||
WriteGzipLevel(w, src, level) //nolint:errcheck
|
||||
return w.b
|
||||
}
|
||||
|
||||
// WriteGzipLevel writes gzipped p to w using the given compression level
|
||||
// and returns the number of compressed bytes written to w.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressNoCompression
|
||||
// - CompressBestSpeed
|
||||
// - CompressBestCompression
|
||||
// - CompressDefaultCompression
|
||||
// - CompressHuffmanOnly
|
||||
func WriteGzipLevel(w io.Writer, p []byte, level int) (int, error) {
|
||||
switch w.(type) {
|
||||
case *byteSliceWriter,
|
||||
*bytes.Buffer,
|
||||
*bytebufferpool.ByteBuffer:
|
||||
// These writers don't block, so we can just use stacklessWriteGzip
|
||||
ctx := &compressCtx{
|
||||
w: w,
|
||||
p: p,
|
||||
level: level,
|
||||
}
|
||||
stacklessWriteGzip(ctx)
|
||||
return len(p), nil
|
||||
default:
|
||||
zw := acquireStacklessGzipWriter(w, level)
|
||||
n, err := zw.Write(p)
|
||||
releaseStacklessGzipWriter(zw, level)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessWriteGzipOnce sync.Once
|
||||
stacklessWriteGzipFunc func(ctx any) bool
|
||||
)
|
||||
|
||||
func stacklessWriteGzip(ctx any) {
|
||||
stacklessWriteGzipOnce.Do(func() {
|
||||
stacklessWriteGzipFunc = stackless.NewFunc(nonblockingWriteGzip)
|
||||
})
|
||||
stacklessWriteGzipFunc(ctx)
|
||||
}
|
||||
|
||||
func nonblockingWriteGzip(ctxv any) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealGzipWriter(ctx.w, ctx.level)
|
||||
|
||||
zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway
|
||||
|
||||
releaseRealGzipWriter(zw, ctx.level)
|
||||
}
|
||||
|
||||
// WriteGzip writes gzipped p to w and returns the number of compressed
|
||||
// bytes written to w.
|
||||
func WriteGzip(w io.Writer, p []byte) (int, error) {
|
||||
return WriteGzipLevel(w, p, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// AppendGzipBytes appends gzipped src to dst and returns the resulting dst.
|
||||
func AppendGzipBytes(dst, src []byte) []byte {
|
||||
return AppendGzipBytesLevel(dst, src, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// WriteGunzip writes ungzipped p to w and returns the number of uncompressed
|
||||
// bytes written to w.
|
||||
func WriteGunzip(w io.Writer, p []byte) (int, error) {
|
||||
r := &byteSliceReader{p}
|
||||
zr, err := acquireGzipReader(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := copyZeroAlloc(w, zr)
|
||||
releaseGzipReader(zr)
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0, fmt.Errorf("too much data gunzipped: %d", n)
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
|
||||
// AppendGunzipBytes appends gunzipped src to dst and returns the resulting dst.
|
||||
func AppendGunzipBytes(dst, src []byte) ([]byte, error) {
|
||||
w := &byteSliceWriter{dst}
|
||||
_, err := WriteGunzip(w, src)
|
||||
return w.b, err
|
||||
}
|
||||
|
||||
// AppendDeflateBytesLevel appends deflated src to dst using the given
|
||||
// compression level and returns the resulting dst.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressNoCompression
|
||||
// - CompressBestSpeed
|
||||
// - CompressBestCompression
|
||||
// - CompressDefaultCompression
|
||||
// - CompressHuffmanOnly
|
||||
func AppendDeflateBytesLevel(dst, src []byte, level int) []byte {
|
||||
w := &byteSliceWriter{dst}
|
||||
WriteDeflateLevel(w, src, level) //nolint:errcheck
|
||||
return w.b
|
||||
}
|
||||
|
||||
// WriteDeflateLevel writes deflated p to w using the given compression level
|
||||
// and returns the number of compressed bytes written to w.
|
||||
//
|
||||
// Supported compression levels are:
|
||||
//
|
||||
// - CompressNoCompression
|
||||
// - CompressBestSpeed
|
||||
// - CompressBestCompression
|
||||
// - CompressDefaultCompression
|
||||
// - CompressHuffmanOnly
|
||||
func WriteDeflateLevel(w io.Writer, p []byte, level int) (int, error) {
|
||||
switch w.(type) {
|
||||
case *byteSliceWriter,
|
||||
*bytes.Buffer,
|
||||
*bytebufferpool.ByteBuffer:
|
||||
// These writers don't block, so we can just use stacklessWriteDeflate
|
||||
ctx := &compressCtx{
|
||||
w: w,
|
||||
p: p,
|
||||
level: level,
|
||||
}
|
||||
stacklessWriteDeflate(ctx)
|
||||
return len(p), nil
|
||||
default:
|
||||
zw := acquireStacklessDeflateWriter(w, level)
|
||||
n, err := zw.Write(p)
|
||||
releaseStacklessDeflateWriter(zw, level)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessWriteDeflateOnce sync.Once
|
||||
stacklessWriteDeflateFunc func(ctx any) bool
|
||||
)
|
||||
|
||||
func stacklessWriteDeflate(ctx any) {
|
||||
stacklessWriteDeflateOnce.Do(func() {
|
||||
stacklessWriteDeflateFunc = stackless.NewFunc(nonblockingWriteDeflate)
|
||||
})
|
||||
stacklessWriteDeflateFunc(ctx)
|
||||
}
|
||||
|
||||
func nonblockingWriteDeflate(ctxv any) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealDeflateWriter(ctx.w, ctx.level)
|
||||
|
||||
zw.Write(ctx.p) //nolint:errcheck // no way to handle this error anyway
|
||||
|
||||
releaseRealDeflateWriter(zw, ctx.level)
|
||||
}
|
||||
|
||||
type compressCtx struct {
|
||||
w io.Writer
|
||||
p []byte
|
||||
level int
|
||||
}
|
||||
|
||||
// WriteDeflate writes deflated p to w and returns the number of compressed
|
||||
// bytes written to w.
|
||||
func WriteDeflate(w io.Writer, p []byte) (int, error) {
|
||||
return WriteDeflateLevel(w, p, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// AppendDeflateBytes appends deflated src to dst and returns the resulting dst.
|
||||
func AppendDeflateBytes(dst, src []byte) []byte {
|
||||
return AppendDeflateBytesLevel(dst, src, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// WriteInflate writes inflated p to w and returns the number of uncompressed
|
||||
// bytes written to w.
|
||||
func WriteInflate(w io.Writer, p []byte) (int, error) {
|
||||
r := &byteSliceReader{p}
|
||||
zr, err := acquireFlateReader(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := copyZeroAlloc(w, zr)
|
||||
releaseFlateReader(zr)
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0, fmt.Errorf("too much data inflated: %d", n)
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
|
||||
// AppendInflateBytes appends inflated src to dst and returns the resulting dst.
|
||||
func AppendInflateBytes(dst, src []byte) ([]byte, error) {
|
||||
w := &byteSliceWriter{dst}
|
||||
_, err := WriteInflate(w, src)
|
||||
return w.b, err
|
||||
}
|
||||
|
||||
type byteSliceWriter struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (w *byteSliceWriter) Write(p []byte) (int, error) {
|
||||
w.b = append(w.b, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type byteSliceReader struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (r *byteSliceReader) Read(p []byte) (int, error) {
|
||||
if len(r.b) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := copy(p, r.b)
|
||||
r.b = r.b[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *byteSliceReader) ReadByte() (byte, error) {
|
||||
if len(r.b) == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n := r.b[0]
|
||||
r.b = r.b[1:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func acquireStacklessDeflateWriter(w io.Writer, level int) stackless.Writer {
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := stacklessDeflateWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
return stackless.NewWriter(w, func(w io.Writer) stackless.Writer {
|
||||
return acquireRealDeflateWriter(w, level)
|
||||
})
|
||||
}
|
||||
sw := v.(stackless.Writer)
|
||||
sw.Reset(w)
|
||||
return sw
|
||||
}
|
||||
|
||||
func releaseStacklessDeflateWriter(sw stackless.Writer, level int) {
|
||||
sw.Close()
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := stacklessDeflateWriterPoolMap[nLevel]
|
||||
p.Put(sw)
|
||||
}
|
||||
|
||||
func acquireRealDeflateWriter(w io.Writer, level int) *zlib.Writer {
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := realDeflateWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
zw, err := zlib.NewWriterLevel(w, level)
|
||||
if err != nil {
|
||||
// zlib.NewWriterLevel only errors for invalid
|
||||
// compression levels. Clamp it to be min or max.
|
||||
if level < zlib.HuffmanOnly {
|
||||
level = zlib.HuffmanOnly
|
||||
} else {
|
||||
level = zlib.BestCompression
|
||||
}
|
||||
zw, _ = zlib.NewWriterLevel(w, level)
|
||||
}
|
||||
return zw
|
||||
}
|
||||
zw := v.(*zlib.Writer)
|
||||
zw.Reset(w)
|
||||
return zw
|
||||
}
|
||||
|
||||
func releaseRealDeflateWriter(zw *zlib.Writer, level int) {
|
||||
zw.Close()
|
||||
nLevel := normalizeCompressLevel(level)
|
||||
p := realDeflateWriterPoolMap[nLevel]
|
||||
p.Put(zw)
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessDeflateWriterPoolMap = newCompressWriterPoolMap()
|
||||
realDeflateWriterPoolMap = newCompressWriterPoolMap()
|
||||
)
|
||||
|
||||
func newCompressWriterPoolMap() []*sync.Pool {
|
||||
// Initialize pools for all the compression levels defined
|
||||
// in https://pkg.go.dev/compress/flate#pkg-constants .
|
||||
// Compression levels are normalized with normalizeCompressLevel,
|
||||
// so the fit [0..11].
|
||||
var m []*sync.Pool
|
||||
for i := 0; i < 12; i++ {
|
||||
m = append(m, &sync.Pool{})
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func isFileCompressible(f fs.File, minCompressRatio float64) bool {
|
||||
// Try compressing the first 4kb of the file
|
||||
// and see if it can be compressed by more than
|
||||
// the given minCompressRatio.
|
||||
b := bytebufferpool.Get()
|
||||
zw := acquireStacklessGzipWriter(b, CompressDefaultCompression)
|
||||
lr := &io.LimitedReader{
|
||||
R: f,
|
||||
N: 4096,
|
||||
}
|
||||
_, err := copyZeroAlloc(zw, lr)
|
||||
releaseStacklessGzipWriter(zw, CompressDefaultCompression)
|
||||
seeker, ok := f.(io.Seeker)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
seeker.Seek(0, io.SeekStart) //nolint:errcheck
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
n := 4096 - lr.N
|
||||
zn := len(b.B)
|
||||
bytebufferpool.Put(b)
|
||||
return float64(zn) < float64(n)*minCompressRatio
|
||||
}
|
||||
|
||||
// normalizes compression level into [0..11], so it could be used as an index
|
||||
// in *PoolMap.
|
||||
func normalizeCompressLevel(level int) int {
|
||||
// -2 is the lowest compression level - CompressHuffmanOnly
|
||||
// 9 is the highest compression level - CompressBestCompression
|
||||
if level < -2 || level > 9 {
|
||||
level = CompressDefaultCompression
|
||||
}
|
||||
return level + 2
|
||||
}
|
||||
+581
@@ -0,0 +1,581 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var zeroTime time.Time
|
||||
|
||||
var (
|
||||
// CookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
|
||||
CookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
|
||||
// CookieExpireUnlimited indicates that the cookie doesn't expire.
|
||||
CookieExpireUnlimited = zeroTime
|
||||
)
|
||||
|
||||
// CookieSameSite is an enum for the mode in which the SameSite flag should be set for the given cookie.
|
||||
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
|
||||
type CookieSameSite int
|
||||
|
||||
const (
|
||||
// CookieSameSiteDisabled removes the SameSite flag.
|
||||
CookieSameSiteDisabled CookieSameSite = iota
|
||||
// CookieSameSiteDefaultMode sets the SameSite flag.
|
||||
CookieSameSiteDefaultMode
|
||||
// CookieSameSiteLaxMode sets the SameSite flag with the "Lax" parameter.
|
||||
CookieSameSiteLaxMode
|
||||
// CookieSameSiteStrictMode sets the SameSite flag with the "Strict" parameter.
|
||||
CookieSameSiteStrictMode
|
||||
// CookieSameSiteNoneMode sets the SameSite flag with the "None" parameter.
|
||||
// See https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
|
||||
CookieSameSiteNoneMode // third-party cookies are phasing out, use Partitioned cookies instead
|
||||
)
|
||||
|
||||
// AcquireCookie returns an empty Cookie object from the pool.
|
||||
//
|
||||
// The returned object may be returned back to the pool with ReleaseCookie.
|
||||
// This allows reducing GC load.
|
||||
func AcquireCookie() *Cookie {
|
||||
return cookiePool.Get().(*Cookie)
|
||||
}
|
||||
|
||||
// ReleaseCookie returns the Cookie object acquired with AcquireCookie back
|
||||
// to the pool.
|
||||
//
|
||||
// Do not access released Cookie object, otherwise data races may occur.
|
||||
func ReleaseCookie(c *Cookie) {
|
||||
c.Reset()
|
||||
cookiePool.Put(c)
|
||||
}
|
||||
|
||||
var cookiePool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &Cookie{}
|
||||
},
|
||||
}
|
||||
|
||||
// Cookie represents HTTP response cookie.
|
||||
//
|
||||
// Do not copy Cookie objects. Create new object and use CopyTo instead.
|
||||
//
|
||||
// Cookie instance MUST NOT be used from concurrently running goroutines.
|
||||
type Cookie struct {
|
||||
noCopy noCopy
|
||||
|
||||
key []byte
|
||||
value []byte
|
||||
expire time.Time
|
||||
maxAge int
|
||||
domain []byte
|
||||
path []byte
|
||||
|
||||
httpOnly bool
|
||||
secure bool
|
||||
sameSite CookieSameSite
|
||||
partitioned bool
|
||||
|
||||
bufKV argsKV
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// CopyTo copies src cookie to c.
|
||||
func (c *Cookie) CopyTo(src *Cookie) {
|
||||
c.Reset()
|
||||
c.key = append(c.key, src.key...)
|
||||
c.value = append(c.value, src.value...)
|
||||
c.expire = src.expire
|
||||
c.maxAge = src.maxAge
|
||||
c.domain = append(c.domain, src.domain...)
|
||||
c.path = append(c.path, src.path...)
|
||||
c.httpOnly = src.httpOnly
|
||||
c.secure = src.secure
|
||||
c.sameSite = src.sameSite
|
||||
c.partitioned = src.partitioned
|
||||
}
|
||||
|
||||
// HTTPOnly returns true if the cookie is http only.
|
||||
func (c *Cookie) HTTPOnly() bool {
|
||||
return c.httpOnly
|
||||
}
|
||||
|
||||
// SetHTTPOnly sets cookie's httpOnly flag to the given value.
|
||||
func (c *Cookie) SetHTTPOnly(httpOnly bool) {
|
||||
c.httpOnly = httpOnly
|
||||
}
|
||||
|
||||
// Secure returns true if the cookie is secure.
|
||||
func (c *Cookie) Secure() bool {
|
||||
return c.secure
|
||||
}
|
||||
|
||||
// SetSecure sets cookie's secure flag to the given value.
|
||||
func (c *Cookie) SetSecure(secure bool) {
|
||||
c.secure = secure
|
||||
}
|
||||
|
||||
// SameSite returns the SameSite mode.
|
||||
func (c *Cookie) SameSite() CookieSameSite {
|
||||
return c.sameSite
|
||||
}
|
||||
|
||||
// SetSameSite sets the cookie's SameSite flag to the given value.
|
||||
// Set value CookieSameSiteNoneMode will set Secure to true also to avoid browser rejection.
|
||||
func (c *Cookie) SetSameSite(mode CookieSameSite) {
|
||||
c.sameSite = mode
|
||||
if mode == CookieSameSiteNoneMode {
|
||||
c.SetSecure(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Partitioned returns true if the cookie is partitioned.
|
||||
func (c *Cookie) Partitioned() bool {
|
||||
return c.partitioned
|
||||
}
|
||||
|
||||
// SetPartitioned sets the cookie's Partitioned flag to the given value.
|
||||
// Set value Partitioned to true will set Secure to true and Path to / also to avoid browser rejection.
|
||||
func (c *Cookie) SetPartitioned(partitioned bool) {
|
||||
c.partitioned = partitioned
|
||||
if partitioned {
|
||||
c.SetSecure(true)
|
||||
c.SetPath("/")
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns cookie path.
|
||||
func (c *Cookie) Path() []byte {
|
||||
return c.path
|
||||
}
|
||||
|
||||
// SetPath sets cookie path.
|
||||
func (c *Cookie) SetPath(path string) {
|
||||
c.buf = append(c.buf[:0], path...)
|
||||
c.path = normalizePath(c.path, c.buf)
|
||||
}
|
||||
|
||||
// SetPathBytes sets cookie path.
|
||||
func (c *Cookie) SetPathBytes(path []byte) {
|
||||
c.buf = append(c.buf[:0], path...)
|
||||
c.path = normalizePath(c.path, c.buf)
|
||||
}
|
||||
|
||||
// Domain returns cookie domain.
|
||||
//
|
||||
// The returned value is valid until the Cookie reused or released (ReleaseCookie).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (c *Cookie) Domain() []byte {
|
||||
return c.domain
|
||||
}
|
||||
|
||||
// SetDomain sets cookie domain.
|
||||
func (c *Cookie) SetDomain(domain string) {
|
||||
c.domain = append(c.domain[:0], domain...)
|
||||
}
|
||||
|
||||
// SetDomainBytes sets cookie domain.
|
||||
func (c *Cookie) SetDomainBytes(domain []byte) {
|
||||
c.domain = append(c.domain[:0], domain...)
|
||||
}
|
||||
|
||||
// MaxAge returns the seconds until the cookie is meant to expire or 0
|
||||
// if no max age.
|
||||
func (c *Cookie) MaxAge() int {
|
||||
return c.maxAge
|
||||
}
|
||||
|
||||
// SetMaxAge sets cookie expiration time based on seconds. This takes precedence
|
||||
// over any absolute expiry set on the cookie.
|
||||
//
|
||||
// Set max age to 0 to unset.
|
||||
func (c *Cookie) SetMaxAge(seconds int) {
|
||||
c.maxAge = seconds
|
||||
}
|
||||
|
||||
// Expire returns cookie expiration time.
|
||||
//
|
||||
// CookieExpireUnlimited is returned if cookie doesn't expire.
|
||||
func (c *Cookie) Expire() time.Time {
|
||||
expire := c.expire
|
||||
if expire.IsZero() {
|
||||
expire = CookieExpireUnlimited
|
||||
}
|
||||
return expire
|
||||
}
|
||||
|
||||
// SetExpire sets cookie expiration time.
|
||||
//
|
||||
// Set expiration time to CookieExpireDelete for expiring (deleting)
|
||||
// the cookie on the client.
|
||||
//
|
||||
// By default cookie lifetime is limited by browser session.
|
||||
func (c *Cookie) SetExpire(expire time.Time) {
|
||||
c.expire = expire
|
||||
}
|
||||
|
||||
// Value returns cookie value.
|
||||
//
|
||||
// The returned value is valid until the Cookie reused or released (ReleaseCookie).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (c *Cookie) Value() []byte {
|
||||
return c.value
|
||||
}
|
||||
|
||||
// SetValue sets cookie value.
|
||||
func (c *Cookie) SetValue(value string) {
|
||||
c.value = append(c.value[:0], value...)
|
||||
}
|
||||
|
||||
// SetValueBytes sets cookie value.
|
||||
func (c *Cookie) SetValueBytes(value []byte) {
|
||||
c.value = append(c.value[:0], value...)
|
||||
}
|
||||
|
||||
// Key returns cookie name.
|
||||
//
|
||||
// The returned value is valid until the Cookie reused or released (ReleaseCookie).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (c *Cookie) Key() []byte {
|
||||
return c.key
|
||||
}
|
||||
|
||||
// SetKey sets cookie name.
|
||||
func (c *Cookie) SetKey(key string) {
|
||||
c.key = append(c.key[:0], key...)
|
||||
}
|
||||
|
||||
// SetKeyBytes sets cookie name.
|
||||
func (c *Cookie) SetKeyBytes(key []byte) {
|
||||
c.key = append(c.key[:0], key...)
|
||||
}
|
||||
|
||||
// Reset clears the cookie.
|
||||
func (c *Cookie) Reset() {
|
||||
c.key = c.key[:0]
|
||||
c.value = c.value[:0]
|
||||
c.expire = zeroTime
|
||||
c.maxAge = 0
|
||||
c.domain = c.domain[:0]
|
||||
c.path = c.path[:0]
|
||||
c.httpOnly = false
|
||||
c.secure = false
|
||||
c.sameSite = CookieSameSiteDisabled
|
||||
c.partitioned = false
|
||||
}
|
||||
|
||||
// AppendBytes appends cookie representation to dst and returns
|
||||
// the extended dst.
|
||||
func (c *Cookie) AppendBytes(dst []byte) []byte {
|
||||
if len(c.key) > 0 {
|
||||
dst = append(dst, c.key...)
|
||||
dst = append(dst, '=')
|
||||
}
|
||||
dst = append(dst, c.value...)
|
||||
|
||||
if c.maxAge > 0 {
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieMaxAge...)
|
||||
dst = append(dst, '=')
|
||||
dst = AppendUint(dst, c.maxAge)
|
||||
} else if !c.expire.IsZero() {
|
||||
c.bufKV.value = AppendHTTPDate(c.bufKV.value[:0], c.expire)
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieExpires...)
|
||||
dst = append(dst, '=')
|
||||
dst = append(dst, c.bufKV.value...)
|
||||
}
|
||||
if len(c.domain) > 0 {
|
||||
dst = appendCookiePart(dst, strCookieDomain, c.domain)
|
||||
}
|
||||
if len(c.path) > 0 {
|
||||
dst = appendCookiePart(dst, strCookiePath, c.path)
|
||||
}
|
||||
if c.httpOnly {
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieHTTPOnly...)
|
||||
}
|
||||
if c.secure {
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieSecure...)
|
||||
}
|
||||
switch c.sameSite {
|
||||
case CookieSameSiteDefaultMode:
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieSameSite...)
|
||||
case CookieSameSiteLaxMode:
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieSameSite...)
|
||||
dst = append(dst, '=')
|
||||
dst = append(dst, strCookieSameSiteLax...)
|
||||
case CookieSameSiteStrictMode:
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieSameSite...)
|
||||
dst = append(dst, '=')
|
||||
dst = append(dst, strCookieSameSiteStrict...)
|
||||
case CookieSameSiteNoneMode:
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookieSameSite...)
|
||||
dst = append(dst, '=')
|
||||
dst = append(dst, strCookieSameSiteNone...)
|
||||
}
|
||||
if c.partitioned {
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, strCookiePartitioned...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Cookie returns cookie representation.
|
||||
//
|
||||
// The returned value is valid until the Cookie reused or released (ReleaseCookie).
|
||||
// Do not store references to the returned value. Make copies instead.
|
||||
func (c *Cookie) Cookie() []byte {
|
||||
c.buf = c.AppendBytes(c.buf[:0])
|
||||
return c.buf
|
||||
}
|
||||
|
||||
// String returns cookie representation.
|
||||
func (c *Cookie) String() string {
|
||||
return string(c.Cookie())
|
||||
}
|
||||
|
||||
// WriteTo writes cookie representation to w.
|
||||
//
|
||||
// WriteTo implements io.WriterTo interface.
|
||||
func (c *Cookie) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(c.Cookie())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
var errNoCookies = errors.New("no cookies found")
|
||||
|
||||
// Parse parses Set-Cookie header.
|
||||
func (c *Cookie) Parse(src string) error {
|
||||
c.buf = append(c.buf[:0], src...)
|
||||
return c.ParseBytes(c.buf)
|
||||
}
|
||||
|
||||
// ParseBytes parses Set-Cookie header.
|
||||
func (c *Cookie) ParseBytes(src []byte) error {
|
||||
c.Reset()
|
||||
|
||||
var s cookieScanner
|
||||
s.b = src
|
||||
|
||||
kv := &c.bufKV
|
||||
if !s.next(kv) {
|
||||
return errNoCookies
|
||||
}
|
||||
|
||||
c.key = append(c.key, kv.key...)
|
||||
c.value = append(c.value, kv.value...)
|
||||
|
||||
for s.next(kv) {
|
||||
if len(kv.key) != 0 {
|
||||
// Case insensitive switch on first char
|
||||
switch kv.key[0] | 0x20 {
|
||||
case 'm':
|
||||
if caseInsensitiveCompare(strCookieMaxAge, kv.key) {
|
||||
maxAge, err := ParseUint(kv.value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.maxAge = maxAge
|
||||
}
|
||||
|
||||
case 'e': // "expires"
|
||||
if caseInsensitiveCompare(strCookieExpires, kv.key) {
|
||||
v := b2s(kv.value)
|
||||
// Try the same two formats as net/http
|
||||
// See: https://github.com/golang/go/blob/00379be17e63a5b75b3237819392d2dc3b313a27/src/net/http/cookie.go#L133-L135
|
||||
exptime, err := time.ParseInLocation(time.RFC1123, v, time.UTC)
|
||||
if err != nil {
|
||||
exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.expire = exptime
|
||||
}
|
||||
|
||||
case 'd': // "domain"
|
||||
if caseInsensitiveCompare(strCookieDomain, kv.key) {
|
||||
c.domain = append(c.domain, kv.value...)
|
||||
}
|
||||
|
||||
case 'p': // "path"
|
||||
if caseInsensitiveCompare(strCookiePath, kv.key) {
|
||||
c.path = append(c.path, kv.value...)
|
||||
}
|
||||
|
||||
case 's': // "samesite"
|
||||
if caseInsensitiveCompare(strCookieSameSite, kv.key) {
|
||||
if len(kv.value) > 0 {
|
||||
// Case insensitive switch on first char
|
||||
switch kv.value[0] | 0x20 {
|
||||
case 'l': // "lax"
|
||||
if caseInsensitiveCompare(strCookieSameSiteLax, kv.value) {
|
||||
c.sameSite = CookieSameSiteLaxMode
|
||||
}
|
||||
case 's': // "strict"
|
||||
if caseInsensitiveCompare(strCookieSameSiteStrict, kv.value) {
|
||||
c.sameSite = CookieSameSiteStrictMode
|
||||
}
|
||||
case 'n': // "none"
|
||||
if caseInsensitiveCompare(strCookieSameSiteNone, kv.value) {
|
||||
c.sameSite = CookieSameSiteNoneMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if len(kv.value) != 0 {
|
||||
// Case insensitive switch on first char
|
||||
switch kv.value[0] | 0x20 {
|
||||
case 'h': // "httponly"
|
||||
if caseInsensitiveCompare(strCookieHTTPOnly, kv.value) {
|
||||
c.httpOnly = true
|
||||
}
|
||||
|
||||
case 's': // "secure"
|
||||
if caseInsensitiveCompare(strCookieSecure, kv.value) {
|
||||
c.secure = true
|
||||
} else if caseInsensitiveCompare(strCookieSameSite, kv.value) {
|
||||
c.sameSite = CookieSameSiteDefaultMode
|
||||
}
|
||||
case 'p': // "partitioned"
|
||||
if caseInsensitiveCompare(strCookiePartitioned, kv.value) {
|
||||
c.partitioned = true
|
||||
}
|
||||
}
|
||||
} // else empty or no match
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appendCookiePart(dst, key, value []byte) []byte {
|
||||
dst = append(dst, ';', ' ')
|
||||
dst = append(dst, key...)
|
||||
dst = append(dst, '=')
|
||||
return append(dst, value...)
|
||||
}
|
||||
|
||||
func getCookieKey(dst, src []byte) []byte {
|
||||
n := bytes.IndexByte(src, '=')
|
||||
if n >= 0 {
|
||||
src = src[:n]
|
||||
}
|
||||
return decodeCookieArg(dst, src, false)
|
||||
}
|
||||
|
||||
func appendRequestCookieBytes(dst []byte, cookies []argsKV) []byte {
|
||||
for i, n := 0, len(cookies); i < n; i++ {
|
||||
kv := &cookies[i]
|
||||
if len(kv.key) > 0 {
|
||||
dst = append(dst, kv.key...)
|
||||
dst = append(dst, '=')
|
||||
}
|
||||
dst = append(dst, kv.value...)
|
||||
if i+1 < n {
|
||||
dst = append(dst, ';', ' ')
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// For Response we can not use the above function as response cookies
|
||||
// already contain the key= in the value.
|
||||
func appendResponseCookieBytes(dst []byte, cookies []argsKV) []byte {
|
||||
for i, n := 0, len(cookies); i < n; i++ {
|
||||
kv := &cookies[i]
|
||||
dst = append(dst, kv.value...)
|
||||
if i+1 < n {
|
||||
dst = append(dst, ';', ' ')
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func parseRequestCookies(cookies []argsKV, src []byte) []argsKV {
|
||||
var s cookieScanner
|
||||
s.b = src
|
||||
var kv *argsKV
|
||||
cookies, kv = allocArg(cookies)
|
||||
for s.next(kv) {
|
||||
if len(kv.key) > 0 || len(kv.value) > 0 {
|
||||
cookies, kv = allocArg(cookies)
|
||||
}
|
||||
}
|
||||
return releaseArg(cookies)
|
||||
}
|
||||
|
||||
type cookieScanner struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func (s *cookieScanner) next(kv *argsKV) bool {
|
||||
b := s.b
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
isKey := true
|
||||
k := 0
|
||||
for i, c := range b {
|
||||
switch c {
|
||||
case '=':
|
||||
if isKey {
|
||||
isKey = false
|
||||
kv.key = decodeCookieArg(kv.key, b[:i], false)
|
||||
k = i + 1
|
||||
}
|
||||
case ';':
|
||||
if isKey {
|
||||
kv.key = kv.key[:0]
|
||||
}
|
||||
kv.value = decodeCookieArg(kv.value, b[k:i], true)
|
||||
s.b = b[i+1:]
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isKey {
|
||||
kv.key = kv.key[:0]
|
||||
}
|
||||
kv.value = decodeCookieArg(kv.value, b[k:], true)
|
||||
s.b = b[len(b):]
|
||||
return true
|
||||
}
|
||||
|
||||
func decodeCookieArg(dst, src []byte, skipQuotes bool) []byte {
|
||||
for len(src) > 0 && src[0] == ' ' {
|
||||
src = src[1:]
|
||||
}
|
||||
for len(src) > 0 && src[len(src)-1] == ' ' {
|
||||
src = src[:len(src)-1]
|
||||
}
|
||||
if skipQuotes {
|
||||
if len(src) > 1 && src[0] == '"' && src[len(src)-1] == '"' {
|
||||
src = src[1 : len(src)-1]
|
||||
}
|
||||
}
|
||||
return append(dst[:0], src...)
|
||||
}
|
||||
|
||||
// caseInsensitiveCompare does a case insensitive equality comparison of
|
||||
// two []byte. Assumes only letters need to be matched.
|
||||
func caseInsensitiveCompare(a, b []byte) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i]|0x20 != b[i]|0x20 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Package fasthttp provides fast HTTP server and client API.
|
||||
|
||||
Fasthttp provides the following features:
|
||||
|
||||
1. Optimized for speed. Easily handles more than 100K qps and more than 1M
|
||||
concurrent keep-alive connections on modern hardware.
|
||||
|
||||
2. Optimized for low memory usage.
|
||||
|
||||
3. Easy 'Connection: Upgrade' support via RequestCtx.Hijack.
|
||||
|
||||
4. Server provides the following anti-DoS limits:
|
||||
|
||||
- The number of concurrent connections.
|
||||
|
||||
- The number of concurrent connections per client IP.
|
||||
|
||||
- The number of requests per connection.
|
||||
|
||||
- Request read timeout.
|
||||
|
||||
- Response write timeout.
|
||||
|
||||
- Maximum request header size.
|
||||
|
||||
- Maximum request body size.
|
||||
|
||||
- Maximum request execution time.
|
||||
|
||||
- Maximum keep-alive connection lifetime.
|
||||
|
||||
- Early filtering out non-GET requests.
|
||||
|
||||
5. A lot of additional useful info is exposed to request handler:
|
||||
|
||||
- Server and client address.
|
||||
|
||||
- Per-request logger.
|
||||
|
||||
- Unique request id.
|
||||
|
||||
- Request start time.
|
||||
|
||||
- Connection start time.
|
||||
|
||||
- Request sequence number for the current connection.
|
||||
|
||||
6. Client supports automatic retry on idempotent requests' failure.
|
||||
|
||||
7. Fasthttp API is designed with the ability to extend existing client
|
||||
and server implementations or to write custom client and server
|
||||
implementations from scratch.
|
||||
*/
|
||||
package fasthttp
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package fasthttputil provides utility functions for fasthttp.
|
||||
package fasthttputil
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package fasthttputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ErrInmemoryListenerClosed indicates that the InmemoryListener is already closed.
|
||||
var ErrInmemoryListenerClosed = errors.New("InmemoryListener is already closed: use of closed network connection")
|
||||
|
||||
// InmemoryListener provides in-memory dialer<->net.Listener implementation.
|
||||
//
|
||||
// It may be used either for fast in-process client<->server communications
|
||||
// without network stack overhead or for client<->server tests.
|
||||
type InmemoryListener struct {
|
||||
lock sync.Mutex
|
||||
closed bool
|
||||
conns chan acceptConn
|
||||
listenerAddr net.Addr
|
||||
addrLock sync.RWMutex
|
||||
}
|
||||
|
||||
type acceptConn struct {
|
||||
conn net.Conn
|
||||
accepted chan struct{}
|
||||
}
|
||||
|
||||
// NewInmemoryListener returns new in-memory dialer<->net.Listener.
|
||||
func NewInmemoryListener() *InmemoryListener {
|
||||
return &InmemoryListener{
|
||||
conns: make(chan acceptConn, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
// SetLocalAddr sets the (simulated) local address for the listener.
|
||||
func (ln *InmemoryListener) SetLocalAddr(localAddr net.Addr) {
|
||||
ln.addrLock.Lock()
|
||||
defer ln.addrLock.Unlock()
|
||||
|
||||
ln.listenerAddr = localAddr
|
||||
}
|
||||
|
||||
// Accept implements net.Listener's Accept.
|
||||
//
|
||||
// It is safe calling Accept from concurrently running goroutines.
|
||||
//
|
||||
// Accept returns new connection per each Dial call.
|
||||
func (ln *InmemoryListener) Accept() (net.Conn, error) {
|
||||
c, ok := <-ln.conns
|
||||
if !ok {
|
||||
return nil, ErrInmemoryListenerClosed
|
||||
}
|
||||
close(c.accepted)
|
||||
return c.conn, nil
|
||||
}
|
||||
|
||||
// Close implements net.Listener's Close.
|
||||
func (ln *InmemoryListener) Close() error {
|
||||
var err error
|
||||
|
||||
ln.lock.Lock()
|
||||
if !ln.closed {
|
||||
close(ln.conns)
|
||||
ln.closed = true
|
||||
} else {
|
||||
err = ErrInmemoryListenerClosed
|
||||
}
|
||||
ln.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
type inmemoryAddr int
|
||||
|
||||
func (inmemoryAddr) Network() string {
|
||||
return "inmemory"
|
||||
}
|
||||
|
||||
func (inmemoryAddr) String() string {
|
||||
return "InmemoryListener"
|
||||
}
|
||||
|
||||
// Addr implements net.Listener's Addr.
|
||||
func (ln *InmemoryListener) Addr() net.Addr {
|
||||
ln.addrLock.RLock()
|
||||
defer ln.addrLock.RUnlock()
|
||||
|
||||
if ln.listenerAddr != nil {
|
||||
return ln.listenerAddr
|
||||
}
|
||||
|
||||
return inmemoryAddr(0)
|
||||
}
|
||||
|
||||
// Dial creates new client<->server connection.
|
||||
// Just like a real Dial it only returns once the server
|
||||
// has accepted the connection.
|
||||
//
|
||||
// It is safe calling Dial from concurrently running goroutines.
|
||||
func (ln *InmemoryListener) Dial() (net.Conn, error) {
|
||||
return ln.DialWithLocalAddr(nil)
|
||||
}
|
||||
|
||||
// DialWithLocalAddr creates new client<->server connection.
|
||||
// Just like a real Dial it only returns once the server
|
||||
// has accepted the connection. The local address of the
|
||||
// client connection can be set with local.
|
||||
//
|
||||
// It is safe calling Dial from concurrently running goroutines.
|
||||
func (ln *InmemoryListener) DialWithLocalAddr(local net.Addr) (net.Conn, error) {
|
||||
pc := NewPipeConns()
|
||||
|
||||
pc.SetAddresses(local, ln.Addr(), ln.Addr(), local)
|
||||
|
||||
cConn := pc.Conn1()
|
||||
sConn := pc.Conn2()
|
||||
ln.lock.Lock()
|
||||
accepted := make(chan struct{})
|
||||
if !ln.closed {
|
||||
ln.conns <- acceptConn{sConn, accepted}
|
||||
// Wait until the connection has been accepted.
|
||||
<-accepted
|
||||
} else {
|
||||
_ = sConn.Close()
|
||||
_ = cConn.Close()
|
||||
cConn = nil
|
||||
}
|
||||
ln.lock.Unlock()
|
||||
|
||||
if cConn == nil {
|
||||
return nil, ErrInmemoryListenerClosed
|
||||
}
|
||||
return cConn, nil
|
||||
}
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
package fasthttputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewPipeConns returns new bi-directional connection pipe.
|
||||
//
|
||||
// PipeConns is NOT safe for concurrent use by multiple goroutines!
|
||||
func NewPipeConns() *PipeConns {
|
||||
ch1 := make(chan *byteBuffer, 4)
|
||||
ch2 := make(chan *byteBuffer, 4)
|
||||
|
||||
pc := &PipeConns{
|
||||
stopCh: make(chan struct{}),
|
||||
}
|
||||
pc.c1.rCh = ch1
|
||||
pc.c1.wCh = ch2
|
||||
pc.c2.rCh = ch2
|
||||
pc.c2.wCh = ch1
|
||||
pc.c1.pc = pc
|
||||
pc.c2.pc = pc
|
||||
return pc
|
||||
}
|
||||
|
||||
// PipeConns provides bi-directional connection pipe,
|
||||
// which use in-process memory as a transport.
|
||||
//
|
||||
// PipeConns must be created by calling NewPipeConns.
|
||||
//
|
||||
// PipeConns has the following additional features comparing to connections
|
||||
// returned from net.Pipe():
|
||||
//
|
||||
// - It is faster.
|
||||
// - It buffers Write calls, so there is no need to have concurrent goroutine
|
||||
// calling Read in order to unblock each Write call.
|
||||
// - It supports read and write deadlines.
|
||||
//
|
||||
// PipeConns is NOT safe for concurrent use by multiple goroutines!
|
||||
type PipeConns struct {
|
||||
c1 pipeConn
|
||||
c2 pipeConn
|
||||
stopCh chan struct{}
|
||||
stopChLock sync.Mutex
|
||||
}
|
||||
|
||||
// SetAddresses sets the local and remote addresses for the connection.
|
||||
func (pc *PipeConns) SetAddresses(localAddr1, remoteAddr1, localAddr2, remoteAddr2 net.Addr) {
|
||||
pc.c1.addrLock.Lock()
|
||||
defer pc.c1.addrLock.Unlock()
|
||||
|
||||
pc.c2.addrLock.Lock()
|
||||
defer pc.c2.addrLock.Unlock()
|
||||
|
||||
pc.c1.localAddr = localAddr1
|
||||
pc.c1.remoteAddr = remoteAddr1
|
||||
|
||||
pc.c2.localAddr = localAddr2
|
||||
pc.c2.remoteAddr = remoteAddr2
|
||||
}
|
||||
|
||||
// Conn1 returns the first end of bi-directional pipe.
|
||||
//
|
||||
// Data written to Conn1 may be read from Conn2.
|
||||
// Data written to Conn2 may be read from Conn1.
|
||||
func (pc *PipeConns) Conn1() net.Conn {
|
||||
return &pc.c1
|
||||
}
|
||||
|
||||
// Conn2 returns the second end of bi-directional pipe.
|
||||
//
|
||||
// Data written to Conn2 may be read from Conn1.
|
||||
// Data written to Conn1 may be read from Conn2.
|
||||
func (pc *PipeConns) Conn2() net.Conn {
|
||||
return &pc.c2
|
||||
}
|
||||
|
||||
// Close closes pipe connections.
|
||||
func (pc *PipeConns) Close() error {
|
||||
pc.stopChLock.Lock()
|
||||
select {
|
||||
case <-pc.stopCh:
|
||||
default:
|
||||
close(pc.stopCh)
|
||||
}
|
||||
pc.stopChLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type pipeConn struct {
|
||||
b *byteBuffer
|
||||
bb []byte
|
||||
|
||||
rCh chan *byteBuffer
|
||||
wCh chan *byteBuffer
|
||||
pc *PipeConns
|
||||
|
||||
readDeadlineTimer *time.Timer
|
||||
writeDeadlineTimer *time.Timer
|
||||
|
||||
readDeadlineCh <-chan time.Time
|
||||
writeDeadlineCh <-chan time.Time
|
||||
|
||||
readDeadlineChLock sync.Mutex
|
||||
|
||||
localAddr net.Addr
|
||||
remoteAddr net.Addr
|
||||
addrLock sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *pipeConn) Write(p []byte) (int, error) {
|
||||
b := acquireByteBuffer()
|
||||
b.b = append(b.b[:0], p...)
|
||||
|
||||
select {
|
||||
case <-c.pc.stopCh:
|
||||
releaseByteBuffer(b)
|
||||
return 0, errConnectionClosed
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case c.wCh <- b:
|
||||
default:
|
||||
select {
|
||||
case c.wCh <- b:
|
||||
case <-c.writeDeadlineCh:
|
||||
c.writeDeadlineCh = closedDeadlineCh
|
||||
return 0, ErrTimeout
|
||||
case <-c.pc.stopCh:
|
||||
releaseByteBuffer(b)
|
||||
return 0, errConnectionClosed
|
||||
}
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) Read(p []byte) (int, error) {
|
||||
mayBlock := true
|
||||
nn := 0
|
||||
for len(p) > 0 {
|
||||
n, err := c.read(p, mayBlock)
|
||||
nn += n
|
||||
if err != nil {
|
||||
if !mayBlock && err == errWouldBlock {
|
||||
err = nil
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
p = p[n:]
|
||||
mayBlock = false
|
||||
}
|
||||
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) read(p []byte, mayBlock bool) (int, error) {
|
||||
if len(c.bb) == 0 {
|
||||
if err := c.readNextByteBuffer(mayBlock); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
n := copy(p, c.bb)
|
||||
c.bb = c.bb[n:]
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) readNextByteBuffer(mayBlock bool) error {
|
||||
releaseByteBuffer(c.b)
|
||||
c.b = nil
|
||||
|
||||
select {
|
||||
case c.b = <-c.rCh:
|
||||
default:
|
||||
if !mayBlock {
|
||||
return errWouldBlock
|
||||
}
|
||||
c.readDeadlineChLock.Lock()
|
||||
readDeadlineCh := c.readDeadlineCh
|
||||
c.readDeadlineChLock.Unlock()
|
||||
select {
|
||||
case c.b = <-c.rCh:
|
||||
case <-readDeadlineCh:
|
||||
c.readDeadlineChLock.Lock()
|
||||
c.readDeadlineCh = closedDeadlineCh
|
||||
c.readDeadlineChLock.Unlock()
|
||||
// rCh may contain data when deadline is reached.
|
||||
// Read the data before returning ErrTimeout.
|
||||
select {
|
||||
case c.b = <-c.rCh:
|
||||
default:
|
||||
return ErrTimeout
|
||||
}
|
||||
case <-c.pc.stopCh:
|
||||
// rCh may contain data when stopCh is closed.
|
||||
// Read the data before returning EOF.
|
||||
select {
|
||||
case c.b = <-c.rCh:
|
||||
default:
|
||||
return io.EOF
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.bb = c.b.b
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
errWouldBlock = errors.New("would block")
|
||||
errConnectionClosed = errors.New("connection closed")
|
||||
)
|
||||
|
||||
type timeoutError struct{}
|
||||
|
||||
func (e *timeoutError) Error() string {
|
||||
return "timeout"
|
||||
}
|
||||
|
||||
// Only implement the Timeout() function of the net.Error interface.
|
||||
// This allows for checks like:
|
||||
//
|
||||
// if x, ok := err.(interface{ Timeout() bool }); ok && x.Timeout() {
|
||||
func (e *timeoutError) Timeout() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrTimeout is returned from Read() or Write() on timeout.
|
||||
var ErrTimeout = &timeoutError{}
|
||||
|
||||
func (c *pipeConn) Close() error {
|
||||
return c.pc.Close()
|
||||
}
|
||||
|
||||
func (c *pipeConn) LocalAddr() net.Addr {
|
||||
c.addrLock.RLock()
|
||||
defer c.addrLock.RUnlock()
|
||||
|
||||
if c.localAddr != nil {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
return pipeAddr(0)
|
||||
}
|
||||
|
||||
func (c *pipeConn) RemoteAddr() net.Addr {
|
||||
c.addrLock.RLock()
|
||||
defer c.addrLock.RUnlock()
|
||||
|
||||
if c.remoteAddr != nil {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
return pipeAddr(0)
|
||||
}
|
||||
|
||||
func (c *pipeConn) SetDeadline(deadline time.Time) error {
|
||||
c.SetReadDeadline(deadline) //nolint:errcheck
|
||||
c.SetWriteDeadline(deadline) //nolint:errcheck
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) SetReadDeadline(deadline time.Time) error {
|
||||
if c.readDeadlineTimer == nil {
|
||||
c.readDeadlineTimer = time.NewTimer(time.Hour)
|
||||
}
|
||||
readDeadlineCh := updateTimer(c.readDeadlineTimer, deadline)
|
||||
c.readDeadlineChLock.Lock()
|
||||
c.readDeadlineCh = readDeadlineCh
|
||||
c.readDeadlineChLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) SetWriteDeadline(deadline time.Time) error {
|
||||
if c.writeDeadlineTimer == nil {
|
||||
c.writeDeadlineTimer = time.NewTimer(time.Hour)
|
||||
}
|
||||
c.writeDeadlineCh = updateTimer(c.writeDeadlineTimer, deadline)
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateTimer(t *time.Timer, deadline time.Time) <-chan time.Time {
|
||||
if !t.Stop() {
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if deadline.IsZero() {
|
||||
return nil
|
||||
}
|
||||
d := time.Until(deadline)
|
||||
if d <= 0 {
|
||||
return closedDeadlineCh
|
||||
}
|
||||
t.Reset(d)
|
||||
return t.C
|
||||
}
|
||||
|
||||
var closedDeadlineCh = func() <-chan time.Time {
|
||||
ch := make(chan time.Time)
|
||||
close(ch)
|
||||
return ch
|
||||
}()
|
||||
|
||||
type pipeAddr int
|
||||
|
||||
func (pipeAddr) Network() string {
|
||||
return "pipe"
|
||||
}
|
||||
|
||||
func (pipeAddr) String() string {
|
||||
return "pipe"
|
||||
}
|
||||
|
||||
type byteBuffer struct {
|
||||
b []byte
|
||||
}
|
||||
|
||||
func acquireByteBuffer() *byteBuffer {
|
||||
return byteBufferPool.Get().(*byteBuffer)
|
||||
}
|
||||
|
||||
func releaseByteBuffer(b *byteBuffer) {
|
||||
if b != nil {
|
||||
byteBufferPool.Put(b)
|
||||
}
|
||||
}
|
||||
|
||||
var byteBufferPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &byteBuffer{
|
||||
b: make([]byte, 1024),
|
||||
}
|
||||
},
|
||||
}
|
||||
+1776
File diff suppressed because it is too large
Load Diff
+3564
File diff suppressed because it is too large
Load Diff
+165
@@ -0,0 +1,165 @@
|
||||
package fasthttp
|
||||
|
||||
// Headers.
|
||||
const (
|
||||
// Authentication.
|
||||
HeaderAuthorization = "Authorization"
|
||||
HeaderProxyAuthenticate = "Proxy-Authenticate"
|
||||
HeaderProxyAuthorization = "Proxy-Authorization"
|
||||
HeaderWWWAuthenticate = "WWW-Authenticate"
|
||||
|
||||
// Caching.
|
||||
HeaderAge = "Age"
|
||||
HeaderCacheControl = "Cache-Control"
|
||||
HeaderClearSiteData = "Clear-Site-Data"
|
||||
HeaderExpires = "Expires"
|
||||
HeaderPragma = "Pragma"
|
||||
HeaderWarning = "Warning"
|
||||
|
||||
// Client hints.
|
||||
HeaderAcceptCH = "Accept-CH"
|
||||
HeaderAcceptCHLifetime = "Accept-CH-Lifetime"
|
||||
HeaderContentDPR = "Content-DPR"
|
||||
HeaderDPR = "DPR"
|
||||
HeaderEarlyData = "Early-Data"
|
||||
HeaderSaveData = "Save-Data"
|
||||
HeaderViewportWidth = "Viewport-Width"
|
||||
HeaderWidth = "Width"
|
||||
|
||||
// Conditionals.
|
||||
HeaderETag = "ETag"
|
||||
HeaderIfMatch = "If-Match"
|
||||
HeaderIfModifiedSince = "If-Modified-Since"
|
||||
HeaderIfNoneMatch = "If-None-Match"
|
||||
HeaderIfUnmodifiedSince = "If-Unmodified-Since"
|
||||
HeaderLastModified = "Last-Modified"
|
||||
HeaderVary = "Vary"
|
||||
|
||||
// Connection management.
|
||||
HeaderConnection = "Connection"
|
||||
HeaderKeepAlive = "Keep-Alive"
|
||||
HeaderProxyConnection = "Proxy-Connection"
|
||||
|
||||
// Content negotiation.
|
||||
HeaderAccept = "Accept"
|
||||
HeaderAcceptCharset = "Accept-Charset"
|
||||
HeaderAcceptEncoding = "Accept-Encoding"
|
||||
HeaderAcceptLanguage = "Accept-Language"
|
||||
|
||||
// Controls.
|
||||
HeaderCookie = "Cookie"
|
||||
HeaderExpect = "Expect"
|
||||
HeaderMaxForwards = "Max-Forwards"
|
||||
HeaderSetCookie = "Set-Cookie"
|
||||
|
||||
// CORS.
|
||||
HeaderAccessControlAllowCredentials = "Access-Control-Allow-Credentials"
|
||||
HeaderAccessControlAllowHeaders = "Access-Control-Allow-Headers"
|
||||
HeaderAccessControlAllowMethods = "Access-Control-Allow-Methods"
|
||||
HeaderAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
HeaderAccessControlExposeHeaders = "Access-Control-Expose-Headers"
|
||||
HeaderAccessControlMaxAge = "Access-Control-Max-Age"
|
||||
HeaderAccessControlRequestHeaders = "Access-Control-Request-Headers"
|
||||
HeaderAccessControlRequestMethod = "Access-Control-Request-Method"
|
||||
HeaderOrigin = "Origin"
|
||||
HeaderTimingAllowOrigin = "Timing-Allow-Origin"
|
||||
HeaderXPermittedCrossDomainPolicies = "X-Permitted-Cross-Domain-Policies"
|
||||
|
||||
// Do Not Track.
|
||||
HeaderDNT = "DNT"
|
||||
HeaderTk = "Tk"
|
||||
|
||||
// Downloads.
|
||||
HeaderContentDisposition = "Content-Disposition"
|
||||
|
||||
// Message body information.
|
||||
HeaderContentEncoding = "Content-Encoding"
|
||||
HeaderContentLanguage = "Content-Language"
|
||||
HeaderContentLength = "Content-Length"
|
||||
HeaderContentLocation = "Content-Location"
|
||||
HeaderContentType = "Content-Type"
|
||||
|
||||
// Proxies.
|
||||
HeaderForwarded = "Forwarded"
|
||||
HeaderVia = "Via"
|
||||
HeaderXForwardedFor = "X-Forwarded-For"
|
||||
HeaderXForwardedHost = "X-Forwarded-Host"
|
||||
HeaderXForwardedProto = "X-Forwarded-Proto"
|
||||
|
||||
// Redirects.
|
||||
HeaderLocation = "Location"
|
||||
|
||||
// Request context.
|
||||
HeaderFrom = "From"
|
||||
HeaderHost = "Host"
|
||||
HeaderReferer = "Referer"
|
||||
HeaderReferrerPolicy = "Referrer-Policy"
|
||||
HeaderUserAgent = "User-Agent"
|
||||
|
||||
// Response context.
|
||||
HeaderAllow = "Allow"
|
||||
HeaderServer = "Server"
|
||||
|
||||
// Range requests.
|
||||
HeaderAcceptRanges = "Accept-Ranges"
|
||||
HeaderContentRange = "Content-Range"
|
||||
HeaderIfRange = "If-Range"
|
||||
HeaderRange = "Range"
|
||||
|
||||
// Security.
|
||||
HeaderContentSecurityPolicy = "Content-Security-Policy"
|
||||
HeaderContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only"
|
||||
HeaderCrossOriginResourcePolicy = "Cross-Origin-Resource-Policy"
|
||||
HeaderExpectCT = "Expect-CT"
|
||||
HeaderFeaturePolicy = "Feature-Policy"
|
||||
HeaderPublicKeyPins = "Public-Key-Pins"
|
||||
HeaderPublicKeyPinsReportOnly = "Public-Key-Pins-Report-Only"
|
||||
HeaderStrictTransportSecurity = "Strict-Transport-Security"
|
||||
HeaderUpgradeInsecureRequests = "Upgrade-Insecure-Requests"
|
||||
HeaderXContentTypeOptions = "X-Content-Type-Options"
|
||||
HeaderXDownloadOptions = "X-Download-Options"
|
||||
HeaderXFrameOptions = "X-Frame-Options"
|
||||
HeaderXPoweredBy = "X-Powered-By"
|
||||
HeaderXXSSProtection = "X-XSS-Protection"
|
||||
|
||||
// Server-sent event.
|
||||
HeaderLastEventID = "Last-Event-ID"
|
||||
HeaderNEL = "NEL"
|
||||
HeaderPingFrom = "Ping-From"
|
||||
HeaderPingTo = "Ping-To"
|
||||
HeaderReportTo = "Report-To"
|
||||
|
||||
// Transfer coding.
|
||||
HeaderTE = "TE"
|
||||
HeaderTrailer = "Trailer"
|
||||
HeaderTransferEncoding = "Transfer-Encoding"
|
||||
|
||||
// WebSockets.
|
||||
HeaderSecWebSocketAccept = "Sec-WebSocket-Accept"
|
||||
HeaderSecWebSocketExtensions = "Sec-WebSocket-Extensions" /* #nosec G101 */
|
||||
HeaderSecWebSocketKey = "Sec-WebSocket-Key"
|
||||
HeaderSecWebSocketProtocol = "Sec-WebSocket-Protocol"
|
||||
HeaderSecWebSocketVersion = "Sec-WebSocket-Version"
|
||||
|
||||
// Other.
|
||||
HeaderAcceptPatch = "Accept-Patch"
|
||||
HeaderAcceptPushPolicy = "Accept-Push-Policy"
|
||||
HeaderAcceptSignature = "Accept-Signature"
|
||||
HeaderAltSvc = "Alt-Svc"
|
||||
HeaderDate = "Date"
|
||||
HeaderIndex = "Index"
|
||||
HeaderLargeAllocation = "Large-Allocation"
|
||||
HeaderLink = "Link"
|
||||
HeaderPushPolicy = "Push-Policy"
|
||||
HeaderRetryAfter = "Retry-After"
|
||||
HeaderServerTiming = "Server-Timing"
|
||||
HeaderSignature = "Signature"
|
||||
HeaderSignedHeaders = "Signed-Headers"
|
||||
HeaderSourceMap = "SourceMap"
|
||||
HeaderUpgrade = "Upgrade"
|
||||
HeaderXDNSPrefetchControl = "X-DNS-Prefetch-Control"
|
||||
HeaderXPingback = "X-Pingback"
|
||||
HeaderXRequestedWith = "X-Requested-With"
|
||||
HeaderXRobotsTag = "X-Robots-Tag"
|
||||
HeaderXUACompatible = "X-UA-Compatible"
|
||||
)
|
||||
+2461
File diff suppressed because it is too large
Load Diff
+203
@@ -0,0 +1,203 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// BalancingClient is the interface for clients, which may be passed
|
||||
// to LBClient.Clients.
|
||||
type BalancingClient interface {
|
||||
DoDeadline(req *Request, resp *Response, deadline time.Time) error
|
||||
PendingRequests() int
|
||||
}
|
||||
|
||||
// LBClient balances requests among available LBClient.Clients.
|
||||
//
|
||||
// It has the following features:
|
||||
//
|
||||
// - Balances load among available clients using 'least loaded' + 'least total'
|
||||
// hybrid technique.
|
||||
// - Dynamically decreases load on unhealthy clients.
|
||||
//
|
||||
// It is forbidden copying LBClient instances. Create new instances instead.
|
||||
//
|
||||
// It is safe calling LBClient methods from concurrently running goroutines.
|
||||
type LBClient struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Clients must contain non-zero clients list.
|
||||
// Incoming requests are balanced among these clients.
|
||||
Clients []BalancingClient
|
||||
|
||||
// HealthCheck is a callback called after each request.
|
||||
//
|
||||
// The request, response and the error returned by the client
|
||||
// is passed to HealthCheck, so the callback may determine whether
|
||||
// the client is healthy.
|
||||
//
|
||||
// Load on the current client is decreased if HealthCheck returns false.
|
||||
//
|
||||
// By default HealthCheck returns false if err != nil.
|
||||
HealthCheck func(req *Request, resp *Response, err error) bool
|
||||
|
||||
// Timeout is the request timeout used when calling LBClient.Do.
|
||||
//
|
||||
// DefaultLBClientTimeout is used by default.
|
||||
Timeout time.Duration
|
||||
|
||||
cs []*lbClient
|
||||
|
||||
once sync.Once
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// DefaultLBClientTimeout is the default request timeout used by LBClient
|
||||
// when calling LBClient.Do.
|
||||
//
|
||||
// The timeout may be overridden via LBClient.Timeout.
|
||||
const DefaultLBClientTimeout = time.Second
|
||||
|
||||
// DoDeadline calls DoDeadline on the least loaded client.
|
||||
func (cc *LBClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
return cc.get().DoDeadline(req, resp, deadline)
|
||||
}
|
||||
|
||||
// DoTimeout calculates deadline and calls DoDeadline on the least loaded client.
|
||||
func (cc *LBClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
return cc.get().DoDeadline(req, resp, deadline)
|
||||
}
|
||||
|
||||
// Do calculates timeout using LBClient.Timeout and calls DoTimeout
|
||||
// on the least loaded client.
|
||||
func (cc *LBClient) Do(req *Request, resp *Response) error {
|
||||
timeout := cc.Timeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultLBClientTimeout
|
||||
}
|
||||
return cc.DoTimeout(req, resp, timeout)
|
||||
}
|
||||
|
||||
func (cc *LBClient) init() {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
if len(cc.Clients) == 0 {
|
||||
// developer sanity-check
|
||||
panic("BUG: LBClient.Clients cannot be empty")
|
||||
}
|
||||
for _, c := range cc.Clients {
|
||||
cc.cs = append(cc.cs, &lbClient{
|
||||
c: c,
|
||||
healthCheck: cc.HealthCheck,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AddClient adds a new client to the balanced clients and
|
||||
// returns the new total number of clients.
|
||||
func (cc *LBClient) AddClient(c BalancingClient) int {
|
||||
cc.mu.Lock()
|
||||
cc.cs = append(cc.cs, &lbClient{
|
||||
c: c,
|
||||
healthCheck: cc.HealthCheck,
|
||||
})
|
||||
cc.mu.Unlock()
|
||||
return len(cc.cs)
|
||||
}
|
||||
|
||||
// RemoveClients removes clients using the provided callback.
|
||||
// If rc returns true, the passed client will be removed.
|
||||
// Returns the new total number of clients.
|
||||
func (cc *LBClient) RemoveClients(rc func(BalancingClient) bool) int {
|
||||
cc.mu.Lock()
|
||||
n := 0
|
||||
for idx, cs := range cc.cs {
|
||||
cc.cs[idx] = nil
|
||||
if rc(cs.c) {
|
||||
continue
|
||||
}
|
||||
cc.cs[n] = cs
|
||||
n++
|
||||
}
|
||||
cc.cs = cc.cs[:n]
|
||||
|
||||
cc.mu.Unlock()
|
||||
return len(cc.cs)
|
||||
}
|
||||
|
||||
func (cc *LBClient) get() *lbClient {
|
||||
cc.once.Do(cc.init)
|
||||
|
||||
cc.mu.RLock()
|
||||
cs := cc.cs
|
||||
|
||||
minC := cs[0]
|
||||
minN := minC.PendingRequests()
|
||||
minT := atomic.LoadUint64(&minC.total)
|
||||
for _, c := range cs[1:] {
|
||||
n := c.PendingRequests()
|
||||
t := atomic.LoadUint64(&c.total) /* #nosec G601 */
|
||||
if n < minN || (n == minN && t < minT) {
|
||||
minC = c
|
||||
minN = n
|
||||
minT = t
|
||||
}
|
||||
}
|
||||
cc.mu.RUnlock()
|
||||
return minC
|
||||
}
|
||||
|
||||
type lbClient struct {
|
||||
c BalancingClient
|
||||
healthCheck func(req *Request, resp *Response, err error) bool
|
||||
penalty uint32
|
||||
|
||||
// total amount of requests handled.
|
||||
total uint64
|
||||
}
|
||||
|
||||
func (c *lbClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
err := c.c.DoDeadline(req, resp, deadline)
|
||||
if !c.isHealthy(req, resp, err) && c.incPenalty() {
|
||||
// Penalize the client returning error, so the next requests
|
||||
// are routed to another clients.
|
||||
time.AfterFunc(penaltyDuration, c.decPenalty)
|
||||
} else {
|
||||
atomic.AddUint64(&c.total, 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *lbClient) PendingRequests() int {
|
||||
n := c.c.PendingRequests()
|
||||
m := atomic.LoadUint32(&c.penalty)
|
||||
return n + int(m)
|
||||
}
|
||||
|
||||
func (c *lbClient) isHealthy(req *Request, resp *Response, err error) bool {
|
||||
if c.healthCheck == nil {
|
||||
return err == nil
|
||||
}
|
||||
return c.healthCheck(req, resp, err)
|
||||
}
|
||||
|
||||
func (c *lbClient) incPenalty() bool {
|
||||
m := atomic.AddUint32(&c.penalty, 1)
|
||||
if m > maxPenalty {
|
||||
c.decPenalty()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *lbClient) decPenalty() {
|
||||
atomic.AddUint32(&c.penalty, ^uint32(0))
|
||||
}
|
||||
|
||||
const (
|
||||
maxPenalty = 300
|
||||
|
||||
penaltyDuration = 3 * time.Second
|
||||
)
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package fasthttp
|
||||
|
||||
// HTTP methods were copied from net/http.
|
||||
const (
|
||||
MethodGet = "GET" // RFC 7231, 4.3.1
|
||||
MethodHead = "HEAD" // RFC 7231, 4.3.2
|
||||
MethodPost = "POST" // RFC 7231, 4.3.3
|
||||
MethodPut = "PUT" // RFC 7231, 4.3.4
|
||||
MethodPatch = "PATCH" // RFC 5789
|
||||
MethodDelete = "DELETE" // RFC 7231, 4.3.5
|
||||
MethodConnect = "CONNECT" // RFC 7231, 4.3.6
|
||||
MethodOptions = "OPTIONS" // RFC 7231, 4.3.7
|
||||
MethodTrace = "TRACE" // RFC 7231, 4.3.8
|
||||
)
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fasthttp
|
||||
|
||||
// Embed this type into a struct, which mustn't be copied,
|
||||
// so `go vet` gives a warning if this struct is copied.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/8005#issuecomment-190753527 for details.
|
||||
// and also: https://stackoverflow.com/questions/52494458/nocopy-minimal-example
|
||||
type noCopy struct{}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
func (*noCopy) Unlock() {}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type perIPConnCounter struct {
|
||||
perIPConnPool sync.Pool
|
||||
perIPTLSConnPool sync.Pool
|
||||
lock sync.Mutex
|
||||
m map[uint32]int
|
||||
}
|
||||
|
||||
func (cc *perIPConnCounter) Register(ip uint32) int {
|
||||
cc.lock.Lock()
|
||||
if cc.m == nil {
|
||||
cc.m = make(map[uint32]int)
|
||||
}
|
||||
n := cc.m[ip] + 1
|
||||
cc.m[ip] = n
|
||||
cc.lock.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (cc *perIPConnCounter) Unregister(ip uint32) {
|
||||
cc.lock.Lock()
|
||||
defer cc.lock.Unlock()
|
||||
if cc.m == nil {
|
||||
// developer safeguard
|
||||
panic("BUG: perIPConnCounter.Register() wasn't called")
|
||||
}
|
||||
n := cc.m[ip] - 1
|
||||
if n < 0 {
|
||||
n = 0
|
||||
}
|
||||
cc.m[ip] = n
|
||||
}
|
||||
|
||||
type perIPConn struct {
|
||||
net.Conn
|
||||
|
||||
ip uint32
|
||||
perIPConnCounter *perIPConnCounter
|
||||
}
|
||||
|
||||
type perIPTLSConn struct {
|
||||
*tls.Conn
|
||||
|
||||
ip uint32
|
||||
perIPConnCounter *perIPConnCounter
|
||||
}
|
||||
|
||||
func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) net.Conn {
|
||||
if tlcConn, ok := conn.(*tls.Conn); ok {
|
||||
v := counter.perIPTLSConnPool.Get()
|
||||
if v == nil {
|
||||
return &perIPTLSConn{
|
||||
perIPConnCounter: counter,
|
||||
Conn: tlcConn,
|
||||
ip: ip,
|
||||
}
|
||||
}
|
||||
c := v.(*perIPConn)
|
||||
c.Conn = conn
|
||||
c.ip = ip
|
||||
return c
|
||||
}
|
||||
|
||||
v := counter.perIPConnPool.Get()
|
||||
if v == nil {
|
||||
return &perIPConn{
|
||||
perIPConnCounter: counter,
|
||||
Conn: conn,
|
||||
ip: ip,
|
||||
}
|
||||
}
|
||||
c := v.(*perIPConn)
|
||||
c.Conn = conn
|
||||
c.ip = ip
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *perIPConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
c.perIPConnCounter.Unregister(c.ip)
|
||||
c.Conn = nil
|
||||
c.perIPConnCounter.perIPConnPool.Put(c)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *perIPTLSConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
c.perIPConnCounter.Unregister(c.ip)
|
||||
c.Conn = nil
|
||||
c.perIPConnCounter.perIPTLSConnPool.Put(c)
|
||||
return err
|
||||
}
|
||||
|
||||
func getUint32IP(c net.Conn) uint32 {
|
||||
return ip2uint32(getConnIP4(c))
|
||||
}
|
||||
|
||||
func getConnIP4(c net.Conn) net.IP {
|
||||
addr := c.RemoteAddr()
|
||||
ipAddr, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return net.IPv4zero
|
||||
}
|
||||
return ipAddr.IP.To4()
|
||||
}
|
||||
|
||||
func ip2uint32(ip net.IP) uint32 {
|
||||
if len(ip) != 4 {
|
||||
return 0
|
||||
}
|
||||
return uint32(ip[0])<<24 | uint32(ip[1])<<16 | uint32(ip[2])<<8 | uint32(ip[3])
|
||||
}
|
||||
|
||||
func uint322ip(ip uint32) net.IP {
|
||||
b := make([]byte, 4)
|
||||
b[0] = byte(ip >> 24)
|
||||
b[1] = byte(ip >> 16)
|
||||
b[2] = byte(ip >> 8)
|
||||
b[3] = byte(ip)
|
||||
return b
|
||||
}
|
||||
BIN
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Max Riveiro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
//go:build !windows && !aix
|
||||
|
||||
// Package reuseport provides TCP net.Listener with SO_REUSEPORT support.
|
||||
//
|
||||
// SO_REUSEPORT allows linear scaling server performance on multi-CPU servers.
|
||||
// See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for more details :)
|
||||
//
|
||||
// The package is based on https://github.com/kavu/go_reuseport .
|
||||
package reuseport
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/tcplisten"
|
||||
)
|
||||
|
||||
// Listen returns TCP listener with SO_REUSEPORT option set.
|
||||
//
|
||||
// The returned listener tries enabling the following TCP options, which usually
|
||||
// have positive impact on performance:
|
||||
//
|
||||
// - TCP_DEFER_ACCEPT. This option expects that the server reads from accepted
|
||||
// connections before writing to them.
|
||||
//
|
||||
// - TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details.
|
||||
//
|
||||
// Use https://github.com/valyala/tcplisten if you want customizing
|
||||
// these options.
|
||||
//
|
||||
// Only tcp4 and tcp6 networks are supported.
|
||||
//
|
||||
// ErrNoReusePort error is returned if the system doesn't support SO_REUSEPORT.
|
||||
func Listen(network, addr string) (net.Listener, error) {
|
||||
ln, err := cfg.NewListener(network, addr)
|
||||
if err != nil && strings.Contains(err.Error(), "SO_REUSEPORT") {
|
||||
return nil, &ErrNoReusePort{err}
|
||||
}
|
||||
return ln, err
|
||||
}
|
||||
|
||||
var cfg = &tcplisten.Config{
|
||||
ReusePort: true,
|
||||
DeferAccept: true,
|
||||
FastOpen: true,
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package reuseport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var listenConfig = net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) (err error) {
|
||||
return c.Control(func(fd uintptr) {
|
||||
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
|
||||
if err == nil {
|
||||
err = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Listen returns a TCP listener with the SO_REUSEADDR and SO_REUSEPORT options set.
|
||||
func Listen(network, addr string) (net.Listener, error) {
|
||||
return listenConfig.Listen(context.Background(), network, addr)
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package reuseport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrNoReusePort is returned if the OS doesn't support SO_REUSEPORT.
|
||||
type ErrNoReusePort struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Error implements error interface.
|
||||
func (e *ErrNoReusePort) Error() string {
|
||||
return fmt.Sprintf("The OS doesn't support SO_REUSEPORT: %v", e.err)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package reuseport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var listenConfig = net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) (err error) {
|
||||
return c.Control(func(fd uintptr) {
|
||||
err = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
// Listen returns TCP listener with SO_REUSEADDR option set, SO_REUSEPORT is not supported on Windows, so it uses
|
||||
// SO_REUSEADDR as an alternative to achieve the same effect.
|
||||
func Listen(network, addr string) (net.Listener, error) {
|
||||
return listenConfig.Listen(context.Background(), network, addr)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//go:build !amd64 && !arm64 && !ppc64 && !ppc64le && !riscv64 && !s390x
|
||||
|
||||
package fasthttp
|
||||
|
||||
import "math"
|
||||
|
||||
func roundUpForSliceCap(n int) int {
|
||||
if n <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Above 100MB, we don't round up as the overhead is too large.
|
||||
if n > 100*1024*1024 {
|
||||
return n
|
||||
}
|
||||
|
||||
x := uint32(n - 1)
|
||||
x |= x >> 1
|
||||
x |= x >> 2
|
||||
x |= x >> 4
|
||||
x |= x >> 8
|
||||
x |= x >> 16
|
||||
|
||||
// Make sure we don't return 0 due to overflow, even on 32 bit systems
|
||||
if x >= uint32(math.MaxInt32) {
|
||||
return math.MaxInt32
|
||||
}
|
||||
|
||||
return int(x + 1)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//go:build amd64 || arm64 || ppc64 || ppc64le || riscv64 || s390x
|
||||
|
||||
package fasthttp
|
||||
|
||||
func roundUpForSliceCap(n int) int {
|
||||
if n <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Above 100MB, we don't round up as the overhead is too large.
|
||||
if n > 100*1024*1024 {
|
||||
return n
|
||||
}
|
||||
|
||||
x := uint64(n - 1)
|
||||
x |= x >> 1
|
||||
x |= x >> 2
|
||||
x |= x >> 4
|
||||
x |= x >> 8
|
||||
x |= x >> 16
|
||||
|
||||
return int(x + 1)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
//go:build go1.20
|
||||
|
||||
package fasthttp
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
func s2b(s string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(s), len(s))
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
//go:build !go1.20
|
||||
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// s2b converts string to a byte slice without memory allocation.
|
||||
//
|
||||
// Note it may break if string and/or slice header will change
|
||||
// in the future go versions.
|
||||
func s2b(s string) (b []byte) {
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh.Data = sh.Data
|
||||
bh.Cap = sh.Len
|
||||
bh.Len = sh.Len
|
||||
return b
|
||||
}
|
||||
+2972
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
// Package stackless provides functionality that may save stack space
|
||||
// for high number of concurrently running goroutines.
|
||||
package stackless
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package stackless
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// NewFunc returns stackless wrapper for the function f.
|
||||
//
|
||||
// Unlike f, the returned stackless wrapper doesn't use stack space
|
||||
// on the goroutine that calls it.
|
||||
// The wrapper may save a lot of stack space if the following conditions
|
||||
// are met:
|
||||
//
|
||||
// - f doesn't contain blocking calls on network, I/O or channels;
|
||||
// - f uses a lot of stack space;
|
||||
// - the wrapper is called from high number of concurrent goroutines.
|
||||
//
|
||||
// The stackless wrapper returns false if the call cannot be processed
|
||||
// at the moment due to high load.
|
||||
func NewFunc(f func(ctx any)) func(ctx any) bool {
|
||||
if f == nil {
|
||||
// developer sanity-check
|
||||
panic("BUG: f cannot be nil")
|
||||
}
|
||||
|
||||
funcWorkCh := make(chan *funcWork, runtime.GOMAXPROCS(-1)*2048)
|
||||
onceInit := func() {
|
||||
n := runtime.GOMAXPROCS(-1)
|
||||
for i := 0; i < n; i++ {
|
||||
go funcWorker(funcWorkCh, f)
|
||||
}
|
||||
}
|
||||
var once sync.Once
|
||||
|
||||
return func(ctx any) bool {
|
||||
once.Do(onceInit)
|
||||
fw := getFuncWork()
|
||||
fw.ctx = ctx
|
||||
|
||||
select {
|
||||
case funcWorkCh <- fw:
|
||||
default:
|
||||
putFuncWork(fw)
|
||||
return false
|
||||
}
|
||||
<-fw.done
|
||||
putFuncWork(fw)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func funcWorker(funcWorkCh <-chan *funcWork, f func(ctx any)) {
|
||||
for fw := range funcWorkCh {
|
||||
f(fw.ctx)
|
||||
fw.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func getFuncWork() *funcWork {
|
||||
v := funcWorkPool.Get()
|
||||
if v == nil {
|
||||
v = &funcWork{
|
||||
done: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
return v.(*funcWork)
|
||||
}
|
||||
|
||||
func putFuncWork(fw *funcWork) {
|
||||
fw.ctx = nil
|
||||
funcWorkPool.Put(fw)
|
||||
}
|
||||
|
||||
var funcWorkPool sync.Pool
|
||||
|
||||
type funcWork struct {
|
||||
ctx any
|
||||
done chan struct{}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package stackless
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// Writer is an interface stackless writer must conform to.
|
||||
//
|
||||
// The interface contains common subset for Writers from compress/* packages.
|
||||
type Writer interface {
|
||||
Write(p []byte) (int, error)
|
||||
Flush() error
|
||||
Close() error
|
||||
Reset(w io.Writer)
|
||||
}
|
||||
|
||||
// NewWriterFunc must return new writer that will be wrapped into
|
||||
// stackless writer.
|
||||
type NewWriterFunc func(w io.Writer) Writer
|
||||
|
||||
// NewWriter creates a stackless writer around a writer returned
|
||||
// from newWriter.
|
||||
//
|
||||
// The returned writer writes data to dstW.
|
||||
//
|
||||
// Writers that use a lot of stack space may be wrapped into stackless writer,
|
||||
// thus saving stack space for high number of concurrently running goroutines.
|
||||
func NewWriter(dstW io.Writer, newWriter NewWriterFunc) Writer {
|
||||
w := &writer{
|
||||
dstW: dstW,
|
||||
}
|
||||
w.zw = newWriter(&w.xw)
|
||||
return w
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
dstW io.Writer
|
||||
zw Writer
|
||||
xw xWriter
|
||||
|
||||
err error
|
||||
n int
|
||||
|
||||
p []byte
|
||||
op op
|
||||
}
|
||||
|
||||
type op int
|
||||
|
||||
const (
|
||||
opWrite op = iota
|
||||
opFlush
|
||||
opClose
|
||||
opReset
|
||||
)
|
||||
|
||||
func (w *writer) Write(p []byte) (int, error) {
|
||||
w.p = p
|
||||
err := w.do(opWrite)
|
||||
w.p = nil
|
||||
return w.n, err
|
||||
}
|
||||
|
||||
func (w *writer) Flush() error {
|
||||
return w.do(opFlush)
|
||||
}
|
||||
|
||||
func (w *writer) Close() error {
|
||||
return w.do(opClose)
|
||||
}
|
||||
|
||||
func (w *writer) Reset(dstW io.Writer) {
|
||||
w.xw.Reset()
|
||||
w.do(opReset) //nolint:errcheck
|
||||
w.dstW = dstW
|
||||
}
|
||||
|
||||
func (w *writer) do(op op) error {
|
||||
w.op = op
|
||||
if !stacklessWriterFunc(w) {
|
||||
return errHighLoad
|
||||
}
|
||||
err := w.err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if w.xw.bb != nil && len(w.xw.bb.B) > 0 {
|
||||
_, err = w.dstW.Write(w.xw.bb.B)
|
||||
}
|
||||
w.xw.Reset()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var errHighLoad = errors.New("cannot compress data due to high load")
|
||||
|
||||
var (
|
||||
stacklessWriterFuncOnce sync.Once
|
||||
stacklessWriterFuncFunc func(ctx any) bool
|
||||
)
|
||||
|
||||
func stacklessWriterFunc(ctx any) bool {
|
||||
stacklessWriterFuncOnce.Do(func() {
|
||||
stacklessWriterFuncFunc = NewFunc(writerFunc)
|
||||
})
|
||||
return stacklessWriterFuncFunc(ctx)
|
||||
}
|
||||
|
||||
func writerFunc(ctx any) {
|
||||
w := ctx.(*writer)
|
||||
switch w.op {
|
||||
case opWrite:
|
||||
w.n, w.err = w.zw.Write(w.p)
|
||||
case opFlush:
|
||||
w.err = w.zw.Flush()
|
||||
case opClose:
|
||||
w.err = w.zw.Close()
|
||||
case opReset:
|
||||
w.zw.Reset(&w.xw)
|
||||
w.err = nil
|
||||
default:
|
||||
panic(fmt.Sprintf("BUG: unexpected op: %d", w.op))
|
||||
}
|
||||
}
|
||||
|
||||
type xWriter struct {
|
||||
bb *bytebufferpool.ByteBuffer
|
||||
}
|
||||
|
||||
func (w *xWriter) Write(p []byte) (int, error) {
|
||||
if w.bb == nil {
|
||||
w.bb = bufferPool.Get()
|
||||
}
|
||||
return w.bb.Write(p)
|
||||
}
|
||||
|
||||
func (w *xWriter) Reset() {
|
||||
if w.bb != nil {
|
||||
bufferPool.Put(w.bb)
|
||||
w.bb = nil
|
||||
}
|
||||
}
|
||||
|
||||
var bufferPool bytebufferpool.Pool
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
statusMessageMin = 100
|
||||
statusMessageMax = 511
|
||||
)
|
||||
|
||||
// HTTP status codes were stolen from net/http.
|
||||
const (
|
||||
StatusContinue = 100 // RFC 7231, 6.2.1
|
||||
StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
|
||||
StatusProcessing = 102 // RFC 2518, 10.1
|
||||
StatusEarlyHints = 103 // RFC 8297
|
||||
|
||||
StatusOK = 200 // RFC 7231, 6.3.1
|
||||
StatusCreated = 201 // RFC 7231, 6.3.2
|
||||
StatusAccepted = 202 // RFC 7231, 6.3.3
|
||||
StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
|
||||
StatusNoContent = 204 // RFC 7231, 6.3.5
|
||||
StatusResetContent = 205 // RFC 7231, 6.3.6
|
||||
StatusPartialContent = 206 // RFC 7233, 4.1
|
||||
StatusMultiStatus = 207 // RFC 4918, 11.1
|
||||
StatusAlreadyReported = 208 // RFC 5842, 7.1
|
||||
StatusIMUsed = 226 // RFC 3229, 10.4.1
|
||||
|
||||
StatusMultipleChoices = 300 // RFC 7231, 6.4.1
|
||||
StatusMovedPermanently = 301 // RFC 7231, 6.4.2
|
||||
StatusFound = 302 // RFC 7231, 6.4.3
|
||||
StatusSeeOther = 303 // RFC 7231, 6.4.4
|
||||
StatusNotModified = 304 // RFC 7232, 4.1
|
||||
StatusUseProxy = 305 // RFC 7231, 6.4.5
|
||||
_ = 306 // RFC 7231, 6.4.6 (Unused)
|
||||
StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7
|
||||
StatusPermanentRedirect = 308 // RFC 7538, 3
|
||||
|
||||
StatusBadRequest = 400 // RFC 7231, 6.5.1
|
||||
StatusUnauthorized = 401 // RFC 7235, 3.1
|
||||
StatusPaymentRequired = 402 // RFC 7231, 6.5.2
|
||||
StatusForbidden = 403 // RFC 7231, 6.5.3
|
||||
StatusNotFound = 404 // RFC 7231, 6.5.4
|
||||
StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5
|
||||
StatusNotAcceptable = 406 // RFC 7231, 6.5.6
|
||||
StatusProxyAuthRequired = 407 // RFC 7235, 3.2
|
||||
StatusRequestTimeout = 408 // RFC 7231, 6.5.7
|
||||
StatusConflict = 409 // RFC 7231, 6.5.8
|
||||
StatusGone = 410 // RFC 7231, 6.5.9
|
||||
StatusLengthRequired = 411 // RFC 7231, 6.5.10
|
||||
StatusPreconditionFailed = 412 // RFC 7232, 4.2
|
||||
StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11
|
||||
StatusRequestURITooLong = 414 // RFC 7231, 6.5.12
|
||||
StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13
|
||||
StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4
|
||||
StatusExpectationFailed = 417 // RFC 7231, 6.5.14
|
||||
StatusTeapot = 418 // RFC 7168, 2.3.3
|
||||
StatusMisdirectedRequest = 421 // RFC 7540, 9.1.2
|
||||
StatusUnprocessableEntity = 422 // RFC 4918, 11.2
|
||||
StatusLocked = 423 // RFC 4918, 11.3
|
||||
StatusFailedDependency = 424 // RFC 4918, 11.4
|
||||
StatusUpgradeRequired = 426 // RFC 7231, 6.5.15
|
||||
StatusPreconditionRequired = 428 // RFC 6585, 3
|
||||
StatusTooManyRequests = 429 // RFC 6585, 4
|
||||
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
|
||||
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
|
||||
|
||||
StatusInternalServerError = 500 // RFC 7231, 6.6.1
|
||||
StatusNotImplemented = 501 // RFC 7231, 6.6.2
|
||||
StatusBadGateway = 502 // RFC 7231, 6.6.3
|
||||
StatusServiceUnavailable = 503 // RFC 7231, 6.6.4
|
||||
StatusGatewayTimeout = 504 // RFC 7231, 6.6.5
|
||||
StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6
|
||||
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
|
||||
StatusInsufficientStorage = 507 // RFC 4918, 11.5
|
||||
StatusLoopDetected = 508 // RFC 5842, 7.2
|
||||
StatusNotExtended = 510 // RFC 2774, 7
|
||||
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
|
||||
)
|
||||
|
||||
var (
|
||||
unknownStatusCode = "Unknown Status Code"
|
||||
|
||||
statusMessages = []string{
|
||||
StatusContinue: "Continue",
|
||||
StatusSwitchingProtocols: "Switching Protocols",
|
||||
StatusProcessing: "Processing",
|
||||
StatusEarlyHints: "Early Hints",
|
||||
|
||||
StatusOK: "OK",
|
||||
StatusCreated: "Created",
|
||||
StatusAccepted: "Accepted",
|
||||
StatusNonAuthoritativeInfo: "Non-Authoritative Information",
|
||||
StatusNoContent: "No Content",
|
||||
StatusResetContent: "Reset Content",
|
||||
StatusPartialContent: "Partial Content",
|
||||
StatusMultiStatus: "Multi-Status",
|
||||
StatusAlreadyReported: "Already Reported",
|
||||
StatusIMUsed: "IM Used",
|
||||
|
||||
StatusMultipleChoices: "Multiple Choices",
|
||||
StatusMovedPermanently: "Moved Permanently",
|
||||
StatusFound: "Found",
|
||||
StatusSeeOther: "See Other",
|
||||
StatusNotModified: "Not Modified",
|
||||
StatusUseProxy: "Use Proxy",
|
||||
StatusTemporaryRedirect: "Temporary Redirect",
|
||||
StatusPermanentRedirect: "Permanent Redirect",
|
||||
|
||||
StatusBadRequest: "Bad Request",
|
||||
StatusUnauthorized: "Unauthorized",
|
||||
StatusPaymentRequired: "Payment Required",
|
||||
StatusForbidden: "Forbidden",
|
||||
StatusNotFound: "Not Found",
|
||||
StatusMethodNotAllowed: "Method Not Allowed",
|
||||
StatusNotAcceptable: "Not Acceptable",
|
||||
StatusProxyAuthRequired: "Proxy Authentication Required",
|
||||
StatusRequestTimeout: "Request Timeout",
|
||||
StatusConflict: "Conflict",
|
||||
StatusGone: "Gone",
|
||||
StatusLengthRequired: "Length Required",
|
||||
StatusPreconditionFailed: "Precondition Failed",
|
||||
StatusRequestEntityTooLarge: "Request Entity Too Large",
|
||||
StatusRequestURITooLong: "Request URI Too Long",
|
||||
StatusUnsupportedMediaType: "Unsupported Media Type",
|
||||
StatusRequestedRangeNotSatisfiable: "Requested Range Not Satisfiable",
|
||||
StatusExpectationFailed: "Expectation Failed",
|
||||
StatusTeapot: "I'm a teapot",
|
||||
StatusMisdirectedRequest: "Misdirected Request",
|
||||
StatusUnprocessableEntity: "Unprocessable Entity",
|
||||
StatusLocked: "Locked",
|
||||
StatusFailedDependency: "Failed Dependency",
|
||||
StatusUpgradeRequired: "Upgrade Required",
|
||||
StatusPreconditionRequired: "Precondition Required",
|
||||
StatusTooManyRequests: "Too Many Requests",
|
||||
StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large",
|
||||
StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons",
|
||||
|
||||
StatusInternalServerError: "Internal Server Error",
|
||||
StatusNotImplemented: "Not Implemented",
|
||||
StatusBadGateway: "Bad Gateway",
|
||||
StatusServiceUnavailable: "Service Unavailable",
|
||||
StatusGatewayTimeout: "Gateway Timeout",
|
||||
StatusHTTPVersionNotSupported: "HTTP Version Not Supported",
|
||||
StatusVariantAlsoNegotiates: "Variant Also Negotiates",
|
||||
StatusInsufficientStorage: "Insufficient Storage",
|
||||
StatusLoopDetected: "Loop Detected",
|
||||
StatusNotExtended: "Not Extended",
|
||||
StatusNetworkAuthenticationRequired: "Network Authentication Required",
|
||||
}
|
||||
)
|
||||
|
||||
// StatusMessage returns HTTP status message for the given status code.
|
||||
func StatusMessage(statusCode int) string {
|
||||
if statusCode < statusMessageMin || statusCode > statusMessageMax {
|
||||
return unknownStatusCode
|
||||
}
|
||||
|
||||
if s := statusMessages[statusCode]; s != "" {
|
||||
return s
|
||||
}
|
||||
return unknownStatusCode
|
||||
}
|
||||
|
||||
func formatStatusLine(dst, protocol []byte, statusCode int, statusText []byte) []byte {
|
||||
dst = append(dst, protocol...)
|
||||
dst = append(dst, ' ')
|
||||
dst = strconv.AppendInt(dst, int64(statusCode), 10)
|
||||
dst = append(dst, ' ')
|
||||
if len(statusText) == 0 {
|
||||
dst = append(dst, s2b(StatusMessage(statusCode))...)
|
||||
} else {
|
||||
dst = append(dst, statusText...)
|
||||
}
|
||||
return append(dst, strCRLF...)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/fasthttp/fasthttputil"
|
||||
)
|
||||
|
||||
// StreamWriter must write data to w.
|
||||
//
|
||||
// Usually StreamWriter writes data to w in a loop (aka 'data streaming').
|
||||
//
|
||||
// StreamWriter must return immediately if w returns error.
|
||||
//
|
||||
// Since the written data is buffered, do not forget calling w.Flush
|
||||
// when the data must be propagated to reader.
|
||||
type StreamWriter func(w *bufio.Writer)
|
||||
|
||||
// NewStreamReader returns a reader, which replays all the data generated by sw.
|
||||
//
|
||||
// The returned reader may be passed to Response.SetBodyStream.
|
||||
//
|
||||
// Close must be called on the returned reader after all the required data
|
||||
// has been read. Otherwise goroutine leak may occur.
|
||||
//
|
||||
// See also Response.SetBodyStreamWriter.
|
||||
func NewStreamReader(sw StreamWriter) io.ReadCloser {
|
||||
pc := fasthttputil.NewPipeConns()
|
||||
pw := pc.Conn1()
|
||||
pr := pc.Conn2()
|
||||
|
||||
var bw *bufio.Writer
|
||||
v := streamWriterBufPool.Get()
|
||||
if v == nil {
|
||||
bw = bufio.NewWriter(pw)
|
||||
} else {
|
||||
bw = v.(*bufio.Writer)
|
||||
bw.Reset(pw)
|
||||
}
|
||||
|
||||
go func() {
|
||||
sw(bw)
|
||||
bw.Flush()
|
||||
pw.Close()
|
||||
|
||||
streamWriterBufPool.Put(bw)
|
||||
}()
|
||||
|
||||
return pr
|
||||
}
|
||||
|
||||
var streamWriterBufPool sync.Pool
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
type headerInterface interface {
|
||||
ContentLength() int
|
||||
ReadTrailer(r *bufio.Reader) error
|
||||
}
|
||||
|
||||
type requestStream struct {
|
||||
header headerInterface
|
||||
prefetchedBytes *bytes.Reader
|
||||
reader *bufio.Reader
|
||||
totalBytesRead int
|
||||
chunkLeft int
|
||||
}
|
||||
|
||||
func (rs *requestStream) Read(p []byte) (int, error) {
|
||||
var (
|
||||
n int
|
||||
err error
|
||||
)
|
||||
if rs.header.ContentLength() == -1 {
|
||||
if rs.chunkLeft == 0 {
|
||||
chunkSize, err := parseChunkSize(rs.reader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if chunkSize == 0 {
|
||||
err = rs.header.ReadTrailer(rs.reader)
|
||||
if err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
rs.chunkLeft = chunkSize
|
||||
}
|
||||
bytesToRead := len(p)
|
||||
if rs.chunkLeft < len(p) {
|
||||
bytesToRead = rs.chunkLeft
|
||||
}
|
||||
n, err = rs.reader.Read(p[:bytesToRead])
|
||||
rs.totalBytesRead += n
|
||||
rs.chunkLeft -= n
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
if err == nil && rs.chunkLeft == 0 {
|
||||
err = readCrLf(rs.reader)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
if rs.totalBytesRead == rs.header.ContentLength() {
|
||||
return 0, io.EOF
|
||||
}
|
||||
prefetchedSize := int(rs.prefetchedBytes.Size())
|
||||
if prefetchedSize > rs.totalBytesRead {
|
||||
left := prefetchedSize - rs.totalBytesRead
|
||||
if len(p) > left {
|
||||
p = p[:left]
|
||||
}
|
||||
n, err := rs.prefetchedBytes.Read(p)
|
||||
rs.totalBytesRead += n
|
||||
if n == rs.header.ContentLength() {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
left := rs.header.ContentLength() - rs.totalBytesRead
|
||||
if len(p) > left {
|
||||
p = p[:left]
|
||||
}
|
||||
n, err = rs.reader.Read(p)
|
||||
rs.totalBytesRead += n
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
if rs.totalBytesRead == rs.header.ContentLength() {
|
||||
err = io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func acquireRequestStream(b *bytebufferpool.ByteBuffer, r *bufio.Reader, h headerInterface) *requestStream {
|
||||
rs := requestStreamPool.Get().(*requestStream)
|
||||
rs.prefetchedBytes = bytes.NewReader(b.B)
|
||||
rs.reader = r
|
||||
rs.header = h
|
||||
return rs
|
||||
}
|
||||
|
||||
func releaseRequestStream(rs *requestStream) {
|
||||
rs.prefetchedBytes = nil
|
||||
rs.totalBytesRead = 0
|
||||
rs.chunkLeft = 0
|
||||
rs.reader = nil
|
||||
rs.header = nil
|
||||
requestStreamPool.Put(rs)
|
||||
}
|
||||
|
||||
var requestStreamPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &requestStream{}
|
||||
},
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package fasthttp
|
||||
|
||||
var (
|
||||
defaultServerName = "fasthttp"
|
||||
defaultUserAgent = "fasthttp"
|
||||
defaultContentType = []byte("text/plain; charset=utf-8")
|
||||
)
|
||||
|
||||
var (
|
||||
strSlash = []byte("/")
|
||||
strSlashSlash = []byte("//")
|
||||
strSlashDotDot = []byte("/..")
|
||||
strSlashDotSlash = []byte("/./")
|
||||
strSlashDotDotSlash = []byte("/../")
|
||||
strBackSlashDotDot = []byte(`\..`)
|
||||
strBackSlashDotBackSlash = []byte(`\.\`)
|
||||
strSlashDotDotBackSlash = []byte(`/..\`)
|
||||
strBackSlashDotDotBackSlash = []byte(`\..\`)
|
||||
strCRLF = []byte("\r\n")
|
||||
strHTTP = []byte("http")
|
||||
strHTTPS = []byte("https")
|
||||
strHTTP11 = []byte("HTTP/1.1")
|
||||
strColon = []byte(":")
|
||||
strColonSlashSlash = []byte("://")
|
||||
strColonSpace = []byte(": ")
|
||||
strCommaSpace = []byte(", ")
|
||||
strGMT = []byte("GMT")
|
||||
|
||||
strResponseContinue = []byte("HTTP/1.1 100 Continue\r\n\r\n")
|
||||
|
||||
strExpect = []byte(HeaderExpect)
|
||||
strConnection = []byte(HeaderConnection)
|
||||
strContentLength = []byte(HeaderContentLength)
|
||||
strContentType = []byte(HeaderContentType)
|
||||
strDate = []byte(HeaderDate)
|
||||
strHost = []byte(HeaderHost)
|
||||
strReferer = []byte(HeaderReferer)
|
||||
strServer = []byte(HeaderServer)
|
||||
strTransferEncoding = []byte(HeaderTransferEncoding)
|
||||
strContentEncoding = []byte(HeaderContentEncoding)
|
||||
strAcceptEncoding = []byte(HeaderAcceptEncoding)
|
||||
strUserAgent = []byte(HeaderUserAgent)
|
||||
strCookie = []byte(HeaderCookie)
|
||||
strSetCookie = []byte(HeaderSetCookie)
|
||||
strLocation = []byte(HeaderLocation)
|
||||
strIfModifiedSince = []byte(HeaderIfModifiedSince)
|
||||
strLastModified = []byte(HeaderLastModified)
|
||||
strAcceptRanges = []byte(HeaderAcceptRanges)
|
||||
strRange = []byte(HeaderRange)
|
||||
strContentRange = []byte(HeaderContentRange)
|
||||
strAuthorization = []byte(HeaderAuthorization)
|
||||
strTE = []byte(HeaderTE)
|
||||
strTrailer = []byte(HeaderTrailer)
|
||||
strMaxForwards = []byte(HeaderMaxForwards)
|
||||
strProxyConnection = []byte(HeaderProxyConnection)
|
||||
strProxyAuthenticate = []byte(HeaderProxyAuthenticate)
|
||||
strProxyAuthorization = []byte(HeaderProxyAuthorization)
|
||||
strWWWAuthenticate = []byte(HeaderWWWAuthenticate)
|
||||
strVary = []byte(HeaderVary)
|
||||
|
||||
strCookieExpires = []byte("expires")
|
||||
strCookieDomain = []byte("domain")
|
||||
strCookiePath = []byte("path")
|
||||
strCookieHTTPOnly = []byte("HttpOnly")
|
||||
strCookieSecure = []byte("secure")
|
||||
strCookiePartitioned = []byte("Partitioned")
|
||||
strCookieMaxAge = []byte("max-age")
|
||||
strCookieSameSite = []byte("SameSite")
|
||||
strCookieSameSiteLax = []byte("Lax")
|
||||
strCookieSameSiteStrict = []byte("Strict")
|
||||
strCookieSameSiteNone = []byte("None")
|
||||
|
||||
strClose = []byte("close")
|
||||
strGzip = []byte("gzip")
|
||||
strBr = []byte("br")
|
||||
strZstd = []byte("zstd")
|
||||
strDeflate = []byte("deflate")
|
||||
strKeepAlive = []byte("keep-alive")
|
||||
strUpgrade = []byte("Upgrade")
|
||||
strChunked = []byte("chunked")
|
||||
strIdentity = []byte("identity")
|
||||
str100Continue = []byte("100-continue")
|
||||
strPostArgsContentType = []byte("application/x-www-form-urlencoded")
|
||||
strDefaultContentType = []byte("application/octet-stream")
|
||||
strMultipartFormData = []byte("multipart/form-data")
|
||||
strBoundary = []byte("boundary")
|
||||
strBytes = []byte("bytes")
|
||||
strBasicSpace = []byte("Basic ")
|
||||
|
||||
strApplicationSlash = []byte("application/")
|
||||
strImageSVG = []byte("image/svg")
|
||||
strImageIcon = []byte("image/x-icon")
|
||||
strFontSlash = []byte("font/")
|
||||
strMultipartSlash = []byte("multipart/")
|
||||
strTextSlash = []byte("text/")
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func isConnectionReset(err error) bool {
|
||||
return errors.Is(err, syscall.ECONNRESET)
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func isConnectionReset(err error) bool {
|
||||
return errors.Is(err, syscall.WSAECONNRESET)
|
||||
}
|
||||
+496
@@ -0,0 +1,496 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Dial dials the given TCP addr using tcp4.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
// - It returns ErrDialTimeout if connection cannot be established during
|
||||
// DefaultDialTimeout seconds. Use DialTimeout for customizing dial timeout.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.Dial or HostClient.Dial.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func Dial(addr string) (net.Conn, error) {
|
||||
return defaultDialer.Dial(addr)
|
||||
}
|
||||
|
||||
// DialTimeout dials the given TCP addr using tcp4 using the given timeout.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.DialTimeout or HostClient.DialTimeout.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return defaultDialer.DialTimeout(addr, timeout)
|
||||
}
|
||||
|
||||
// DialDualStack dials the given TCP addr using both tcp4 and tcp6.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
// - It returns ErrDialTimeout if connection cannot be established during
|
||||
// DefaultDialTimeout seconds. Use DialDualStackTimeout for custom dial
|
||||
// timeout.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.Dial or HostClient.Dial.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func DialDualStack(addr string) (net.Conn, error) {
|
||||
return defaultDialer.DialDualStack(addr)
|
||||
}
|
||||
|
||||
// DialDualStackTimeout dials the given TCP addr using both tcp4 and tcp6
|
||||
// using the given timeout.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.DialTimeout or HostClient.DialTimeout.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return defaultDialer.DialDualStackTimeout(addr, timeout)
|
||||
}
|
||||
|
||||
var defaultDialer = &TCPDialer{Concurrency: 1000}
|
||||
|
||||
// Resolver represents interface of the tcp resolver.
|
||||
type Resolver interface {
|
||||
LookupIPAddr(context.Context, string) (names []net.IPAddr, err error)
|
||||
}
|
||||
|
||||
// TCPDialer contains options to control a group of Dial calls.
|
||||
type TCPDialer struct {
|
||||
// Concurrency controls the maximum number of concurrent Dials
|
||||
// that can be performed using this object.
|
||||
// Setting this to 0 means unlimited.
|
||||
//
|
||||
// WARNING: This can only be changed before the first Dial.
|
||||
// Changes made after the first Dial will not affect anything.
|
||||
Concurrency int
|
||||
|
||||
// LocalAddr is the local address to use when dialing an
|
||||
// address.
|
||||
// If nil, a local address is automatically chosen.
|
||||
LocalAddr *net.TCPAddr
|
||||
|
||||
// This may be used to override DNS resolving policy, like this:
|
||||
// var dialer = &fasthttp.TCPDialer{
|
||||
// Resolver: &net.Resolver{
|
||||
// PreferGo: true,
|
||||
// StrictErrors: false,
|
||||
// Dial: func (ctx context.Context, network, address string) (net.Conn, error) {
|
||||
// d := net.Dialer{}
|
||||
// return d.DialContext(ctx, "udp", "8.8.8.8:53")
|
||||
// },
|
||||
// },
|
||||
// }
|
||||
Resolver Resolver
|
||||
|
||||
// DisableDNSResolution may be used to disable DNS resolution
|
||||
DisableDNSResolution bool
|
||||
// DNSCacheDuration may be used to override the default DNS cache duration (DefaultDNSCacheDuration)
|
||||
DNSCacheDuration time.Duration
|
||||
|
||||
tcpAddrsMap sync.Map
|
||||
|
||||
concurrencyCh chan struct{}
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// Dial dials the given TCP addr using tcp4.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
// - It returns ErrDialTimeout if connection cannot be established during
|
||||
// DefaultDialTimeout seconds. Use DialTimeout for customizing dial timeout.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.Dial or HostClient.Dial.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func (d *TCPDialer) Dial(addr string) (net.Conn, error) {
|
||||
return d.dial(addr, false, DefaultDialTimeout)
|
||||
}
|
||||
|
||||
// DialTimeout dials the given TCP addr using tcp4 using the given timeout.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.DialTimeout or HostClient.DialTimeout.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func (d *TCPDialer) DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return d.dial(addr, false, timeout)
|
||||
}
|
||||
|
||||
// DialDualStack dials the given TCP addr using both tcp4 and tcp6.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
// - It returns ErrDialTimeout if connection cannot be established during
|
||||
// DefaultDialTimeout seconds. Use DialDualStackTimeout for custom dial
|
||||
// timeout.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.Dial or HostClient.Dial.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func (d *TCPDialer) DialDualStack(addr string) (net.Conn, error) {
|
||||
return d.dial(addr, true, DefaultDialTimeout)
|
||||
}
|
||||
|
||||
// DialDualStackTimeout dials the given TCP addr using both tcp4 and tcp6
|
||||
// using the given timeout.
|
||||
//
|
||||
// This function has the following additional features comparing to net.Dial:
|
||||
//
|
||||
// - It reduces load on DNS resolver by caching resolved TCP addressed
|
||||
// for DNSCacheDuration.
|
||||
// - It dials all the resolved TCP addresses in round-robin manner until
|
||||
// connection is established. This may be useful if certain addresses
|
||||
// are temporarily unreachable.
|
||||
//
|
||||
// This dialer is intended for custom code wrapping before passing
|
||||
// to Client.DialTimeout or HostClient.DialTimeout.
|
||||
//
|
||||
// For instance, per-host counters and/or limits may be implemented
|
||||
// by such wrappers.
|
||||
//
|
||||
// The addr passed to the function must contain port. Example addr values:
|
||||
//
|
||||
// - foobar.baz:443
|
||||
// - foo.bar:80
|
||||
// - aaa.com:8080
|
||||
func (d *TCPDialer) DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return d.dial(addr, true, timeout)
|
||||
}
|
||||
|
||||
func (d *TCPDialer) dial(addr string, dualStack bool, timeout time.Duration) (net.Conn, error) {
|
||||
d.once.Do(func() {
|
||||
if d.Concurrency > 0 {
|
||||
d.concurrencyCh = make(chan struct{}, d.Concurrency)
|
||||
}
|
||||
|
||||
if d.DNSCacheDuration == 0 {
|
||||
d.DNSCacheDuration = DefaultDNSCacheDuration
|
||||
}
|
||||
|
||||
if !d.DisableDNSResolution {
|
||||
go d.tcpAddrsClean()
|
||||
}
|
||||
})
|
||||
deadline := time.Now().Add(timeout)
|
||||
network := "tcp4"
|
||||
if dualStack {
|
||||
network = "tcp"
|
||||
}
|
||||
if d.DisableDNSResolution {
|
||||
return d.tryDial(network, addr, deadline, d.concurrencyCh)
|
||||
}
|
||||
addrs, idx, err := d.getTCPAddrs(addr, dualStack, deadline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var conn net.Conn
|
||||
n := uint32(len(addrs))
|
||||
for n > 0 {
|
||||
conn, err = d.tryDial(network, addrs[idx%n].String(), deadline, d.concurrencyCh)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
if errors.Is(err, ErrDialTimeout) {
|
||||
return nil, err
|
||||
}
|
||||
idx++
|
||||
n--
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (d *TCPDialer) tryDial(
|
||||
network string, addr string, deadline time.Time, concurrencyCh chan struct{},
|
||||
) (net.Conn, error) {
|
||||
timeout := time.Until(deadline)
|
||||
if timeout <= 0 {
|
||||
return nil, wrapDialWithUpstream(ErrDialTimeout, addr)
|
||||
}
|
||||
|
||||
if concurrencyCh != nil {
|
||||
select {
|
||||
case concurrencyCh <- struct{}{}:
|
||||
default:
|
||||
tc := AcquireTimer(timeout)
|
||||
isTimeout := false
|
||||
select {
|
||||
case concurrencyCh <- struct{}{}:
|
||||
case <-tc.C:
|
||||
isTimeout = true
|
||||
}
|
||||
ReleaseTimer(tc)
|
||||
if isTimeout {
|
||||
return nil, wrapDialWithUpstream(ErrDialTimeout, addr)
|
||||
}
|
||||
}
|
||||
defer func() { <-concurrencyCh }()
|
||||
}
|
||||
|
||||
dialer := net.Dialer{}
|
||||
if d.LocalAddr != nil {
|
||||
dialer.LocalAddr = d.LocalAddr
|
||||
}
|
||||
|
||||
ctx, cancelCtx := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancelCtx()
|
||||
conn, err := dialer.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return nil, wrapDialWithUpstream(ErrDialTimeout, addr)
|
||||
}
|
||||
return nil, wrapDialWithUpstream(err, addr)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
// ErrDialTimeout is returned when TCP dialing is timed out.
|
||||
var ErrDialTimeout = errors.New("dialing to the given TCP address timed out")
|
||||
|
||||
// ErrDialWithUpstream wraps dial error with upstream info.
|
||||
//
|
||||
// Should use errors.As to get upstream information from error:
|
||||
//
|
||||
// hc := fasthttp.HostClient{Addr: "foo.com,bar.com"}
|
||||
// err := hc.Do(req, res)
|
||||
//
|
||||
// var dialErr *fasthttp.ErrDialWithUpstream
|
||||
// if errors.As(err, &dialErr) {
|
||||
// upstream = dialErr.Upstream // 34.206.39.153:80
|
||||
// }
|
||||
type ErrDialWithUpstream struct {
|
||||
Upstream string
|
||||
wrapErr error
|
||||
}
|
||||
|
||||
func (e *ErrDialWithUpstream) Error() string {
|
||||
return fmt.Sprintf("error when dialing %s: %s", e.Upstream, e.wrapErr.Error())
|
||||
}
|
||||
|
||||
func (e *ErrDialWithUpstream) Unwrap() error {
|
||||
return e.wrapErr
|
||||
}
|
||||
|
||||
func wrapDialWithUpstream(err error, upstream string) error {
|
||||
return &ErrDialWithUpstream{
|
||||
Upstream: upstream,
|
||||
wrapErr: err,
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultDialTimeout is timeout used by Dial and DialDualStack
|
||||
// for establishing TCP connections.
|
||||
const DefaultDialTimeout = 3 * time.Second
|
||||
|
||||
type tcpAddrEntry struct {
|
||||
addrs []net.TCPAddr
|
||||
addrsIdx uint32
|
||||
|
||||
pending int32
|
||||
resolveTime time.Time
|
||||
}
|
||||
|
||||
// DefaultDNSCacheDuration is the duration for caching resolved TCP addresses
|
||||
// by Dial* functions.
|
||||
const DefaultDNSCacheDuration = time.Minute
|
||||
|
||||
func (d *TCPDialer) tcpAddrsClean() {
|
||||
expireDuration := 2 * d.DNSCacheDuration
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
t := time.Now()
|
||||
d.tcpAddrsMap.Range(func(k, v any) bool {
|
||||
if e, ok := v.(*tcpAddrEntry); ok && t.Sub(e.resolveTime) > expireDuration {
|
||||
d.tcpAddrsMap.Delete(k)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (d *TCPDialer) getTCPAddrs(addr string, dualStack bool, deadline time.Time) ([]net.TCPAddr, uint32, error) {
|
||||
item, exist := d.tcpAddrsMap.Load(addr)
|
||||
e, ok := item.(*tcpAddrEntry)
|
||||
if exist && ok && e != nil && time.Since(e.resolveTime) > d.DNSCacheDuration {
|
||||
// Only let one goroutine re-resolve at a time.
|
||||
if atomic.SwapInt32(&e.pending, 1) == 0 {
|
||||
e = nil
|
||||
}
|
||||
}
|
||||
|
||||
if e == nil {
|
||||
addrs, err := resolveTCPAddrs(addr, dualStack, d.Resolver, deadline)
|
||||
if err != nil {
|
||||
item, exist := d.tcpAddrsMap.Load(addr)
|
||||
e, ok = item.(*tcpAddrEntry)
|
||||
if exist && ok && e != nil {
|
||||
// Set pending to 0 so another goroutine can retry.
|
||||
atomic.StoreInt32(&e.pending, 0)
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
e = &tcpAddrEntry{
|
||||
addrs: addrs,
|
||||
resolveTime: time.Now(),
|
||||
}
|
||||
d.tcpAddrsMap.Store(addr, e)
|
||||
}
|
||||
|
||||
idx := atomic.AddUint32(&e.addrsIdx, 1)
|
||||
return e.addrs, idx, nil
|
||||
}
|
||||
|
||||
func resolveTCPAddrs(addr string, dualStack bool, resolver Resolver, deadline time.Time) ([]net.TCPAddr, error) {
|
||||
host, portS, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
port, err := strconv.Atoi(portS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resolver == nil {
|
||||
resolver = net.DefaultResolver
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithDeadline(context.Background(), deadline)
|
||||
defer cancel()
|
||||
ipaddrs, err := resolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := len(ipaddrs)
|
||||
addrs := make([]net.TCPAddr, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ip := ipaddrs[i]
|
||||
if !dualStack && ip.IP.To4() == nil {
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, net.TCPAddr{
|
||||
IP: ip.IP,
|
||||
Port: port,
|
||||
Zone: ip.Zone,
|
||||
})
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
return nil, errNoDNSEntries
|
||||
}
|
||||
return addrs, nil
|
||||
}
|
||||
|
||||
var errNoDNSEntries = errors.New("couldn't find DNS entries for the given domain. Try using DialDualStack")
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func initTimer(t *time.Timer, timeout time.Duration) *time.Timer {
|
||||
if t == nil {
|
||||
return time.NewTimer(timeout)
|
||||
}
|
||||
if t.Reset(timeout) {
|
||||
// developer sanity-check
|
||||
panic("BUG: active timer trapped into initTimer()")
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func stopTimer(t *time.Timer) {
|
||||
if !t.Stop() {
|
||||
// Collect possibly added time from the channel
|
||||
// if timer has been stopped and nobody collected its value.
|
||||
select {
|
||||
case <-t.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AcquireTimer returns a time.Timer from the pool and updates it to
|
||||
// send the current time on its channel after at least timeout.
|
||||
//
|
||||
// The returned Timer may be returned to the pool with ReleaseTimer
|
||||
// when no longer needed. This allows reducing GC load.
|
||||
func AcquireTimer(timeout time.Duration) *time.Timer {
|
||||
v := timerPool.Get()
|
||||
if v == nil {
|
||||
return time.NewTimer(timeout)
|
||||
}
|
||||
t := v.(*time.Timer)
|
||||
initTimer(t, timeout)
|
||||
return t
|
||||
}
|
||||
|
||||
// ReleaseTimer returns the time.Timer acquired via AcquireTimer to the pool
|
||||
// and prevents the Timer from firing.
|
||||
//
|
||||
// Do not access the released time.Timer or read from its channel otherwise
|
||||
// data races may occur.
|
||||
func ReleaseTimer(t *time.Timer) {
|
||||
stopTimer(t)
|
||||
timerPool.Put(t)
|
||||
}
|
||||
|
||||
var timerPool sync.Pool
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GenerateTestCertificate generates a test certificate and private key based on the given host.
|
||||
func GenerateTestCertificate(host string) ([]byte, []byte, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
cert := &x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"fasthttp test"},
|
||||
},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
DNSNames: []string{host},
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
certBytes, err := x509.CreateCertificate(
|
||||
rand.Reader, cert, cert, &priv.PublicKey, priv,
|
||||
)
|
||||
|
||||
p := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: x509.MarshalPKCS1PrivateKey(priv),
|
||||
},
|
||||
)
|
||||
|
||||
b := pem.EncodeToMemory(
|
||||
&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certBytes,
|
||||
},
|
||||
)
|
||||
|
||||
return b, p, err
|
||||
}
|
||||
+909
@@ -0,0 +1,909 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// AcquireURI returns an empty URI instance from the pool.
|
||||
//
|
||||
// Release the URI with ReleaseURI after the URI is no longer needed.
|
||||
// This allows reducing GC load.
|
||||
func AcquireURI() *URI {
|
||||
return uriPool.Get().(*URI)
|
||||
}
|
||||
|
||||
// ReleaseURI releases the URI acquired via AcquireURI.
|
||||
//
|
||||
// The released URI mustn't be used after releasing it, otherwise data races
|
||||
// may occur.
|
||||
func ReleaseURI(u *URI) {
|
||||
u.Reset()
|
||||
uriPool.Put(u)
|
||||
}
|
||||
|
||||
var uriPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return &URI{}
|
||||
},
|
||||
}
|
||||
|
||||
// URI represents URI :) .
|
||||
//
|
||||
// It is forbidden copying URI instances. Create new instance and use CopyTo
|
||||
// instead.
|
||||
//
|
||||
// URI instance MUST NOT be used from concurrently running goroutines.
|
||||
type URI struct {
|
||||
noCopy noCopy
|
||||
|
||||
pathOriginal []byte
|
||||
scheme []byte
|
||||
path []byte
|
||||
queryString []byte
|
||||
hash []byte
|
||||
host []byte
|
||||
|
||||
queryArgs Args
|
||||
parsedQueryArgs bool
|
||||
|
||||
// Path values are sent as-is without normalization.
|
||||
//
|
||||
// Disabled path normalization may be useful for proxying incoming requests
|
||||
// to servers that are expecting paths to be forwarded as-is.
|
||||
//
|
||||
// By default path values are normalized, i.e.
|
||||
// extra slashes are removed, special characters are encoded.
|
||||
DisablePathNormalizing bool
|
||||
|
||||
fullURI []byte
|
||||
requestURI []byte
|
||||
|
||||
username []byte
|
||||
password []byte
|
||||
}
|
||||
|
||||
// CopyTo copies uri contents to dst.
|
||||
func (u *URI) CopyTo(dst *URI) {
|
||||
dst.Reset()
|
||||
dst.pathOriginal = append(dst.pathOriginal, u.pathOriginal...)
|
||||
dst.scheme = append(dst.scheme, u.scheme...)
|
||||
dst.path = append(dst.path, u.path...)
|
||||
dst.queryString = append(dst.queryString, u.queryString...)
|
||||
dst.hash = append(dst.hash, u.hash...)
|
||||
dst.host = append(dst.host, u.host...)
|
||||
dst.username = append(dst.username, u.username...)
|
||||
dst.password = append(dst.password, u.password...)
|
||||
|
||||
u.queryArgs.CopyTo(&dst.queryArgs)
|
||||
dst.parsedQueryArgs = u.parsedQueryArgs
|
||||
dst.DisablePathNormalizing = u.DisablePathNormalizing
|
||||
|
||||
// fullURI and requestURI shouldn't be copied, since they are created
|
||||
// from scratch on each FullURI() and RequestURI() call.
|
||||
}
|
||||
|
||||
// Hash returns URI hash, i.e. qwe of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Hash() []byte {
|
||||
return u.hash
|
||||
}
|
||||
|
||||
// SetHash sets URI hash.
|
||||
func (u *URI) SetHash(hash string) {
|
||||
u.hash = append(u.hash[:0], hash...)
|
||||
}
|
||||
|
||||
// SetHashBytes sets URI hash.
|
||||
func (u *URI) SetHashBytes(hash []byte) {
|
||||
u.hash = append(u.hash[:0], hash...)
|
||||
}
|
||||
|
||||
// Username returns URI username
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Username() []byte {
|
||||
return u.username
|
||||
}
|
||||
|
||||
// SetUsername sets URI username.
|
||||
func (u *URI) SetUsername(username string) {
|
||||
u.username = append(u.username[:0], username...)
|
||||
}
|
||||
|
||||
// SetUsernameBytes sets URI username.
|
||||
func (u *URI) SetUsernameBytes(username []byte) {
|
||||
u.username = append(u.username[:0], username...)
|
||||
}
|
||||
|
||||
// Password returns URI password.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Password() []byte {
|
||||
return u.password
|
||||
}
|
||||
|
||||
// SetPassword sets URI password.
|
||||
func (u *URI) SetPassword(password string) {
|
||||
u.password = append(u.password[:0], password...)
|
||||
}
|
||||
|
||||
// SetPasswordBytes sets URI password.
|
||||
func (u *URI) SetPasswordBytes(password []byte) {
|
||||
u.password = append(u.password[:0], password...)
|
||||
}
|
||||
|
||||
// QueryString returns URI query string,
|
||||
// i.e. baz=123 of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) QueryString() []byte {
|
||||
return u.queryString
|
||||
}
|
||||
|
||||
// SetQueryString sets URI query string.
|
||||
func (u *URI) SetQueryString(queryString string) {
|
||||
u.queryString = append(u.queryString[:0], queryString...)
|
||||
u.parsedQueryArgs = false
|
||||
}
|
||||
|
||||
// SetQueryStringBytes sets URI query string.
|
||||
func (u *URI) SetQueryStringBytes(queryString []byte) {
|
||||
u.queryString = append(u.queryString[:0], queryString...)
|
||||
u.parsedQueryArgs = false
|
||||
}
|
||||
|
||||
// Path returns URI path, i.e. /foo/bar of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// The returned path is always urldecoded and normalized,
|
||||
// i.e. '//f%20obar/baz/../zzz' becomes '/f obar/zzz'.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Path() []byte {
|
||||
path := u.path
|
||||
if len(path) == 0 {
|
||||
path = strSlash
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// SetPath sets URI path.
|
||||
func (u *URI) SetPath(path string) {
|
||||
u.pathOriginal = append(u.pathOriginal[:0], path...)
|
||||
u.path = normalizePath(u.path, u.pathOriginal)
|
||||
}
|
||||
|
||||
// SetPathBytes sets URI path.
|
||||
func (u *URI) SetPathBytes(path []byte) {
|
||||
u.pathOriginal = append(u.pathOriginal[:0], path...)
|
||||
u.path = normalizePath(u.path, u.pathOriginal)
|
||||
}
|
||||
|
||||
// PathOriginal returns the original path from requestURI passed to URI.Parse().
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) PathOriginal() []byte {
|
||||
return u.pathOriginal
|
||||
}
|
||||
|
||||
// Scheme returns URI scheme, i.e. http of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// Returned scheme is always lowercased.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Scheme() []byte {
|
||||
scheme := u.scheme
|
||||
if len(scheme) == 0 {
|
||||
scheme = strHTTP
|
||||
}
|
||||
return scheme
|
||||
}
|
||||
|
||||
// SetScheme sets URI scheme, i.e. http, https, ftp, etc.
|
||||
func (u *URI) SetScheme(scheme string) {
|
||||
u.scheme = append(u.scheme[:0], scheme...)
|
||||
lowercaseBytes(u.scheme)
|
||||
}
|
||||
|
||||
// SetSchemeBytes sets URI scheme, i.e. http, https, ftp, etc.
|
||||
func (u *URI) SetSchemeBytes(scheme []byte) {
|
||||
u.scheme = append(u.scheme[:0], scheme...)
|
||||
lowercaseBytes(u.scheme)
|
||||
}
|
||||
|
||||
func (u *URI) isHTTPS() bool {
|
||||
return bytes.Equal(u.scheme, strHTTPS)
|
||||
}
|
||||
|
||||
func (u *URI) isHTTP() bool {
|
||||
return len(u.scheme) == 0 || bytes.Equal(u.scheme, strHTTP)
|
||||
}
|
||||
|
||||
// Reset clears uri.
|
||||
func (u *URI) Reset() {
|
||||
u.pathOriginal = u.pathOriginal[:0]
|
||||
u.scheme = u.scheme[:0]
|
||||
u.path = u.path[:0]
|
||||
u.queryString = u.queryString[:0]
|
||||
u.hash = u.hash[:0]
|
||||
u.username = u.username[:0]
|
||||
u.password = u.password[:0]
|
||||
|
||||
u.host = u.host[:0]
|
||||
u.queryArgs.Reset()
|
||||
u.parsedQueryArgs = false
|
||||
u.DisablePathNormalizing = false
|
||||
|
||||
// There is no need in u.fullURI = u.fullURI[:0], since full uri
|
||||
// is calculated on each call to FullURI().
|
||||
|
||||
// There is no need in u.requestURI = u.requestURI[:0], since requestURI
|
||||
// is calculated on each call to RequestURI().
|
||||
}
|
||||
|
||||
// Host returns host part, i.e. aaa.com of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// Host is always lowercased.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) Host() []byte {
|
||||
return u.host
|
||||
}
|
||||
|
||||
// SetHost sets host for the uri.
|
||||
func (u *URI) SetHost(host string) {
|
||||
u.host = append(u.host[:0], host...)
|
||||
lowercaseBytes(u.host)
|
||||
}
|
||||
|
||||
// SetHostBytes sets host for the uri.
|
||||
func (u *URI) SetHostBytes(host []byte) {
|
||||
u.host = append(u.host[:0], host...)
|
||||
lowercaseBytes(u.host)
|
||||
}
|
||||
|
||||
var ErrorInvalidURI = errors.New("invalid uri")
|
||||
|
||||
// Parse initializes URI from the given host and uri.
|
||||
//
|
||||
// host may be nil. In this case uri must contain fully qualified uri,
|
||||
// i.e. with scheme and host. http is assumed if scheme is omitted.
|
||||
//
|
||||
// uri may contain e.g. RequestURI without scheme and host if host is non-empty.
|
||||
func (u *URI) Parse(host, uri []byte) error {
|
||||
return u.parse(host, uri, false)
|
||||
}
|
||||
|
||||
func (u *URI) parse(host, uri []byte, isTLS bool) error {
|
||||
u.Reset()
|
||||
|
||||
if stringContainsCTLByte(uri) {
|
||||
return ErrorInvalidURI
|
||||
}
|
||||
|
||||
if len(host) == 0 || bytes.Contains(uri, strColonSlashSlash) {
|
||||
scheme, newHost, newURI := splitHostURI(host, uri)
|
||||
u.SetSchemeBytes(scheme)
|
||||
host = newHost
|
||||
uri = newURI
|
||||
}
|
||||
|
||||
if isTLS {
|
||||
u.SetSchemeBytes(strHTTPS)
|
||||
}
|
||||
|
||||
if n := bytes.IndexByte(host, '@'); n >= 0 {
|
||||
auth := host[:n]
|
||||
host = host[n+1:]
|
||||
|
||||
if n := bytes.IndexByte(auth, ':'); n >= 0 {
|
||||
u.username = append(u.username[:0], auth[:n]...)
|
||||
u.password = append(u.password[:0], auth[n+1:]...)
|
||||
} else {
|
||||
u.username = append(u.username[:0], auth...)
|
||||
u.password = u.password[:0]
|
||||
}
|
||||
}
|
||||
|
||||
u.host = append(u.host, host...)
|
||||
if parsedHost, err := parseHost(u.host); err != nil {
|
||||
return err
|
||||
} else {
|
||||
u.host = parsedHost
|
||||
}
|
||||
lowercaseBytes(u.host)
|
||||
|
||||
b := uri
|
||||
queryIndex := bytes.IndexByte(b, '?')
|
||||
fragmentIndex := bytes.IndexByte(b, '#')
|
||||
// Ignore query in fragment part
|
||||
if fragmentIndex >= 0 && queryIndex > fragmentIndex {
|
||||
queryIndex = -1
|
||||
}
|
||||
|
||||
if queryIndex < 0 && fragmentIndex < 0 {
|
||||
u.pathOriginal = append(u.pathOriginal, b...)
|
||||
u.path = normalizePath(u.path, u.pathOriginal)
|
||||
return nil
|
||||
}
|
||||
|
||||
if queryIndex >= 0 {
|
||||
// Path is everything up to the start of the query
|
||||
u.pathOriginal = append(u.pathOriginal, b[:queryIndex]...)
|
||||
u.path = normalizePath(u.path, u.pathOriginal)
|
||||
|
||||
if fragmentIndex < 0 {
|
||||
u.queryString = append(u.queryString, b[queryIndex+1:]...)
|
||||
} else {
|
||||
u.queryString = append(u.queryString, b[queryIndex+1:fragmentIndex]...)
|
||||
u.hash = append(u.hash, b[fragmentIndex+1:]...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fragmentIndex >= 0 && queryIndex < 0
|
||||
// Path is up to the start of fragment
|
||||
u.pathOriginal = append(u.pathOriginal, b[:fragmentIndex]...)
|
||||
u.path = normalizePath(u.path, u.pathOriginal)
|
||||
u.hash = append(u.hash, b[fragmentIndex+1:]...)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseHost parses host as an authority without user
|
||||
// information. That is, as host[:port].
|
||||
//
|
||||
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L619
|
||||
//
|
||||
// The host is parsed and unescaped in place overwriting the contents of the host parameter.
|
||||
func parseHost(host []byte) ([]byte, error) {
|
||||
if len(host) > 0 && host[0] == '[' {
|
||||
// Parse an IP-Literal in RFC 3986 and RFC 6874.
|
||||
// E.g., "[fe80::1]", "[fe80::1%25en0]", "[fe80::1]:80".
|
||||
i := bytes.LastIndexByte(host, ']')
|
||||
if i < 0 {
|
||||
return nil, errors.New("missing ']' in host")
|
||||
}
|
||||
colonPort := host[i+1:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
return nil, fmt.Errorf("invalid port %q after host", colonPort)
|
||||
}
|
||||
|
||||
// RFC 6874 defines that %25 (%-encoded percent) introduces
|
||||
// the zone identifier, and the zone identifier can use basically
|
||||
// any %-encoding it likes. That's different from the host, which
|
||||
// can only %-encode non-ASCII bytes.
|
||||
// We do impose some restrictions on the zone, to avoid stupidity
|
||||
// like newlines.
|
||||
zone := bytes.Index(host[:i], []byte("%25"))
|
||||
if zone >= 0 {
|
||||
host1, err := unescape(host[:zone], encodeHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host2, err := unescape(host[zone:i], encodeZone)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
host3, err := unescape(host[i:], encodeHost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return append(host1, append(host2, host3...)...), nil
|
||||
}
|
||||
} else if i := bytes.LastIndexByte(host, ':'); i != -1 {
|
||||
colonPort := host[i:]
|
||||
if !validOptionalPort(colonPort) {
|
||||
return nil, fmt.Errorf("invalid port %q after host", colonPort)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if host, err = unescape(host, encodeHost); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return host, nil
|
||||
}
|
||||
|
||||
type encoding int
|
||||
|
||||
const (
|
||||
encodeHost encoding = 1 + iota
|
||||
encodeZone
|
||||
)
|
||||
|
||||
type EscapeError string
|
||||
|
||||
func (e EscapeError) Error() string {
|
||||
return "invalid URL escape " + strconv.Quote(string(e))
|
||||
}
|
||||
|
||||
type InvalidHostError string
|
||||
|
||||
func (e InvalidHostError) Error() string {
|
||||
return "invalid character " + strconv.Quote(string(e)) + " in host name"
|
||||
}
|
||||
|
||||
// unescape unescapes a string; the mode specifies
|
||||
// which section of the URL string is being unescaped.
|
||||
//
|
||||
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L199
|
||||
//
|
||||
// Unescapes in place overwriting the contents of s and returning it.
|
||||
func unescape(s []byte, mode encoding) ([]byte, error) {
|
||||
// Count %, check that they're well-formed.
|
||||
n := 0
|
||||
for i := 0; i < len(s); {
|
||||
switch s[i] {
|
||||
case '%':
|
||||
n++
|
||||
if i+2 >= len(s) || !ishex(s[i+1]) || !ishex(s[i+2]) {
|
||||
s = s[i:]
|
||||
if len(s) > 3 {
|
||||
s = s[:3]
|
||||
}
|
||||
return nil, EscapeError(s)
|
||||
}
|
||||
// Per https://tools.ietf.org/html/rfc3986#page-21
|
||||
// in the host component %-encoding can only be used
|
||||
// for non-ASCII bytes.
|
||||
// But https://tools.ietf.org/html/rfc6874#section-2
|
||||
// introduces %25 being allowed to escape a percent sign
|
||||
// in IPv6 scoped-address literals. Yay.
|
||||
if mode == encodeHost && unhex(s[i+1]) < 8 && !bytes.Equal(s[i:i+3], []byte("%25")) {
|
||||
return nil, EscapeError(s[i : i+3])
|
||||
}
|
||||
if mode == encodeZone {
|
||||
// RFC 6874 says basically "anything goes" for zone identifiers
|
||||
// and that even non-ASCII can be redundantly escaped,
|
||||
// but it seems prudent to restrict %-escaped bytes here to those
|
||||
// that are valid host name bytes in their unescaped form.
|
||||
// That is, you can use escaping in the zone identifier but not
|
||||
// to introduce bytes you couldn't just write directly.
|
||||
// But Windows puts spaces here! Yay.
|
||||
v := unhex(s[i+1])<<4 | unhex(s[i+2])
|
||||
if !bytes.Equal(s[i:i+3], []byte("%25")) && v != ' ' && shouldEscape(v, encodeHost) {
|
||||
return nil, EscapeError(s[i : i+3])
|
||||
}
|
||||
}
|
||||
i += 3
|
||||
default:
|
||||
if (mode == encodeHost || mode == encodeZone) && s[i] < 0x80 && shouldEscape(s[i], mode) {
|
||||
return nil, InvalidHostError(s[i : i+1])
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
t := s[:0]
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '%':
|
||||
t = append(t, unhex(s[i+1])<<4|unhex(s[i+2]))
|
||||
i += 2
|
||||
default:
|
||||
t = append(t, s[i])
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Return true if the specified character should be escaped when
|
||||
// appearing in a URL string, according to RFC 3986.
|
||||
//
|
||||
// Please be informed that for now shouldEscape does not check all
|
||||
// reserved characters correctly. See https://github.com/golang/go/issues/5684.
|
||||
//
|
||||
// Based on https://github.com/golang/go/blob/8ac5cbe05d61df0a7a7c9a38ff33305d4dcfea32/src/net/url/url.go#L100
|
||||
func shouldEscape(c byte, mode encoding) bool {
|
||||
// §2.3 Unreserved characters (alphanum)
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {
|
||||
return false
|
||||
}
|
||||
|
||||
if mode == encodeHost || mode == encodeZone {
|
||||
// §3.2.2 Host allows
|
||||
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
|
||||
// as part of reg-name.
|
||||
// We add : because we include :port as part of host.
|
||||
// We add [ ] because we include [ipv6]:port as part of host.
|
||||
// We add < > because they're the only characters left that
|
||||
// we could possibly allow, and Parse will reject them if we
|
||||
// escape them (because hosts can't use %-encoding for
|
||||
// ASCII bytes).
|
||||
switch c {
|
||||
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=', ':', '[', ']', '<', '>', '"':
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if c == '-' || c == '_' || c == '.' || c == '~' { // §2.3 Unreserved characters (mark)
|
||||
return false
|
||||
}
|
||||
|
||||
// Everything else must be escaped.
|
||||
return true
|
||||
}
|
||||
|
||||
func ishex(c byte) bool {
|
||||
return ('0' <= c && c <= '9') ||
|
||||
('a' <= c && c <= 'f') ||
|
||||
('A' <= c && c <= 'F')
|
||||
}
|
||||
|
||||
func unhex(c byte) byte {
|
||||
switch {
|
||||
case '0' <= c && c <= '9':
|
||||
return c - '0'
|
||||
case 'a' <= c && c <= 'f':
|
||||
return c - 'a' + 10
|
||||
case 'A' <= c && c <= 'F':
|
||||
return c - 'A' + 10
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// validOptionalPort reports whether port is either an empty string
|
||||
// or matches /^:\d*$/.
|
||||
func validOptionalPort(port []byte) bool {
|
||||
if len(port) == 0 {
|
||||
return true
|
||||
}
|
||||
if port[0] != ':' {
|
||||
return false
|
||||
}
|
||||
for _, b := range port[1:] {
|
||||
if b < '0' || b > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normalizePath(dst, src []byte) []byte {
|
||||
dst = dst[:0]
|
||||
dst = addLeadingSlash(dst, src)
|
||||
dst = decodeArgAppendNoPlus(dst, src)
|
||||
|
||||
// remove duplicate slashes
|
||||
b := dst
|
||||
bSize := len(b)
|
||||
for {
|
||||
n := bytes.Index(b, strSlashSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
b = b[n:]
|
||||
copy(b, b[1:])
|
||||
b = b[:len(b)-1]
|
||||
bSize--
|
||||
}
|
||||
dst = dst[:bSize]
|
||||
|
||||
// remove /./ parts
|
||||
b = dst
|
||||
for {
|
||||
n := bytes.Index(b, strSlashDotSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
nn := n + len(strSlashDotSlash) - 1
|
||||
copy(b[n:], b[nn:])
|
||||
b = b[:len(b)-nn+n]
|
||||
}
|
||||
|
||||
// remove /foo/../ parts
|
||||
for {
|
||||
n := bytes.Index(b, strSlashDotDotSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
nn := bytes.LastIndexByte(b[:n], '/')
|
||||
if nn < 0 {
|
||||
nn = 0
|
||||
}
|
||||
n += len(strSlashDotDotSlash) - 1
|
||||
copy(b[nn:], b[n:])
|
||||
b = b[:len(b)-n+nn]
|
||||
}
|
||||
|
||||
// remove trailing /foo/..
|
||||
n := bytes.LastIndex(b, strSlashDotDot)
|
||||
if n >= 0 && n+len(strSlashDotDot) == len(b) {
|
||||
nn := bytes.LastIndexByte(b[:n], '/')
|
||||
if nn < 0 {
|
||||
return append(dst[:0], strSlash...)
|
||||
}
|
||||
b = b[:nn+1]
|
||||
}
|
||||
|
||||
if filepath.Separator == '\\' {
|
||||
// remove \.\ parts
|
||||
for {
|
||||
n := bytes.Index(b, strBackSlashDotBackSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
nn := n + len(strSlashDotSlash) - 1
|
||||
copy(b[n:], b[nn:])
|
||||
b = b[:len(b)-nn+n]
|
||||
}
|
||||
|
||||
// remove /foo/..\ parts
|
||||
for {
|
||||
n := bytes.Index(b, strSlashDotDotBackSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
nn := bytes.LastIndexByte(b[:n], '/')
|
||||
if nn < 0 {
|
||||
nn = 0
|
||||
}
|
||||
nn++
|
||||
n += len(strSlashDotDotBackSlash)
|
||||
copy(b[nn:], b[n:])
|
||||
b = b[:len(b)-n+nn]
|
||||
}
|
||||
|
||||
// remove /foo\..\ parts
|
||||
for {
|
||||
n := bytes.Index(b, strBackSlashDotDotBackSlash)
|
||||
if n < 0 {
|
||||
break
|
||||
}
|
||||
nn := bytes.LastIndexByte(b[:n], '/')
|
||||
if nn < 0 {
|
||||
nn = 0
|
||||
}
|
||||
n += len(strBackSlashDotDotBackSlash) - 1
|
||||
copy(b[nn:], b[n:])
|
||||
b = b[:len(b)-n+nn]
|
||||
}
|
||||
|
||||
// remove trailing \foo\..
|
||||
n := bytes.LastIndex(b, strBackSlashDotDot)
|
||||
if n >= 0 && n+len(strSlashDotDot) == len(b) {
|
||||
nn := bytes.LastIndexByte(b[:n], '/')
|
||||
if nn < 0 {
|
||||
return append(dst[:0], strSlash...)
|
||||
}
|
||||
b = b[:nn+1]
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// RequestURI returns RequestURI - i.e. URI without Scheme and Host.
|
||||
func (u *URI) RequestURI() []byte {
|
||||
var dst []byte
|
||||
if u.DisablePathNormalizing {
|
||||
dst = u.requestURI[:0]
|
||||
dst = append(dst, u.PathOriginal()...)
|
||||
} else {
|
||||
dst = appendQuotedPath(u.requestURI[:0], u.Path())
|
||||
}
|
||||
if u.parsedQueryArgs && u.queryArgs.Len() > 0 {
|
||||
dst = append(dst, '?')
|
||||
dst = u.queryArgs.AppendBytes(dst)
|
||||
} else if len(u.queryString) > 0 {
|
||||
dst = append(dst, '?')
|
||||
dst = append(dst, u.queryString...)
|
||||
}
|
||||
u.requestURI = dst
|
||||
return u.requestURI
|
||||
}
|
||||
|
||||
// LastPathSegment returns the last part of uri path after '/'.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// - For /foo/bar/baz.html path returns baz.html.
|
||||
// - For /foo/bar/ returns empty byte slice.
|
||||
// - For /foobar.js returns foobar.js.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) LastPathSegment() []byte {
|
||||
path := u.Path()
|
||||
n := bytes.LastIndexByte(path, '/')
|
||||
if n < 0 {
|
||||
return path
|
||||
}
|
||||
return path[n+1:]
|
||||
}
|
||||
|
||||
// Update updates uri.
|
||||
//
|
||||
// The following newURI types are accepted:
|
||||
//
|
||||
// - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
|
||||
// uri is replaced by newURI.
|
||||
// - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
|
||||
// the original scheme is preserved.
|
||||
// - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
|
||||
// of the original uri is replaced.
|
||||
// - Relative path, i.e. xx?yy=abc . In this case the original RequestURI
|
||||
// is updated according to the new relative path.
|
||||
func (u *URI) Update(newURI string) {
|
||||
u.UpdateBytes(s2b(newURI))
|
||||
}
|
||||
|
||||
// UpdateBytes updates uri.
|
||||
//
|
||||
// The following newURI types are accepted:
|
||||
//
|
||||
// - Absolute, i.e. http://foobar.com/aaa/bb?cc . In this case the original
|
||||
// uri is replaced by newURI.
|
||||
// - Absolute without scheme, i.e. //foobar.com/aaa/bb?cc. In this case
|
||||
// the original scheme is preserved.
|
||||
// - Missing host, i.e. /aaa/bb?cc . In this case only RequestURI part
|
||||
// of the original uri is replaced.
|
||||
// - Relative path, i.e. xx?yy=abc . In this case the original RequestURI
|
||||
// is updated according to the new relative path.
|
||||
func (u *URI) UpdateBytes(newURI []byte) {
|
||||
u.requestURI = u.updateBytes(newURI, u.requestURI)
|
||||
}
|
||||
|
||||
func (u *URI) updateBytes(newURI, buf []byte) []byte {
|
||||
if len(newURI) == 0 {
|
||||
return buf
|
||||
}
|
||||
|
||||
n := bytes.Index(newURI, strSlashSlash)
|
||||
if n >= 0 {
|
||||
// absolute uri
|
||||
var b [32]byte
|
||||
schemeOriginal := b[:0]
|
||||
if len(u.scheme) > 0 {
|
||||
schemeOriginal = append([]byte(nil), u.scheme...)
|
||||
}
|
||||
if err := u.Parse(nil, newURI); err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(schemeOriginal) > 0 && len(u.scheme) == 0 {
|
||||
u.scheme = append(u.scheme[:0], schemeOriginal...)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
if newURI[0] == '/' {
|
||||
// uri without host
|
||||
buf = u.appendSchemeHost(buf[:0])
|
||||
buf = append(buf, newURI...)
|
||||
if err := u.Parse(nil, buf); err != nil {
|
||||
return nil
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// relative path
|
||||
switch newURI[0] {
|
||||
case '?':
|
||||
// query string only update
|
||||
u.SetQueryStringBytes(newURI[1:])
|
||||
return append(buf[:0], u.FullURI()...)
|
||||
case '#':
|
||||
// update only hash
|
||||
u.SetHashBytes(newURI[1:])
|
||||
return append(buf[:0], u.FullURI()...)
|
||||
default:
|
||||
// update the last path part after the slash
|
||||
path := u.Path()
|
||||
n = bytes.LastIndexByte(path, '/')
|
||||
if n < 0 {
|
||||
panic(fmt.Sprintf("BUG: path must contain at least one slash: %q %q", u.Path(), newURI))
|
||||
}
|
||||
buf = u.appendSchemeHost(buf[:0])
|
||||
buf = appendQuotedPath(buf, path[:n+1])
|
||||
buf = append(buf, newURI...)
|
||||
if err := u.Parse(nil, buf); err != nil {
|
||||
return nil
|
||||
}
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
// FullURI returns full uri in the form {Scheme}://{Host}{RequestURI}#{Hash}.
|
||||
//
|
||||
// The returned bytes are valid until the next URI method call.
|
||||
func (u *URI) FullURI() []byte {
|
||||
u.fullURI = u.AppendBytes(u.fullURI[:0])
|
||||
return u.fullURI
|
||||
}
|
||||
|
||||
// AppendBytes appends full uri to dst and returns the extended dst.
|
||||
func (u *URI) AppendBytes(dst []byte) []byte {
|
||||
dst = u.appendSchemeHost(dst)
|
||||
dst = append(dst, u.RequestURI()...)
|
||||
if len(u.hash) > 0 {
|
||||
dst = append(dst, '#')
|
||||
dst = append(dst, u.hash...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func (u *URI) appendSchemeHost(dst []byte) []byte {
|
||||
dst = append(dst, u.Scheme()...)
|
||||
dst = append(dst, strColonSlashSlash...)
|
||||
return append(dst, u.Host()...)
|
||||
}
|
||||
|
||||
// WriteTo writes full uri to w.
|
||||
//
|
||||
// WriteTo implements io.WriterTo interface.
|
||||
func (u *URI) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(u.FullURI())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// String returns full uri.
|
||||
func (u *URI) String() string {
|
||||
return string(u.FullURI())
|
||||
}
|
||||
|
||||
func splitHostURI(host, uri []byte) ([]byte, []byte, []byte) {
|
||||
n := bytes.Index(uri, strSlashSlash)
|
||||
if n < 0 {
|
||||
return strHTTP, host, uri
|
||||
}
|
||||
scheme := uri[:n]
|
||||
if bytes.IndexByte(scheme, '/') >= 0 {
|
||||
return strHTTP, host, uri
|
||||
}
|
||||
if len(scheme) > 0 && scheme[len(scheme)-1] == ':' {
|
||||
scheme = scheme[:len(scheme)-1]
|
||||
}
|
||||
n += len(strSlashSlash)
|
||||
uri = uri[n:]
|
||||
n = bytes.IndexByte(uri, '/')
|
||||
nq := bytes.IndexByte(uri, '?')
|
||||
if nq >= 0 && nq < n {
|
||||
// A hack for urls like foobar.com?a=b/xyz
|
||||
n = nq
|
||||
} else if n < 0 {
|
||||
// A hack for bogus urls like foobar.com?a=b without
|
||||
// slash after host.
|
||||
if nq >= 0 {
|
||||
return scheme, uri[:nq], uri[nq:]
|
||||
}
|
||||
return scheme, uri, strSlash
|
||||
}
|
||||
return scheme, uri[:n], uri[n:]
|
||||
}
|
||||
|
||||
// QueryArgs returns query args.
|
||||
//
|
||||
// The returned args are valid until the next URI method call.
|
||||
func (u *URI) QueryArgs() *Args {
|
||||
u.parseQueryArgs()
|
||||
return &u.queryArgs
|
||||
}
|
||||
|
||||
func (u *URI) parseQueryArgs() {
|
||||
if u.parsedQueryArgs {
|
||||
return
|
||||
}
|
||||
u.queryArgs.ParseBytes(u.queryString)
|
||||
u.parsedQueryArgs = true
|
||||
}
|
||||
|
||||
// stringContainsCTLByte reports whether s contains any ASCII control character.
|
||||
func stringContainsCTLByte(s []byte) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
b := s[i]
|
||||
if b < ' ' || b == 0x7f {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
//go:build !windows
|
||||
|
||||
package fasthttp
|
||||
|
||||
func addLeadingSlash(dst, src []byte) []byte {
|
||||
// add leading slash for unix paths
|
||||
if len(src) == 0 || src[0] != '/' {
|
||||
dst = append(dst, '/')
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
package fasthttp
|
||||
|
||||
func addLeadingSlash(dst, src []byte) []byte {
|
||||
// zero length 、"C:/" and "a" case
|
||||
isDisk := len(src) > 2 && src[1] == ':'
|
||||
if len(src) == 0 || (!isDisk && src[0] != '/') {
|
||||
dst = append(dst, '/')
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type userDataKV struct {
|
||||
key any
|
||||
value any
|
||||
}
|
||||
|
||||
type userData []userDataKV
|
||||
|
||||
func (d *userData) Set(key, value any) {
|
||||
if b, ok := key.([]byte); ok {
|
||||
key = string(b)
|
||||
}
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
kv.value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if value == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c := cap(args)
|
||||
if c > n {
|
||||
args = args[:n+1]
|
||||
kv := &args[n]
|
||||
kv.key = key
|
||||
kv.value = value
|
||||
*d = args
|
||||
return
|
||||
}
|
||||
|
||||
kv := userDataKV{}
|
||||
kv.key = key
|
||||
kv.value = value
|
||||
args = append(args, kv)
|
||||
*d = args
|
||||
}
|
||||
|
||||
func (d *userData) SetBytes(key []byte, value any) {
|
||||
d.Set(key, value)
|
||||
}
|
||||
|
||||
func (d *userData) Get(key any) any {
|
||||
if b, ok := key.([]byte); ok {
|
||||
key = b2s(b)
|
||||
}
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
return kv.value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *userData) GetBytes(key []byte) any {
|
||||
return d.Get(key)
|
||||
}
|
||||
|
||||
func (d *userData) Reset() {
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
v := args[i].value
|
||||
if vc, ok := v.(io.Closer); ok {
|
||||
vc.Close()
|
||||
}
|
||||
}
|
||||
*d = (*d)[:0]
|
||||
}
|
||||
|
||||
func (d *userData) Remove(key any) {
|
||||
if b, ok := key.([]byte); ok {
|
||||
key = b2s(b)
|
||||
}
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if kv.key == key {
|
||||
n--
|
||||
args[i], args[n] = args[n], args[i]
|
||||
args[n].value = nil
|
||||
args = args[:n]
|
||||
*d = args
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *userData) RemoveBytes(key []byte) {
|
||||
d.Remove(key)
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// workerPool serves incoming connections via a pool of workers
|
||||
// in FILO order, i.e. the most recently stopped worker will serve the next
|
||||
// incoming connection.
|
||||
//
|
||||
// Such a scheme keeps CPU caches hot (in theory).
|
||||
type workerPool struct {
|
||||
// Function for serving server connections.
|
||||
// It must leave c unclosed.
|
||||
WorkerFunc ServeHandler
|
||||
|
||||
MaxWorkersCount int
|
||||
|
||||
LogAllErrors bool
|
||||
|
||||
MaxIdleWorkerDuration time.Duration
|
||||
|
||||
Logger Logger
|
||||
|
||||
lock sync.Mutex
|
||||
workersCount int
|
||||
mustStop bool
|
||||
|
||||
ready []*workerChan
|
||||
|
||||
stopCh chan struct{}
|
||||
|
||||
workerChanPool sync.Pool
|
||||
|
||||
connState func(net.Conn, ConnState)
|
||||
}
|
||||
|
||||
type workerChan struct {
|
||||
lastUseTime time.Time
|
||||
ch chan net.Conn
|
||||
}
|
||||
|
||||
func (wp *workerPool) Start() {
|
||||
if wp.stopCh != nil {
|
||||
return
|
||||
}
|
||||
wp.stopCh = make(chan struct{})
|
||||
stopCh := wp.stopCh
|
||||
wp.workerChanPool.New = func() any {
|
||||
return &workerChan{
|
||||
ch: make(chan net.Conn, workerChanCap),
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
var scratch []*workerChan
|
||||
for {
|
||||
wp.clean(&scratch)
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
default:
|
||||
time.Sleep(wp.getMaxIdleWorkerDuration())
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (wp *workerPool) Stop() {
|
||||
if wp.stopCh == nil {
|
||||
return
|
||||
}
|
||||
close(wp.stopCh)
|
||||
wp.stopCh = nil
|
||||
|
||||
// Stop all the workers waiting for incoming connections.
|
||||
// Do not wait for busy workers - they will stop after
|
||||
// serving the connection and noticing wp.mustStop = true.
|
||||
wp.lock.Lock()
|
||||
ready := wp.ready
|
||||
for i := range ready {
|
||||
ready[i].ch <- nil
|
||||
ready[i] = nil
|
||||
}
|
||||
wp.ready = ready[:0]
|
||||
wp.mustStop = true
|
||||
wp.lock.Unlock()
|
||||
}
|
||||
|
||||
func (wp *workerPool) getMaxIdleWorkerDuration() time.Duration {
|
||||
if wp.MaxIdleWorkerDuration <= 0 {
|
||||
return 10 * time.Second
|
||||
}
|
||||
return wp.MaxIdleWorkerDuration
|
||||
}
|
||||
|
||||
func (wp *workerPool) clean(scratch *[]*workerChan) {
|
||||
maxIdleWorkerDuration := wp.getMaxIdleWorkerDuration()
|
||||
|
||||
// Clean least recently used workers if they didn't serve connections
|
||||
// for more than maxIdleWorkerDuration.
|
||||
criticalTime := time.Now().Add(-maxIdleWorkerDuration)
|
||||
|
||||
wp.lock.Lock()
|
||||
ready := wp.ready
|
||||
n := len(ready)
|
||||
|
||||
// Use binary-search algorithm to find out the index of the least recently worker which can be cleaned up.
|
||||
l, r := 0, n-1
|
||||
for l <= r {
|
||||
mid := (l + r) / 2
|
||||
if criticalTime.After(wp.ready[mid].lastUseTime) {
|
||||
l = mid + 1
|
||||
} else {
|
||||
r = mid - 1
|
||||
}
|
||||
}
|
||||
i := r
|
||||
if i == -1 {
|
||||
wp.lock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
*scratch = append((*scratch)[:0], ready[:i+1]...)
|
||||
m := copy(ready, ready[i+1:])
|
||||
for i = m; i < n; i++ {
|
||||
ready[i] = nil
|
||||
}
|
||||
wp.ready = ready[:m]
|
||||
wp.lock.Unlock()
|
||||
|
||||
// Notify obsolete workers to stop.
|
||||
// This notification must be outside the wp.lock, since ch.ch
|
||||
// may be blocking and may consume a lot of time if many workers
|
||||
// are located on non-local CPUs.
|
||||
tmp := *scratch
|
||||
for i := range tmp {
|
||||
tmp[i].ch <- nil
|
||||
tmp[i] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (wp *workerPool) Serve(c net.Conn) bool {
|
||||
ch := wp.getCh()
|
||||
if ch == nil {
|
||||
return false
|
||||
}
|
||||
ch.ch <- c
|
||||
return true
|
||||
}
|
||||
|
||||
var workerChanCap = func() int {
|
||||
// Use blocking workerChan if GOMAXPROCS=1.
|
||||
// This immediately switches Serve to WorkerFunc, which results
|
||||
// in higher performance (under go1.5 at least).
|
||||
if runtime.GOMAXPROCS(0) == 1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Use non-blocking workerChan if GOMAXPROCS>1,
|
||||
// since otherwise the Serve caller (Acceptor) may lag accepting
|
||||
// new connections if WorkerFunc is CPU-bound.
|
||||
return 1
|
||||
}()
|
||||
|
||||
func (wp *workerPool) getCh() *workerChan {
|
||||
var ch *workerChan
|
||||
createWorker := false
|
||||
|
||||
wp.lock.Lock()
|
||||
ready := wp.ready
|
||||
n := len(ready) - 1
|
||||
if n < 0 {
|
||||
if wp.workersCount < wp.MaxWorkersCount {
|
||||
createWorker = true
|
||||
wp.workersCount++
|
||||
}
|
||||
} else {
|
||||
ch = ready[n]
|
||||
ready[n] = nil
|
||||
wp.ready = ready[:n]
|
||||
}
|
||||
wp.lock.Unlock()
|
||||
|
||||
if ch == nil {
|
||||
if !createWorker {
|
||||
return nil
|
||||
}
|
||||
vch := wp.workerChanPool.Get()
|
||||
ch = vch.(*workerChan)
|
||||
go func() {
|
||||
wp.workerFunc(ch)
|
||||
wp.workerChanPool.Put(vch)
|
||||
}()
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (wp *workerPool) release(ch *workerChan) bool {
|
||||
ch.lastUseTime = time.Now()
|
||||
wp.lock.Lock()
|
||||
if wp.mustStop {
|
||||
wp.lock.Unlock()
|
||||
return false
|
||||
}
|
||||
wp.ready = append(wp.ready, ch)
|
||||
wp.lock.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
func (wp *workerPool) workerFunc(ch *workerChan) {
|
||||
var c net.Conn
|
||||
|
||||
var err error
|
||||
for c = range ch.ch {
|
||||
if c == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err = wp.WorkerFunc(c); err != nil && err != errHijacked {
|
||||
errStr := err.Error()
|
||||
if wp.LogAllErrors || !(strings.Contains(errStr, "broken pipe") ||
|
||||
strings.Contains(errStr, "reset by peer") ||
|
||||
strings.Contains(errStr, "request headers: small read buffer") ||
|
||||
strings.Contains(errStr, "unexpected EOF") ||
|
||||
strings.Contains(errStr, "i/o timeout") ||
|
||||
errors.Is(err, ErrBadTrailer)) {
|
||||
wp.Logger.Printf("error when serving connection %q<->%q: %v", c.LocalAddr(), c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
if err == errHijacked {
|
||||
wp.connState(c, StateHijacked)
|
||||
} else {
|
||||
_ = c.Close()
|
||||
wp.connState(c, StateClosed)
|
||||
}
|
||||
|
||||
if !wp.release(ch) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wp.lock.Lock()
|
||||
wp.workersCount--
|
||||
wp.lock.Unlock()
|
||||
}
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"github.com/valyala/fasthttp/stackless"
|
||||
)
|
||||
|
||||
const (
|
||||
CompressZstdSpeedNotSet = iota
|
||||
CompressZstdBestSpeed
|
||||
CompressZstdDefault
|
||||
CompressZstdSpeedBetter
|
||||
CompressZstdBestCompression
|
||||
)
|
||||
|
||||
var (
|
||||
zstdDecoderPool sync.Pool
|
||||
zstdEncoderPool sync.Pool
|
||||
realZstdWriterPoolMap = newCompressWriterPoolMap()
|
||||
stacklessZstdWriterPoolMap = newCompressWriterPoolMap()
|
||||
)
|
||||
|
||||
func acquireZstdReader(r io.Reader) (*zstd.Decoder, error) {
|
||||
v := zstdDecoderPool.Get()
|
||||
if v == nil {
|
||||
return zstd.NewReader(r)
|
||||
}
|
||||
zr := v.(*zstd.Decoder)
|
||||
if err := zr.Reset(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return zr, nil
|
||||
}
|
||||
|
||||
func releaseZstdReader(zr *zstd.Decoder) {
|
||||
zstdDecoderPool.Put(zr)
|
||||
}
|
||||
|
||||
func acquireZstdWriter(w io.Writer, level int) (*zstd.Encoder, error) {
|
||||
v := zstdEncoderPool.Get()
|
||||
if v == nil {
|
||||
return zstd.NewWriter(w, zstd.WithEncoderLevel(zstd.EncoderLevel(level)))
|
||||
}
|
||||
zw := v.(*zstd.Encoder)
|
||||
zw.Reset(w)
|
||||
return zw, nil
|
||||
}
|
||||
|
||||
func releaseZstdWriter(zw *zstd.Encoder) { //nolint:unused
|
||||
zw.Close()
|
||||
zstdEncoderPool.Put(zw)
|
||||
}
|
||||
|
||||
func acquireStacklessZstdWriter(w io.Writer, compressLevel int) stackless.Writer {
|
||||
nLevel := normalizeZstdCompressLevel(compressLevel)
|
||||
p := stacklessZstdWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
return stackless.NewWriter(w, func(w io.Writer) stackless.Writer {
|
||||
return acquireRealZstdWriter(w, compressLevel)
|
||||
})
|
||||
}
|
||||
sw := v.(stackless.Writer)
|
||||
sw.Reset(w)
|
||||
return sw
|
||||
}
|
||||
|
||||
func releaseStacklessZstdWriter(zf stackless.Writer, zstdDefault int) {
|
||||
zf.Close()
|
||||
nLevel := normalizeZstdCompressLevel(zstdDefault)
|
||||
p := stacklessZstdWriterPoolMap[nLevel]
|
||||
p.Put(zf)
|
||||
}
|
||||
|
||||
func acquireRealZstdWriter(w io.Writer, level int) *zstd.Encoder {
|
||||
nLevel := normalizeZstdCompressLevel(level)
|
||||
p := realZstdWriterPoolMap[nLevel]
|
||||
v := p.Get()
|
||||
if v == nil {
|
||||
zw, err := acquireZstdWriter(w, level)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return zw
|
||||
}
|
||||
zw := v.(*zstd.Encoder)
|
||||
zw.Reset(w)
|
||||
return zw
|
||||
}
|
||||
|
||||
func releaseRealZstdWrter(zw *zstd.Encoder, level int) {
|
||||
zw.Close()
|
||||
nLevel := normalizeZstdCompressLevel(level)
|
||||
p := realZstdWriterPoolMap[nLevel]
|
||||
p.Put(zw)
|
||||
}
|
||||
|
||||
func AppendZstdBytesLevel(dst, src []byte, level int) []byte {
|
||||
w := &byteSliceWriter{dst}
|
||||
WriteZstdLevel(w, src, level) //nolint:errcheck
|
||||
return w.b
|
||||
}
|
||||
|
||||
func WriteZstdLevel(w io.Writer, p []byte, level int) (int, error) {
|
||||
level = normalizeZstdCompressLevel(level)
|
||||
switch w.(type) {
|
||||
case *byteSliceWriter,
|
||||
*bytes.Buffer,
|
||||
*bytebufferpool.ByteBuffer:
|
||||
ctx := &compressCtx{
|
||||
w: w,
|
||||
p: p,
|
||||
level: level,
|
||||
}
|
||||
stacklessWriteZstd(ctx)
|
||||
return len(p), nil
|
||||
default:
|
||||
zw := acquireStacklessZstdWriter(w, level)
|
||||
n, err := zw.Write(p)
|
||||
releaseStacklessZstdWriter(zw, level)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
stacklessWriteZstdOnce sync.Once
|
||||
stacklessWriteZstdFunc func(ctx any) bool
|
||||
)
|
||||
|
||||
func stacklessWriteZstd(ctx any) {
|
||||
stacklessWriteZstdOnce.Do(func() {
|
||||
stacklessWriteZstdFunc = stackless.NewFunc(nonblockingWriteZstd)
|
||||
})
|
||||
stacklessWriteZstdFunc(ctx)
|
||||
}
|
||||
|
||||
func nonblockingWriteZstd(ctxv any) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealZstdWriter(ctx.w, ctx.level)
|
||||
zw.Write(ctx.p) //nolint:errcheck
|
||||
releaseRealZstdWrter(zw, ctx.level)
|
||||
}
|
||||
|
||||
// AppendZstdBytes appends zstd src to dst and returns the resulting dst.
|
||||
func AppendZstdBytes(dst, src []byte) []byte {
|
||||
return AppendZstdBytesLevel(dst, src, CompressZstdDefault)
|
||||
}
|
||||
|
||||
// WriteUnzstd writes unzstd p to w and returns the number of uncompressed
|
||||
// bytes written to w.
|
||||
func WriteUnzstd(w io.Writer, p []byte) (int, error) {
|
||||
r := &byteSliceReader{p}
|
||||
zr, err := acquireZstdReader(r)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := copyZeroAlloc(w, zr)
|
||||
releaseZstdReader(zr)
|
||||
nn := int(n)
|
||||
if int64(nn) != n {
|
||||
return 0, fmt.Errorf("too much data unzstd: %d", n)
|
||||
}
|
||||
return nn, err
|
||||
}
|
||||
|
||||
// AppendUnzstdBytes appends unzstd src to dst and returns the resulting dst.
|
||||
func AppendUnzstdBytes(dst, src []byte) ([]byte, error) {
|
||||
w := &byteSliceWriter{dst}
|
||||
_, err := WriteUnzstd(w, src)
|
||||
return w.b, err
|
||||
}
|
||||
|
||||
// normalizes compression level into [0..7], so it could be used as an index
|
||||
// in *PoolMap.
|
||||
func normalizeZstdCompressLevel(level int) int {
|
||||
if level < CompressZstdSpeedNotSet || level > CompressZstdBestCompression {
|
||||
level = CompressZstdDefault
|
||||
}
|
||||
return level
|
||||
}
|
||||
Reference in New Issue
Block a user