init
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.6
|
||||
|
||||
script:
|
||||
# build test for supported platforms
|
||||
- GOOS=linux go build
|
||||
- GOOS=darwin go build
|
||||
- GOOS=freebsd go build
|
||||
- GOOS=windows go build
|
||||
- GOARCH=386 go build
|
||||
|
||||
# run tests on a standard platform
|
||||
- go test -v ./...
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Aliaksandr Valialkin, VertaMedia
|
||||
|
||||
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.
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
[](https://travis-ci.org/valyala/bytebufferpool)
|
||||
[](http://godoc.org/github.com/valyala/bytebufferpool)
|
||||
[](http://goreportcard.com/report/valyala/bytebufferpool)
|
||||
|
||||
# bytebufferpool
|
||||
|
||||
An implementation of a pool of byte buffers with anti-memory-waste protection.
|
||||
|
||||
The pool may waste limited amount of memory due to fragmentation.
|
||||
This amount equals to the maximum total size of the byte buffers
|
||||
in concurrent use.
|
||||
|
||||
# Benchmark results
|
||||
Currently bytebufferpool is fastest and most effective buffer pool written in Go.
|
||||
|
||||
You can find results [here](https://omgnull.github.io/go-benchmark/buffer/).
|
||||
|
||||
# bytebufferpool users
|
||||
|
||||
* [fasthttp](https://github.com/valyala/fasthttp)
|
||||
* [quicktemplate](https://github.com/valyala/quicktemplate)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package bytebufferpool
|
||||
|
||||
import "io"
|
||||
|
||||
// ByteBuffer provides byte buffer, which can be used for minimizing
|
||||
// memory allocations.
|
||||
//
|
||||
// ByteBuffer may be used with functions appending data to the given []byte
|
||||
// slice. See example code for details.
|
||||
//
|
||||
// Use Get for obtaining an empty byte buffer.
|
||||
type ByteBuffer struct {
|
||||
|
||||
// B is a byte buffer to use in append-like workloads.
|
||||
// See example code for details.
|
||||
B []byte
|
||||
}
|
||||
|
||||
// Len returns the size of the byte buffer.
|
||||
func (b *ByteBuffer) Len() int {
|
||||
return len(b.B)
|
||||
}
|
||||
|
||||
// ReadFrom implements io.ReaderFrom.
|
||||
//
|
||||
// The function appends all the data read from r to b.
|
||||
func (b *ByteBuffer) ReadFrom(r io.Reader) (int64, error) {
|
||||
p := b.B
|
||||
nStart := int64(len(p))
|
||||
nMax := int64(cap(p))
|
||||
n := nStart
|
||||
if nMax == 0 {
|
||||
nMax = 64
|
||||
p = make([]byte, nMax)
|
||||
} else {
|
||||
p = p[:nMax]
|
||||
}
|
||||
for {
|
||||
if n == nMax {
|
||||
nMax *= 2
|
||||
bNew := make([]byte, nMax)
|
||||
copy(bNew, p)
|
||||
p = bNew
|
||||
}
|
||||
nn, err := r.Read(p[n:])
|
||||
n += int64(nn)
|
||||
if err != nil {
|
||||
b.B = p[:n]
|
||||
n -= nStart
|
||||
if err == io.EOF {
|
||||
return n, nil
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WriteTo implements io.WriterTo.
|
||||
func (b *ByteBuffer) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(b.B)
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Bytes returns b.B, i.e. all the bytes accumulated in the buffer.
|
||||
//
|
||||
// The purpose of this function is bytes.Buffer compatibility.
|
||||
func (b *ByteBuffer) Bytes() []byte {
|
||||
return b.B
|
||||
}
|
||||
|
||||
// Write implements io.Writer - it appends p to ByteBuffer.B
|
||||
func (b *ByteBuffer) Write(p []byte) (int, error) {
|
||||
b.B = append(b.B, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// WriteByte appends the byte c to the buffer.
|
||||
//
|
||||
// The purpose of this function is bytes.Buffer compatibility.
|
||||
//
|
||||
// The function always returns nil.
|
||||
func (b *ByteBuffer) WriteByte(c byte) error {
|
||||
b.B = append(b.B, c)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteString appends s to ByteBuffer.B.
|
||||
func (b *ByteBuffer) WriteString(s string) (int, error) {
|
||||
b.B = append(b.B, s...)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
// Set sets ByteBuffer.B to p.
|
||||
func (b *ByteBuffer) Set(p []byte) {
|
||||
b.B = append(b.B[:0], p...)
|
||||
}
|
||||
|
||||
// SetString sets ByteBuffer.B to s.
|
||||
func (b *ByteBuffer) SetString(s string) {
|
||||
b.B = append(b.B[:0], s...)
|
||||
}
|
||||
|
||||
// String returns string representation of ByteBuffer.B.
|
||||
func (b *ByteBuffer) String() string {
|
||||
return string(b.B)
|
||||
}
|
||||
|
||||
// Reset makes ByteBuffer.B empty.
|
||||
func (b *ByteBuffer) Reset() {
|
||||
b.B = b.B[:0]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// Package bytebufferpool implements a pool of byte buffers
|
||||
// with anti-fragmentation protection.
|
||||
//
|
||||
// The pool may waste limited amount of memory due to fragmentation.
|
||||
// This amount equals to the maximum total size of the byte buffers
|
||||
// in concurrent use.
|
||||
package bytebufferpool
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package bytebufferpool
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
minBitSize = 6 // 2**6=64 is a CPU cache line size
|
||||
steps = 20
|
||||
|
||||
minSize = 1 << minBitSize
|
||||
maxSize = 1 << (minBitSize + steps - 1)
|
||||
|
||||
calibrateCallsThreshold = 42000
|
||||
maxPercentile = 0.95
|
||||
)
|
||||
|
||||
// Pool represents byte buffer pool.
|
||||
//
|
||||
// Distinct pools may be used for distinct types of byte buffers.
|
||||
// Properly determined byte buffer types with their own pools may help reducing
|
||||
// memory waste.
|
||||
type Pool struct {
|
||||
calls [steps]uint64
|
||||
calibrating uint64
|
||||
|
||||
defaultSize uint64
|
||||
maxSize uint64
|
||||
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
var defaultPool Pool
|
||||
|
||||
// Get returns an empty byte buffer from the pool.
|
||||
//
|
||||
// Got byte buffer may be returned to the pool via Put call.
|
||||
// This reduces the number of memory allocations required for byte buffer
|
||||
// management.
|
||||
func Get() *ByteBuffer { return defaultPool.Get() }
|
||||
|
||||
// Get returns new byte buffer with zero length.
|
||||
//
|
||||
// The byte buffer may be returned to the pool via Put after the use
|
||||
// in order to minimize GC overhead.
|
||||
func (p *Pool) Get() *ByteBuffer {
|
||||
v := p.pool.Get()
|
||||
if v != nil {
|
||||
return v.(*ByteBuffer)
|
||||
}
|
||||
return &ByteBuffer{
|
||||
B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)),
|
||||
}
|
||||
}
|
||||
|
||||
// Put returns byte buffer to the pool.
|
||||
//
|
||||
// ByteBuffer.B mustn't be touched after returning it to the pool.
|
||||
// Otherwise data races will occur.
|
||||
func Put(b *ByteBuffer) { defaultPool.Put(b) }
|
||||
|
||||
// Put releases byte buffer obtained via Get to the pool.
|
||||
//
|
||||
// The buffer mustn't be accessed after returning to the pool.
|
||||
func (p *Pool) Put(b *ByteBuffer) {
|
||||
idx := index(len(b.B))
|
||||
|
||||
if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold {
|
||||
p.calibrate()
|
||||
}
|
||||
|
||||
maxSize := int(atomic.LoadUint64(&p.maxSize))
|
||||
if maxSize == 0 || cap(b.B) <= maxSize {
|
||||
b.Reset()
|
||||
p.pool.Put(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) calibrate() {
|
||||
if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
a := make(callSizes, 0, steps)
|
||||
var callsSum uint64
|
||||
for i := uint64(0); i < steps; i++ {
|
||||
calls := atomic.SwapUint64(&p.calls[i], 0)
|
||||
callsSum += calls
|
||||
a = append(a, callSize{
|
||||
calls: calls,
|
||||
size: minSize << i,
|
||||
})
|
||||
}
|
||||
sort.Sort(a)
|
||||
|
||||
defaultSize := a[0].size
|
||||
maxSize := defaultSize
|
||||
|
||||
maxSum := uint64(float64(callsSum) * maxPercentile)
|
||||
callsSum = 0
|
||||
for i := 0; i < steps; i++ {
|
||||
if callsSum > maxSum {
|
||||
break
|
||||
}
|
||||
callsSum += a[i].calls
|
||||
size := a[i].size
|
||||
if size > maxSize {
|
||||
maxSize = size
|
||||
}
|
||||
}
|
||||
|
||||
atomic.StoreUint64(&p.defaultSize, defaultSize)
|
||||
atomic.StoreUint64(&p.maxSize, maxSize)
|
||||
|
||||
atomic.StoreUint64(&p.calibrating, 0)
|
||||
}
|
||||
|
||||
type callSize struct {
|
||||
calls uint64
|
||||
size uint64
|
||||
}
|
||||
|
||||
type callSizes []callSize
|
||||
|
||||
func (ci callSizes) Len() int {
|
||||
return len(ci)
|
||||
}
|
||||
|
||||
func (ci callSizes) Less(i, j int) bool {
|
||||
return ci[i].calls > ci[j].calls
|
||||
}
|
||||
|
||||
func (ci callSizes) Swap(i, j int) {
|
||||
ci[i], ci[j] = ci[j], ci[i]
|
||||
}
|
||||
|
||||
func index(n int) int {
|
||||
n--
|
||||
n >>= minBitSize
|
||||
idx := 0
|
||||
for n > 0 {
|
||||
n >>= 1
|
||||
idx++
|
||||
}
|
||||
if idx >= steps {
|
||||
idx = steps - 1
|
||||
}
|
||||
return idx
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.9.x
|
||||
- 1.8.x
|
||||
|
||||
script:
|
||||
# build test for supported platforms
|
||||
- GOOS=linux go build
|
||||
- GOOS=darwin go build
|
||||
- GOOS=freebsd go build
|
||||
- GOOS=windows go build
|
||||
- GOARCH=386 go build
|
||||
|
||||
# run tests on a standard platform
|
||||
- go test -v ./...
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2016 Aliaksandr Valialkin, VertaMedia
|
||||
|
||||
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.
|
||||
|
||||
+580
@@ -0,0 +1,580 @@
|
||||
[](https://travis-ci.org/valyala/fasthttp)
|
||||
[](http://godoc.org/github.com/valyala/fasthttp)
|
||||
[](https://goreportcard.com/report/github.com/valyala/fasthttp)
|
||||
|
||||
# fasthttp
|
||||
Fast HTTP implementation for Go.
|
||||
|
||||
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 12 results](https://www.techempower.com/benchmarks/#section=data-r12&hw=peak&test=plaintext)
|
||||
|
||||
[Server Benchmarks](#http-server-performance-comparison-with-nethttp)
|
||||
|
||||
[Client Benchmarks](#http-client-comparison-with-nethttp)
|
||||
|
||||
[Install](#install)
|
||||
|
||||
[Documentation](https://godoc.org/github.com/valyala/fasthttp)
|
||||
|
||||
[Examples from docs](https://godoc.org/github.com/valyala/fasthttp#pkg-examples)
|
||||
|
||||
[Code examples](examples)
|
||||
|
||||
[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://golang.org/pkg/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://godoc.org/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://godoc.org/github.com/valyala/fasthttp#RequestHandler)
|
||||
instead of objects implementing [Handler interface](https://golang.org/pkg/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://godoc.org/github.com/valyala/fasthttp#RequestHandler)
|
||||
accepts only one argument - [RequestCtx](https://godoc.org/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://golang.org/pkg/net/http/#ServeMux),
|
||||
but there are more powerful third-party routers and web frameworks
|
||||
with fasthttp support:
|
||||
|
||||
* [Iris](https://github.com/kataras/iris)
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing)
|
||||
* [fasthttprouter](https://github.com/buaazp/fasthttprouter)
|
||||
* [lu](https://github.com/vincentLiuxiang/lu)
|
||||
|
||||
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)
|
||||
```
|
||||
|
||||
* 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://godoc.org/github.com/valyala/fasthttp#RequestCtx.PostBody)
|
||||
* r.URL.Path -> [ctx.Path()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Path)
|
||||
* r.URL -> [ctx.URI()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.URI)
|
||||
* r.Method -> [ctx.Method()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Method)
|
||||
* r.Header -> [ctx.Request.Header](https://godoc.org/github.com/valyala/fasthttp#RequestHeader)
|
||||
* r.Header.Get() -> [ctx.Request.Header.Peek()](https://godoc.org/github.com/valyala/fasthttp#RequestHeader.Peek)
|
||||
* r.Host -> [ctx.Host()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Host)
|
||||
* r.Form -> [ctx.QueryArgs()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.QueryArgs) +
|
||||
[ctx.PostArgs()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.PostArgs)
|
||||
* r.PostForm -> [ctx.PostArgs()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.PostArgs)
|
||||
* r.FormValue() -> [ctx.FormValue()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.FormValue)
|
||||
* r.FormFile() -> [ctx.FormFile()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.FormFile)
|
||||
* r.MultipartForm -> [ctx.MultipartForm()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.MultipartForm)
|
||||
* r.RemoteAddr -> [ctx.RemoteAddr()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.RemoteAddr)
|
||||
* r.RequestURI -> [ctx.RequestURI()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.RequestURI)
|
||||
* r.TLS -> [ctx.IsTLS()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.IsTLS)
|
||||
* r.Cookie() -> [ctx.Request.Header.Cookie()](https://godoc.org/github.com/valyala/fasthttp#RequestHeader.Cookie)
|
||||
* r.Referer() -> [ctx.Referer()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Referer)
|
||||
* r.UserAgent() -> [ctx.UserAgent()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.UserAgent)
|
||||
* w.Header() -> [ctx.Response.Header](https://godoc.org/github.com/valyala/fasthttp#ResponseHeader)
|
||||
* w.Header().Set() -> [ctx.Response.Header.Set()](https://godoc.org/github.com/valyala/fasthttp#ResponseHeader.Set)
|
||||
* w.Header().Set("Content-Type") -> [ctx.SetContentType()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.SetContentType)
|
||||
* w.Header().Set("Set-Cookie") -> [ctx.Response.Header.SetCookie()](https://godoc.org/github.com/valyala/fasthttp#ResponseHeader.SetCookie)
|
||||
* w.Write() -> [ctx.Write()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Write),
|
||||
[ctx.SetBody()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.SetBody),
|
||||
[ctx.SetBodyStream()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.SetBodyStream),
|
||||
[ctx.SetBodyStreamWriter()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.SetBodyStreamWriter)
|
||||
* w.WriteHeader() -> [ctx.SetStatusCode()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.SetStatusCode)
|
||||
* w.(http.Hijacker).Hijack() -> [ctx.Hijack()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Hijack)
|
||||
* http.Error() -> [ctx.Error()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Error)
|
||||
* http.FileServer() -> [fasthttp.FSHandler()](https://godoc.org/github.com/valyala/fasthttp#FSHandler),
|
||||
[fasthttp.FS](https://godoc.org/github.com/valyala/fasthttp#FS)
|
||||
* http.ServeFile() -> [fasthttp.ServeFile()](https://godoc.org/github.com/valyala/fasthttp#ServeFile)
|
||||
* http.Redirect() -> [ctx.Redirect()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Redirect)
|
||||
* http.NotFound() -> [ctx.NotFound()](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.NotFound)
|
||||
* http.StripPrefix() -> [fasthttp.PathRewriteFunc](https://godoc.org/github.com/valyala/fasthttp#PathRewriteFunc)
|
||||
|
||||
* *VERY IMPORTANT!* Fasthttp disallows holding references
|
||||
to [RequestCtx](https://godoc.org/github.com/valyala/fasthttp#RequestCtx) or to its'
|
||||
members after returning from [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler).
|
||||
Otherwise [data races](http://blog.golang.org/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://godoc.org/github.com/valyala/fasthttp#TimeoutHandler).
|
||||
* Call [TimeoutError](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from RequestHandler if there are references to RequestCtx or to its' members.
|
||||
See [the example](https://godoc.org/github.com/valyala/fasthttp#example-RequestCtx-TimeoutError)
|
||||
for more details.
|
||||
|
||||
Use this brilliant tool - [race detector](http://blog.golang.org/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://godoc.org/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from [RequestHandler](https://godoc.org/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://godoc.org/github.com/valyala/fasthttp#RequestHandler).
|
||||
So [profile](http://blog.golang.org/profiling-go-programs) and optimize your
|
||||
code after switching to fasthttp. For instance, use [quicktemplate](https://github.com/valyala/quicktemplate)
|
||||
instead of [html/template](https://golang.org/pkg/html/template/).
|
||||
|
||||
* See also [fasthttputil](https://godoc.org/github.com/valyala/fasthttp/fasthttputil),
|
||||
[fasthttpadaptor](https://godoc.org/github.com/valyala/fasthttp/fasthttpadaptor) and
|
||||
[expvarhandler](https://godoc.org/github.com/valyala/fasthttp/expvarhandler).
|
||||
|
||||
|
||||
# Performance optimization tips for multi-core systems
|
||||
|
||||
* Use [reuseport](https://godoc.org/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 Go 1.6 as it provides some considerable 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://golang.org/pkg/sync/#Pool) is your best friend.
|
||||
* [Profile your program](http://blog.golang.org/profiling-go-programs)
|
||||
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://golang.org/pkg/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://golang.org/doc/articles/race_detector.html) on a regular basis.
|
||||
* Prefer [quicktemplate](https://github.com/valyala/quicktemplate) instead of
|
||||
[html/template](https://golang.org/pkg/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)
|
||||
```
|
||||
|
||||
# Related projects
|
||||
|
||||
* [fasthttp-contrib](https://github.com/fasthttp-contrib) - various useful
|
||||
helpers for projects based on fasthttp.
|
||||
* [iris](https://github.com/kataras/iris) - web application framework built
|
||||
on top of fasthttp. Features speed and functionality.
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing) - fast and
|
||||
powerful routing package for fasthttp servers.
|
||||
* [fasthttprouter](https://github.com/buaazp/fasthttprouter) - a high
|
||||
performance fasthttp request router that scales well.
|
||||
* [lu](https://github.com/vincentLiuxiang/lu) - a high performance
|
||||
go middleware web framework which is based on fasthttp.
|
||||
* [websocket](https://github.com/leavengood/websocket) - Gorilla-based
|
||||
websocket implementation for fasthttp.
|
||||
|
||||
|
||||
# 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://golang.org/pkg/net/http/#Hijacker)
|
||||
to [fasthttp connection hijacking](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Hijack).
|
||||
* Compare [net/http Request.Body reading](https://golang.org/pkg/net/http/#Request)
|
||||
to [fasthttp request body reading](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.PostBody).
|
||||
|
||||
* *Why fasthttp doesn't support HTTP/2.0 and WebSockets?*
|
||||
|
||||
There are [plans](TODO) for adding HTTP/2.0 and WebSockets support
|
||||
in the future.
|
||||
In the mean time, third parties may use [RequestCtx.Hijack](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.Hijack)
|
||||
for implementing these goodies. See [the first third-party websocket implementation on the top of fasthttp](https://github.com/leavengood/websocket).
|
||||
|
||||
* *Are there known net/http advantages comparing to fasthttp?*
|
||||
|
||||
Yes:
|
||||
* net/http supports [HTTP/2.0 starting from go1.6](https://http2.golang.org/).
|
||||
* net/http API is stable, while fasthttp API constantly evolves.
|
||||
* net/http handles more HTTP corner cases.
|
||||
* net/http should contain less bugs, since it is used and tested by much
|
||||
wider audience.
|
||||
* net/http works on Go older than 1.5.
|
||||
|
||||
* *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?*
|
||||
|
||||
Go1.5+. Older versions won't be supported, since their standard package
|
||||
[miss useful functions](https://github.com/valyala/fasthttp/issues/5).
|
||||
|
||||
* *Please provide real benchmark data and sever 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:
|
||||
|
||||
* [Iris](https://github.com/kataras/iris)
|
||||
* [fasthttp-routing](https://github.com/qiangxue/fasthttp-routing)
|
||||
* [fasthttprouter](https://github.com/buaazp/fasthttprouter)
|
||||
* [gramework](https://github.com/gramework/gramework)
|
||||
* [lu](https://github.com/vincentLiuxiang/lu)
|
||||
|
||||
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://godoc.org/github.com/valyala/fasthttp#RequestCtx)
|
||||
or to its' members after returning from [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler).
|
||||
* Make sure you call [TimeoutError](https://godoc.org/github.com/valyala/fasthttp#RequestCtx.TimeoutError)
|
||||
before returning from [RequestHandler](https://godoc.org/github.com/valyala/fasthttp#RequestHandler)
|
||||
if there are references to [RequestCtx](https://godoc.org/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).
|
||||
+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 .
|
||||
+517
@@ -0,0 +1,517 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// 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 AquireArgs 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() interface{} {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.Reset()
|
||||
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 next call to Args methods.
|
||||
func (a *Args) QueryString() []byte {
|
||||
a.buf = a.AppendBytes(a.buf[:0])
|
||||
return a.buf
|
||||
}
|
||||
|
||||
// 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 len(kv.value) > 0 {
|
||||
dst = append(dst, '=')
|
||||
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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// Set sets 'key=value' argument.
|
||||
func (a *Args) Set(key, value string) {
|
||||
a.args = setArg(a.args, key, value)
|
||||
}
|
||||
|
||||
// SetBytesK sets 'key=value' argument.
|
||||
func (a *Args) SetBytesK(key []byte, value string) {
|
||||
a.args = setArg(a.args, b2s(key), value)
|
||||
}
|
||||
|
||||
// SetBytesV sets 'key=value' argument.
|
||||
func (a *Args) SetBytesV(key string, value []byte) {
|
||||
a.args = setArg(a.args, key, b2s(value))
|
||||
}
|
||||
|
||||
// SetBytesKV sets 'key=value' argument.
|
||||
func (a *Args) SetBytesKV(key, value []byte) {
|
||||
a.args = setArgBytes(a.args, key, value)
|
||||
}
|
||||
|
||||
// Peek returns query arg value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next Args call.
|
||||
func (a *Args) Peek(key string) []byte {
|
||||
return peekArgStr(a.args, key)
|
||||
}
|
||||
|
||||
// PeekBytes returns query arg value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next Args call.
|
||||
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 := AcquireByteBuffer()
|
||||
bb.B = AppendUint(bb.B[:0], value)
|
||||
a.SetBytesV(key, bb.B)
|
||||
ReleaseByteBuffer(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', 'y' and 'yes' values,
|
||||
// otherwise false is returned.
|
||||
func (a *Args) GetBool(key string) bool {
|
||||
switch string(a.Peek(key)) {
|
||||
case "1", "y", "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 copyArgs(dst, src []argsKV) []argsKV {
|
||||
if cap(dst) < len(src) {
|
||||
tmp := make([]argsKV, len(src))
|
||||
copy(tmp, dst)
|
||||
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...)
|
||||
dstKV.value = append(dstKV.value[:0], srcKV.value...)
|
||||
}
|
||||
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--
|
||||
args[n] = tmp
|
||||
args = args[:n]
|
||||
}
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func setArgBytes(h []argsKV, key, value []byte) []argsKV {
|
||||
return setArg(h, b2s(key), b2s(value))
|
||||
}
|
||||
|
||||
func setArg(h []argsKV, key, value string) []argsKV {
|
||||
n := len(h)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &h[i]
|
||||
if key == string(kv.key) {
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
return h
|
||||
}
|
||||
}
|
||||
return appendArg(h, key, value)
|
||||
}
|
||||
|
||||
func appendArgBytes(h []argsKV, key, value []byte) []argsKV {
|
||||
return appendArg(h, b2s(key), b2s(value))
|
||||
}
|
||||
|
||||
func appendArg(args []argsKV, key, value string) []argsKV {
|
||||
var kv *argsKV
|
||||
args, kv = allocArg(args)
|
||||
kv.key = append(kv.key[:0], key...)
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
return args
|
||||
}
|
||||
|
||||
func allocArg(h []argsKV) ([]argsKV, *argsKV) {
|
||||
n := len(h)
|
||||
if cap(h) > n {
|
||||
h = h[:n+1]
|
||||
} else {
|
||||
h = append(h, argsKV{})
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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]
|
||||
} 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]
|
||||
} 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 {
|
||||
if bytes.IndexByte(src, '%') < 0 && bytes.IndexByte(src, '+') < 0 {
|
||||
// fast path: src doesn't contain encoded chars
|
||||
return append(dst, src...)
|
||||
}
|
||||
|
||||
// slow path
|
||||
for i := 0; 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 if c == '+' {
|
||||
dst = append(dst, ' ')
|
||||
} else {
|
||||
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 preformance
|
||||
// reasons only.
|
||||
func decodeArgAppendNoPlus(dst, src []byte) []byte {
|
||||
if bytes.IndexByte(src, '%') < 0 {
|
||||
// fast path: src doesn't contain encoded chars
|
||||
return append(dst, src...)
|
||||
}
|
||||
|
||||
// slow path
|
||||
for i := 0; 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
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// ByteBuffer provides byte buffer, which can be used with fasthttp API
|
||||
// in order to minimize memory allocations.
|
||||
//
|
||||
// ByteBuffer may be used with functions appending data to the given []byte
|
||||
// slice. See example code for details.
|
||||
//
|
||||
// Use AcquireByteBuffer for obtaining an empty byte buffer.
|
||||
//
|
||||
// ByteBuffer is deprecated. Use github.com/valyala/bytebufferpool instead.
|
||||
type ByteBuffer bytebufferpool.ByteBuffer
|
||||
|
||||
// Write implements io.Writer - it appends p to ByteBuffer.B
|
||||
func (b *ByteBuffer) Write(p []byte) (int, error) {
|
||||
return bb(b).Write(p)
|
||||
}
|
||||
|
||||
// WriteString appends s to ByteBuffer.B
|
||||
func (b *ByteBuffer) WriteString(s string) (int, error) {
|
||||
return bb(b).WriteString(s)
|
||||
}
|
||||
|
||||
// Set sets ByteBuffer.B to p
|
||||
func (b *ByteBuffer) Set(p []byte) {
|
||||
bb(b).Set(p)
|
||||
}
|
||||
|
||||
// SetString sets ByteBuffer.B to s
|
||||
func (b *ByteBuffer) SetString(s string) {
|
||||
bb(b).SetString(s)
|
||||
}
|
||||
|
||||
// Reset makes ByteBuffer.B empty.
|
||||
func (b *ByteBuffer) Reset() {
|
||||
bb(b).Reset()
|
||||
}
|
||||
|
||||
// AcquireByteBuffer returns an empty byte buffer from the pool.
|
||||
//
|
||||
// Acquired byte buffer may be returned to the pool via ReleaseByteBuffer call.
|
||||
// This reduces the number of memory allocations required for byte buffer
|
||||
// management.
|
||||
func AcquireByteBuffer() *ByteBuffer {
|
||||
return (*ByteBuffer)(defaultByteBufferPool.Get())
|
||||
}
|
||||
|
||||
// ReleaseByteBuffer returns byte buffer to the pool.
|
||||
//
|
||||
// ByteBuffer.B mustn't be touched after returning it to the pool.
|
||||
// Otherwise data races occur.
|
||||
func ReleaseByteBuffer(b *ByteBuffer) {
|
||||
defaultByteBufferPool.Put(bb(b))
|
||||
}
|
||||
|
||||
func bb(b *ByteBuffer) *bytebufferpool.ByteBuffer {
|
||||
return (*bytebufferpool.ByteBuffer)(b)
|
||||
}
|
||||
|
||||
var defaultByteBufferPool bytebufferpool.Pool
|
||||
+447
@@ -0,0 +1,447 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// AppendHTMLEscape appends html-escaped s to dst and returns the extended dst.
|
||||
func AppendHTMLEscape(dst []byte, s string) []byte {
|
||||
if strings.IndexByte(s, '<') < 0 &&
|
||||
strings.IndexByte(s, '>') < 0 &&
|
||||
strings.IndexByte(s, '"') < 0 &&
|
||||
strings.IndexByte(s, '\'') < 0 {
|
||||
|
||||
// fast path - nothing to escape
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// slow path
|
||||
var prev int
|
||||
var sub string
|
||||
for i, n := 0, len(s); i < n; i++ {
|
||||
sub = ""
|
||||
switch s[i] {
|
||||
case '<':
|
||||
sub = "<"
|
||||
case '>':
|
||||
sub = ">"
|
||||
case '"':
|
||||
sub = """
|
||||
case '\'':
|
||||
sub = "'"
|
||||
}
|
||||
if len(sub) > 0 {
|
||||
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 {
|
||||
dst = make([]byte, net.IPv4len)
|
||||
}
|
||||
copy(dst, net.IPv4zero)
|
||||
dst = dst.To4()
|
||||
if dst == nil {
|
||||
panic("BUG: dst must not be nil")
|
||||
}
|
||||
|
||||
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: %s", 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: %s", 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 {
|
||||
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 traling 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
|
||||
}
|
||||
if i >= maxIntChars {
|
||||
return -1, i, errTooLongInt
|
||||
}
|
||||
v = 10*v + int(k)
|
||||
}
|
||||
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
|
||||
var 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*int(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) {
|
||||
n := 0
|
||||
i := 0
|
||||
var k 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
|
||||
}
|
||||
r.UnreadByte()
|
||||
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 {
|
||||
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] = int2hexbyte(n & 0xf)
|
||||
n >>= 4
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
i--
|
||||
}
|
||||
_, err := w.Write(buf[i:])
|
||||
hexIntBufPool.Put(v)
|
||||
return err
|
||||
}
|
||||
|
||||
func int2hexbyte(n int) byte {
|
||||
if n < 10 {
|
||||
return '0' + byte(n)
|
||||
}
|
||||
return 'a' + byte(n) - 10
|
||||
}
|
||||
|
||||
func hexCharUpper(c byte) byte {
|
||||
if c < 10 {
|
||||
return '0' + c
|
||||
}
|
||||
return c - 10 + 'A'
|
||||
}
|
||||
|
||||
var hex2intTable = func() []byte {
|
||||
b := make([]byte, 256)
|
||||
for i := 0; i < 256; i++ {
|
||||
c := byte(16)
|
||||
if i >= '0' && i <= '9' {
|
||||
c = byte(i) - '0'
|
||||
} else if i >= 'a' && i <= 'f' {
|
||||
c = byte(i) - 'a' + 10
|
||||
} else if i >= 'A' && i <= 'F' {
|
||||
c = byte(i) - 'A' + 10
|
||||
}
|
||||
b[i] = c
|
||||
}
|
||||
return b
|
||||
}()
|
||||
|
||||
const toLower = 'a' - 'A'
|
||||
|
||||
var toLowerTable = func() [256]byte {
|
||||
var a [256]byte
|
||||
for i := 0; i < 256; i++ {
|
||||
c := byte(i)
|
||||
if c >= 'A' && c <= 'Z' {
|
||||
c += toLower
|
||||
}
|
||||
a[i] = c
|
||||
}
|
||||
return a
|
||||
}()
|
||||
|
||||
var toUpperTable = func() [256]byte {
|
||||
var a [256]byte
|
||||
for i := 0; i < 256; i++ {
|
||||
c := byte(i)
|
||||
if c >= 'a' && c <= 'z' {
|
||||
c -= toLower
|
||||
}
|
||||
a[i] = c
|
||||
}
|
||||
return a
|
||||
}()
|
||||
|
||||
func lowercaseBytes(b []byte) {
|
||||
for i := 0; i < len(b); i++ {
|
||||
p := &b[i]
|
||||
*p = toLowerTable[*p]
|
||||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
||||
// 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) []byte {
|
||||
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return *(*[]byte)(unsafe.Pointer(&bh))
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// See http://www.w3.org/TR/html5/forms.html#form-submission-algorithm
|
||||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
|
||||
c == '*' || c == '-' || c == '.' || c == '_' {
|
||||
dst = append(dst, c)
|
||||
} else {
|
||||
dst = append(dst, '%', hexCharUpper(c>>4), hexCharUpper(c&15))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendQuotedPath(dst, src []byte) []byte {
|
||||
for _, c := range src {
|
||||
if c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' ||
|
||||
c == '/' || c == '.' || c == ',' || c == '=' || c == ':' || c == '&' || c == '~' || c == '-' || c == '_' {
|
||||
dst = append(dst, c)
|
||||
} else {
|
||||
dst = append(dst, '%', hexCharUpper(c>>4), hexCharUpper(c&15))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// EqualBytesStr returns true if string(b) == s.
|
||||
//
|
||||
// This function has no performance benefits comparing to string(b) == s.
|
||||
// It is left here for backwards compatibility only.
|
||||
//
|
||||
// This function is deperecated and may be deleted soon.
|
||||
func EqualBytesStr(b []byte, s string) bool {
|
||||
return string(b) == s
|
||||
}
|
||||
|
||||
// AppendBytesStr appends src to dst and returns the extended dst.
|
||||
//
|
||||
// This function has no performance benefits comparing to append(dst, src...).
|
||||
// It is left here for backwards compatibility only.
|
||||
//
|
||||
// This function is deprecated and may be deleted soon.
|
||||
func AppendBytesStr(dst []byte, src string) []byte {
|
||||
return append(dst, src...)
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// +build !amd64,!arm64,!ppc64
|
||||
|
||||
package fasthttp
|
||||
|
||||
const (
|
||||
maxIntChars = 9
|
||||
maxHexIntChars = 7
|
||||
)
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
// +build amd64 arm64 ppc64
|
||||
|
||||
package fasthttp
|
||||
|
||||
const (
|
||||
maxIntChars = 18
|
||||
maxHexIntChars = 15
|
||||
)
|
||||
+2163
@@ -0,0 +1,2163 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Do performs the given http request and fills the given http response.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all DefaultMaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func Do(req *Request, resp *Response) error {
|
||||
return defaultClient.Do(req, resp)
|
||||
}
|
||||
|
||||
// DoTimeout performs the given request and waits for response during
|
||||
// the given timeout duration.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned during
|
||||
// the given timeout.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all DefaultMaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func DoTimeout(req *Request, resp *Response, timeout time.Duration) error {
|
||||
return defaultClient.DoTimeout(req, resp, timeout)
|
||||
}
|
||||
|
||||
// DoDeadline performs the given request and waits for response until
|
||||
// the given deadline.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned until
|
||||
// the given deadline.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all DefaultMaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
return defaultClient.DoDeadline(req, resp, deadline)
|
||||
}
|
||||
|
||||
// Get appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
func Get(dst []byte, url string) (statusCode int, body []byte, err error) {
|
||||
return defaultClient.Get(dst, url)
|
||||
}
|
||||
|
||||
// GetTimeout appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// during the given timeout.
|
||||
func GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) {
|
||||
return defaultClient.GetTimeout(dst, url, timeout)
|
||||
}
|
||||
|
||||
// GetDeadline appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// until the given deadline.
|
||||
func GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) {
|
||||
return defaultClient.GetDeadline(dst, url, deadline)
|
||||
}
|
||||
|
||||
// Post sends POST request to the given url with the given POST arguments.
|
||||
//
|
||||
// Response body is appended to dst, which is returned as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// Empty POST body is sent if postArgs is nil.
|
||||
func Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) {
|
||||
return defaultClient.Post(dst, url, postArgs)
|
||||
}
|
||||
|
||||
var defaultClient Client
|
||||
|
||||
// Client implements http client.
|
||||
//
|
||||
// Copying Client by value is prohibited. Create new instance instead.
|
||||
//
|
||||
// It is safe calling Client methods from concurrently running goroutines.
|
||||
type Client struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Client name. Used in User-Agent request header.
|
||||
//
|
||||
// Default client name is used if not set.
|
||||
Name string
|
||||
|
||||
// Callback for establishing new connections to hosts.
|
||||
//
|
||||
// Default Dial is used if not set.
|
||||
Dial DialFunc
|
||||
|
||||
// Attempt to connect to both ipv4 and ipv6 addresses if set to true.
|
||||
//
|
||||
// This option is used only if default TCP dialer is used,
|
||||
// i.e. if Dial is blank.
|
||||
//
|
||||
// By default client connects only to ipv4 addresses,
|
||||
// since unfortunately ipv6 remains broken in many networks worldwide :)
|
||||
DialDualStack bool
|
||||
|
||||
// TLS config for https connections.
|
||||
//
|
||||
// Default TLS config is used if not set.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Maximum number of connections per each host which may be established.
|
||||
//
|
||||
// DefaultMaxConnsPerHost is used if not set.
|
||||
MaxConnsPerHost int
|
||||
|
||||
// Idle keep-alive connections are closed after this duration.
|
||||
//
|
||||
// By default idle connections are closed
|
||||
// after DefaultMaxIdleConnDuration.
|
||||
MaxIdleConnDuration time.Duration
|
||||
|
||||
// Per-connection buffer size for responses' reading.
|
||||
// This also limits the maximum header size.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
ReadBufferSize int
|
||||
|
||||
// Per-connection buffer size for requests' writing.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
WriteBufferSize int
|
||||
|
||||
// Maximum duration for full response reading (including body).
|
||||
//
|
||||
// By default response read timeout is unlimited.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// Maximum duration for full request writing (including body).
|
||||
//
|
||||
// By default request write timeout is unlimited.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// Maximum response body size.
|
||||
//
|
||||
// The client returns ErrBodyTooLarge if this limit is greater than 0
|
||||
// and response body is greater than the limit.
|
||||
//
|
||||
// By default response body size is unlimited.
|
||||
MaxResponseBodySize int
|
||||
|
||||
// Header names are passed as-is without normalization
|
||||
// if this option is set.
|
||||
//
|
||||
// Disabled header names' normalization may be useful only for proxying
|
||||
// responses to other clients expecting case-sensitive
|
||||
// header names. See https://github.com/valyala/fasthttp/issues/57
|
||||
// for details.
|
||||
//
|
||||
// By default request and response header names are normalized, i.e.
|
||||
// The first letter and the first letters following dashes
|
||||
// are uppercased, while all the other letters are lowercased.
|
||||
// Examples:
|
||||
//
|
||||
// * HOST -> Host
|
||||
// * content-type -> Content-Type
|
||||
// * cONTENT-lenGTH -> Content-Length
|
||||
DisableHeaderNamesNormalizing bool
|
||||
|
||||
mLock sync.Mutex
|
||||
m map[string]*HostClient
|
||||
ms map[string]*HostClient
|
||||
}
|
||||
|
||||
// Get appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
func (c *Client) Get(dst []byte, url string) (statusCode int, body []byte, err error) {
|
||||
return clientGetURL(dst, url, c)
|
||||
}
|
||||
|
||||
// GetTimeout appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// during the given timeout.
|
||||
func (c *Client) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) {
|
||||
return clientGetURLTimeout(dst, url, timeout, c)
|
||||
}
|
||||
|
||||
// GetDeadline appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// until the given deadline.
|
||||
func (c *Client) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) {
|
||||
return clientGetURLDeadline(dst, url, deadline, c)
|
||||
}
|
||||
|
||||
// Post sends POST request to the given url with the given POST arguments.
|
||||
//
|
||||
// Response body is appended to dst, which is returned as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// Empty POST body is sent if postArgs is nil.
|
||||
func (c *Client) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) {
|
||||
return clientPostURL(dst, url, postArgs, c)
|
||||
}
|
||||
|
||||
// DoTimeout performs the given request and waits for response during
|
||||
// the given timeout duration.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned during
|
||||
// the given timeout.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *Client) DoTimeout(req *Request, resp *Response, timeout time.Duration) error {
|
||||
return clientDoTimeout(req, resp, timeout, c)
|
||||
}
|
||||
|
||||
// DoDeadline performs the given request and waits for response until
|
||||
// the given deadline.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned until
|
||||
// the given deadline.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *Client) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
return clientDoDeadline(req, resp, deadline, c)
|
||||
}
|
||||
|
||||
// Do performs the given http request and fills the given http response.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// Client determines the server to be requested in the following order:
|
||||
//
|
||||
// - from RequestURI if it contains full url with scheme and host;
|
||||
// - from Host header otherwise.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all Client.MaxConnsPerHost connections
|
||||
// to the requested host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *Client) Do(req *Request, resp *Response) error {
|
||||
uri := req.URI()
|
||||
host := uri.Host()
|
||||
|
||||
isTLS := false
|
||||
scheme := uri.Scheme()
|
||||
if bytes.Equal(scheme, strHTTPS) {
|
||||
isTLS = true
|
||||
} else if !bytes.Equal(scheme, strHTTP) {
|
||||
return fmt.Errorf("unsupported protocol %q. http and https are supported", scheme)
|
||||
}
|
||||
|
||||
startCleaner := false
|
||||
|
||||
c.mLock.Lock()
|
||||
m := c.m
|
||||
if isTLS {
|
||||
m = c.ms
|
||||
}
|
||||
if m == nil {
|
||||
m = make(map[string]*HostClient)
|
||||
if isTLS {
|
||||
c.ms = m
|
||||
} else {
|
||||
c.m = m
|
||||
}
|
||||
}
|
||||
hc := m[string(host)]
|
||||
if hc == nil {
|
||||
hc = &HostClient{
|
||||
Addr: addMissingPort(string(host), isTLS),
|
||||
Name: c.Name,
|
||||
Dial: c.Dial,
|
||||
DialDualStack: c.DialDualStack,
|
||||
IsTLS: isTLS,
|
||||
TLSConfig: c.TLSConfig,
|
||||
MaxConns: c.MaxConnsPerHost,
|
||||
MaxIdleConnDuration: c.MaxIdleConnDuration,
|
||||
ReadBufferSize: c.ReadBufferSize,
|
||||
WriteBufferSize: c.WriteBufferSize,
|
||||
ReadTimeout: c.ReadTimeout,
|
||||
WriteTimeout: c.WriteTimeout,
|
||||
MaxResponseBodySize: c.MaxResponseBodySize,
|
||||
DisableHeaderNamesNormalizing: c.DisableHeaderNamesNormalizing,
|
||||
}
|
||||
m[string(host)] = hc
|
||||
if len(m) == 1 {
|
||||
startCleaner = true
|
||||
}
|
||||
}
|
||||
c.mLock.Unlock()
|
||||
|
||||
if startCleaner {
|
||||
go c.mCleaner(m)
|
||||
}
|
||||
|
||||
return hc.Do(req, resp)
|
||||
}
|
||||
|
||||
func (c *Client) mCleaner(m map[string]*HostClient) {
|
||||
mustStop := false
|
||||
for {
|
||||
t := time.Now()
|
||||
c.mLock.Lock()
|
||||
for k, v := range m {
|
||||
if t.Sub(v.LastUseTime()) > time.Minute {
|
||||
delete(m, k)
|
||||
}
|
||||
}
|
||||
if len(m) == 0 {
|
||||
mustStop = true
|
||||
}
|
||||
c.mLock.Unlock()
|
||||
|
||||
if mustStop {
|
||||
break
|
||||
}
|
||||
time.Sleep(10 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultMaxConnsPerHost is the maximum number of concurrent connections
|
||||
// http client may establish per host by default (i.e. if
|
||||
// Client.MaxConnsPerHost isn't set).
|
||||
const DefaultMaxConnsPerHost = 512
|
||||
|
||||
// DefaultMaxIdleConnDuration is the default duration before idle keep-alive
|
||||
// connection is closed.
|
||||
const DefaultMaxIdleConnDuration = 10 * time.Second
|
||||
|
||||
// DialFunc must establish connection to addr.
|
||||
//
|
||||
// There is no need in establishing TLS (SSL) connection for https.
|
||||
// The client automatically converts connection to TLS
|
||||
// if HostClient.IsTLS is set.
|
||||
//
|
||||
// TCP address passed to DialFunc always contains host and port.
|
||||
// Example TCP addr values:
|
||||
//
|
||||
// - foobar.com:80
|
||||
// - foobar.com:443
|
||||
// - foobar.com:8080
|
||||
type DialFunc func(addr string) (net.Conn, error)
|
||||
|
||||
// HostClient balances http requests among hosts listed in Addr.
|
||||
//
|
||||
// HostClient may be used for balancing load among multiple upstream hosts.
|
||||
// While multiple addresses passed to HostClient.Addr may be used for balancing
|
||||
// load among them, it would be better using LBClient instead, since HostClient
|
||||
// may unevenly balance load among upstream hosts.
|
||||
//
|
||||
// It is forbidden copying HostClient instances. Create new instances instead.
|
||||
//
|
||||
// It is safe calling HostClient methods from concurrently running goroutines.
|
||||
type HostClient struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Comma-separated list of upstream HTTP server host addresses,
|
||||
// which are passed to Dial in a round-robin manner.
|
||||
//
|
||||
// Each address may contain port if default dialer is used.
|
||||
// For example,
|
||||
//
|
||||
// - foobar.com:80
|
||||
// - foobar.com:443
|
||||
// - foobar.com:8080
|
||||
Addr string
|
||||
|
||||
// Client name. Used in User-Agent request header.
|
||||
Name string
|
||||
|
||||
// Callback for establishing new connection to the host.
|
||||
//
|
||||
// Default Dial is used if not set.
|
||||
Dial DialFunc
|
||||
|
||||
// Attempt to connect to both ipv4 and ipv6 host addresses
|
||||
// if set to true.
|
||||
//
|
||||
// This option is used only if default TCP dialer is used,
|
||||
// i.e. if Dial is blank.
|
||||
//
|
||||
// By default client connects only to ipv4 addresses,
|
||||
// since unfortunately ipv6 remains broken in many networks worldwide :)
|
||||
DialDualStack bool
|
||||
|
||||
// Whether to use TLS (aka SSL or HTTPS) for host connections.
|
||||
IsTLS bool
|
||||
|
||||
// Optional TLS config.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Maximum number of connections which may be established to all hosts
|
||||
// listed in Addr.
|
||||
//
|
||||
// DefaultMaxConnsPerHost is used if not set.
|
||||
MaxConns int
|
||||
|
||||
// Keep-alive connections are closed after this duration.
|
||||
//
|
||||
// By default connection duration is unlimited.
|
||||
MaxConnDuration time.Duration
|
||||
|
||||
// Idle keep-alive connections are closed after this duration.
|
||||
//
|
||||
// By default idle connections are closed
|
||||
// after DefaultMaxIdleConnDuration.
|
||||
MaxIdleConnDuration time.Duration
|
||||
|
||||
// Per-connection buffer size for responses' reading.
|
||||
// This also limits the maximum header size.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
ReadBufferSize int
|
||||
|
||||
// Per-connection buffer size for requests' writing.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
WriteBufferSize int
|
||||
|
||||
// Maximum duration for full response reading (including body).
|
||||
//
|
||||
// By default response read timeout is unlimited.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// Maximum duration for full request writing (including body).
|
||||
//
|
||||
// By default request write timeout is unlimited.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// Maximum response body size.
|
||||
//
|
||||
// The client returns ErrBodyTooLarge if this limit is greater than 0
|
||||
// and response body is greater than the limit.
|
||||
//
|
||||
// By default response body size is unlimited.
|
||||
MaxResponseBodySize int
|
||||
|
||||
// Header names are passed as-is without normalization
|
||||
// if this option is set.
|
||||
//
|
||||
// Disabled header names' normalization may be useful only for proxying
|
||||
// responses to other clients expecting case-sensitive
|
||||
// header names. See https://github.com/valyala/fasthttp/issues/57
|
||||
// for details.
|
||||
//
|
||||
// By default request and response header names are normalized, i.e.
|
||||
// The first letter and the first letters following dashes
|
||||
// are uppercased, while all the other letters are lowercased.
|
||||
// Examples:
|
||||
//
|
||||
// * HOST -> Host
|
||||
// * content-type -> Content-Type
|
||||
// * cONTENT-lenGTH -> Content-Length
|
||||
DisableHeaderNamesNormalizing bool
|
||||
|
||||
clientName atomic.Value
|
||||
lastUseTime uint32
|
||||
|
||||
connsLock sync.Mutex
|
||||
connsCount int
|
||||
conns []*clientConn
|
||||
|
||||
addrsLock sync.Mutex
|
||||
addrs []string
|
||||
addrIdx uint32
|
||||
|
||||
tlsConfigMap map[string]*tls.Config
|
||||
tlsConfigMapLock sync.Mutex
|
||||
|
||||
readerPool sync.Pool
|
||||
writerPool sync.Pool
|
||||
|
||||
pendingRequests uint64
|
||||
|
||||
connsCleanerRun bool
|
||||
}
|
||||
|
||||
type clientConn struct {
|
||||
c net.Conn
|
||||
|
||||
createdTime time.Time
|
||||
lastUseTime time.Time
|
||||
|
||||
lastReadDeadlineTime time.Time
|
||||
lastWriteDeadlineTime time.Time
|
||||
}
|
||||
|
||||
var startTimeUnix = time.Now().Unix()
|
||||
|
||||
// LastUseTime returns time the client was last used
|
||||
func (c *HostClient) LastUseTime() time.Time {
|
||||
n := atomic.LoadUint32(&c.lastUseTime)
|
||||
return time.Unix(startTimeUnix+int64(n), 0)
|
||||
}
|
||||
|
||||
// Get appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
func (c *HostClient) Get(dst []byte, url string) (statusCode int, body []byte, err error) {
|
||||
return clientGetURL(dst, url, c)
|
||||
}
|
||||
|
||||
// GetTimeout appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// during the given timeout.
|
||||
func (c *HostClient) GetTimeout(dst []byte, url string, timeout time.Duration) (statusCode int, body []byte, err error) {
|
||||
return clientGetURLTimeout(dst, url, timeout, c)
|
||||
}
|
||||
|
||||
// GetDeadline appends url contents to dst and returns it as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// ErrTimeout error is returned if url contents couldn't be fetched
|
||||
// until the given deadline.
|
||||
func (c *HostClient) GetDeadline(dst []byte, url string, deadline time.Time) (statusCode int, body []byte, err error) {
|
||||
return clientGetURLDeadline(dst, url, deadline, c)
|
||||
}
|
||||
|
||||
// Post sends POST request to the given url with the given POST arguments.
|
||||
//
|
||||
// Response body is appended to dst, which is returned as body.
|
||||
//
|
||||
// The function follows redirects. Use Do* for manually handling redirects.
|
||||
//
|
||||
// New body buffer is allocated if dst is nil.
|
||||
//
|
||||
// Empty POST body is sent if postArgs is nil.
|
||||
func (c *HostClient) Post(dst []byte, url string, postArgs *Args) (statusCode int, body []byte, err error) {
|
||||
return clientPostURL(dst, url, postArgs, c)
|
||||
}
|
||||
|
||||
type clientDoer interface {
|
||||
Do(req *Request, resp *Response) error
|
||||
}
|
||||
|
||||
func clientGetURL(dst []byte, url string, c clientDoer) (statusCode int, body []byte, err error) {
|
||||
req := AcquireRequest()
|
||||
|
||||
statusCode, body, err = doRequestFollowRedirects(req, dst, url, c)
|
||||
|
||||
ReleaseRequest(req)
|
||||
return statusCode, body, err
|
||||
}
|
||||
|
||||
func clientGetURLTimeout(dst []byte, url string, timeout time.Duration, c clientDoer) (statusCode int, body []byte, err error) {
|
||||
deadline := time.Now().Add(timeout)
|
||||
return clientGetURLDeadline(dst, url, deadline, c)
|
||||
}
|
||||
|
||||
type clientURLResponse struct {
|
||||
statusCode int
|
||||
body []byte
|
||||
err error
|
||||
}
|
||||
|
||||
func clientGetURLDeadline(dst []byte, url string, deadline time.Time, c clientDoer) (statusCode int, body []byte, err error) {
|
||||
timeout := -time.Since(deadline)
|
||||
if timeout <= 0 {
|
||||
return 0, dst, ErrTimeout
|
||||
}
|
||||
|
||||
var ch chan clientURLResponse
|
||||
chv := clientURLResponseChPool.Get()
|
||||
if chv == nil {
|
||||
chv = make(chan clientURLResponse, 1)
|
||||
}
|
||||
ch = chv.(chan clientURLResponse)
|
||||
|
||||
req := AcquireRequest()
|
||||
|
||||
// Note that the request continues execution on ErrTimeout until
|
||||
// client-specific ReadTimeout exceeds. This helps limiting load
|
||||
// on slow hosts by MaxConns* concurrent requests.
|
||||
//
|
||||
// Without this 'hack' the load on slow host could exceed MaxConns*
|
||||
// concurrent requests, since timed out requests on client side
|
||||
// usually continue execution on the host.
|
||||
go func() {
|
||||
statusCodeCopy, bodyCopy, errCopy := doRequestFollowRedirects(req, dst, url, c)
|
||||
ch <- clientURLResponse{
|
||||
statusCode: statusCodeCopy,
|
||||
body: bodyCopy,
|
||||
err: errCopy,
|
||||
}
|
||||
}()
|
||||
|
||||
tc := acquireTimer(timeout)
|
||||
select {
|
||||
case resp := <-ch:
|
||||
ReleaseRequest(req)
|
||||
clientURLResponseChPool.Put(chv)
|
||||
statusCode = resp.statusCode
|
||||
body = resp.body
|
||||
err = resp.err
|
||||
case <-tc.C:
|
||||
body = dst
|
||||
err = ErrTimeout
|
||||
}
|
||||
releaseTimer(tc)
|
||||
|
||||
return statusCode, body, err
|
||||
}
|
||||
|
||||
var clientURLResponseChPool sync.Pool
|
||||
|
||||
func clientPostURL(dst []byte, url string, postArgs *Args, c clientDoer) (statusCode int, body []byte, err error) {
|
||||
req := AcquireRequest()
|
||||
req.Header.SetMethodBytes(strPost)
|
||||
req.Header.SetContentTypeBytes(strPostArgsContentType)
|
||||
if postArgs != nil {
|
||||
postArgs.WriteTo(req.BodyWriter())
|
||||
}
|
||||
|
||||
statusCode, body, err = doRequestFollowRedirects(req, dst, url, c)
|
||||
|
||||
ReleaseRequest(req)
|
||||
return statusCode, body, err
|
||||
}
|
||||
|
||||
var (
|
||||
errMissingLocation = errors.New("missing Location header for http redirect")
|
||||
errTooManyRedirects = errors.New("too many redirects detected when doing the request")
|
||||
)
|
||||
|
||||
const maxRedirectsCount = 16
|
||||
|
||||
func doRequestFollowRedirects(req *Request, dst []byte, url string, c clientDoer) (statusCode int, body []byte, err error) {
|
||||
resp := AcquireResponse()
|
||||
bodyBuf := resp.bodyBuffer()
|
||||
resp.keepBodyBuffer = true
|
||||
oldBody := bodyBuf.B
|
||||
bodyBuf.B = dst
|
||||
|
||||
redirectsCount := 0
|
||||
for {
|
||||
req.parsedURI = false
|
||||
req.Header.host = req.Header.host[:0]
|
||||
req.SetRequestURI(url)
|
||||
|
||||
if err = c.Do(req, resp); err != nil {
|
||||
break
|
||||
}
|
||||
statusCode = resp.Header.StatusCode()
|
||||
if statusCode != StatusMovedPermanently && statusCode != StatusFound && statusCode != StatusSeeOther {
|
||||
break
|
||||
}
|
||||
|
||||
redirectsCount++
|
||||
if redirectsCount > maxRedirectsCount {
|
||||
err = errTooManyRedirects
|
||||
break
|
||||
}
|
||||
location := resp.Header.peek(strLocation)
|
||||
if len(location) == 0 {
|
||||
err = errMissingLocation
|
||||
break
|
||||
}
|
||||
url = getRedirectURL(url, location)
|
||||
}
|
||||
|
||||
body = bodyBuf.B
|
||||
bodyBuf.B = oldBody
|
||||
resp.keepBodyBuffer = false
|
||||
ReleaseResponse(resp)
|
||||
|
||||
return statusCode, body, err
|
||||
}
|
||||
|
||||
func getRedirectURL(baseURL string, location []byte) string {
|
||||
u := AcquireURI()
|
||||
u.Update(baseURL)
|
||||
u.UpdateBytes(location)
|
||||
redirectURL := u.String()
|
||||
ReleaseURI(u)
|
||||
return redirectURL
|
||||
}
|
||||
|
||||
var (
|
||||
requestPool sync.Pool
|
||||
responsePool sync.Pool
|
||||
)
|
||||
|
||||
// AcquireRequest returns an empty Request instance from request pool.
|
||||
//
|
||||
// The returned Request instance may be passed to ReleaseRequest when it is
|
||||
// no longer needed. This allows Request recycling, reduces GC pressure
|
||||
// and usually improves performance.
|
||||
func AcquireRequest() *Request {
|
||||
v := requestPool.Get()
|
||||
if v == nil {
|
||||
return &Request{}
|
||||
}
|
||||
return v.(*Request)
|
||||
}
|
||||
|
||||
// ReleaseRequest returns req acquired via AcquireRequest to request pool.
|
||||
//
|
||||
// It is forbidden accessing req and/or its' members after returning
|
||||
// it to request pool.
|
||||
func ReleaseRequest(req *Request) {
|
||||
req.Reset()
|
||||
requestPool.Put(req)
|
||||
}
|
||||
|
||||
// AcquireResponse returns an empty Response instance from response pool.
|
||||
//
|
||||
// The returned Response instance may be passed to ReleaseResponse when it is
|
||||
// no longer needed. This allows Response recycling, reduces GC pressure
|
||||
// and usually improves performance.
|
||||
func AcquireResponse() *Response {
|
||||
v := responsePool.Get()
|
||||
if v == nil {
|
||||
return &Response{}
|
||||
}
|
||||
return v.(*Response)
|
||||
}
|
||||
|
||||
// ReleaseResponse return resp acquired via AcquireResponse to response pool.
|
||||
//
|
||||
// It is forbidden accessing resp and/or its' members after returning
|
||||
// it to response pool.
|
||||
func ReleaseResponse(resp *Response) {
|
||||
resp.Reset()
|
||||
responsePool.Put(resp)
|
||||
}
|
||||
|
||||
// DoTimeout performs the given request and waits for response during
|
||||
// the given timeout duration.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned during
|
||||
// the given timeout.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all HostClient.MaxConns connections
|
||||
// to the host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *HostClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error {
|
||||
return clientDoTimeout(req, resp, timeout, c)
|
||||
}
|
||||
|
||||
// DoDeadline performs the given request and waits for response until
|
||||
// the given deadline.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned until
|
||||
// the given deadline.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all HostClient.MaxConns connections
|
||||
// to the host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *HostClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
return clientDoDeadline(req, resp, deadline, c)
|
||||
}
|
||||
|
||||
func clientDoTimeout(req *Request, resp *Response, timeout time.Duration, c clientDoer) error {
|
||||
deadline := time.Now().Add(timeout)
|
||||
return clientDoDeadline(req, resp, deadline, c)
|
||||
}
|
||||
|
||||
func clientDoDeadline(req *Request, resp *Response, deadline time.Time, c clientDoer) error {
|
||||
timeout := -time.Since(deadline)
|
||||
if timeout <= 0 {
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
var ch chan error
|
||||
chv := errorChPool.Get()
|
||||
if chv == nil {
|
||||
chv = make(chan error, 1)
|
||||
}
|
||||
ch = chv.(chan error)
|
||||
|
||||
// Make req and resp copies, since on timeout they no longer
|
||||
// may be accessed.
|
||||
reqCopy := AcquireRequest()
|
||||
req.copyToSkipBody(reqCopy)
|
||||
swapRequestBody(req, reqCopy)
|
||||
respCopy := AcquireResponse()
|
||||
|
||||
// Note that the request continues execution on ErrTimeout until
|
||||
// client-specific ReadTimeout exceeds. This helps limiting load
|
||||
// on slow hosts by MaxConns* concurrent requests.
|
||||
//
|
||||
// Without this 'hack' the load on slow host could exceed MaxConns*
|
||||
// concurrent requests, since timed out requests on client side
|
||||
// usually continue execution on the host.
|
||||
go func() {
|
||||
ch <- c.Do(reqCopy, respCopy)
|
||||
}()
|
||||
|
||||
tc := acquireTimer(timeout)
|
||||
var err error
|
||||
select {
|
||||
case err = <-ch:
|
||||
if resp != nil {
|
||||
respCopy.copyToSkipBody(resp)
|
||||
swapResponseBody(resp, respCopy)
|
||||
}
|
||||
swapRequestBody(reqCopy, req)
|
||||
ReleaseResponse(respCopy)
|
||||
ReleaseRequest(reqCopy)
|
||||
errorChPool.Put(chv)
|
||||
case <-tc.C:
|
||||
err = ErrTimeout
|
||||
}
|
||||
releaseTimer(tc)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
var errorChPool sync.Pool
|
||||
|
||||
// Do performs the given http request and sets the corresponding response.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrNoFreeConns is returned if all HostClient.MaxConns connections
|
||||
// to the host are busy.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *HostClient) Do(req *Request, resp *Response) error {
|
||||
var err error
|
||||
var retry bool
|
||||
const maxAttempts = 5
|
||||
attempts := 0
|
||||
|
||||
atomic.AddUint64(&c.pendingRequests, 1)
|
||||
for {
|
||||
retry, err = c.do(req, resp)
|
||||
if err == nil || !retry {
|
||||
break
|
||||
}
|
||||
|
||||
if !isIdempotent(req) {
|
||||
// Retry non-idempotent requests if the server closes
|
||||
// the connection before sending the response.
|
||||
//
|
||||
// This case is possible if the server closes the idle
|
||||
// keep-alive connection on timeout.
|
||||
//
|
||||
// Apache and nginx usually do this.
|
||||
if err != io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
attempts++
|
||||
if attempts >= maxAttempts {
|
||||
break
|
||||
}
|
||||
}
|
||||
atomic.AddUint64(&c.pendingRequests, ^uint64(0))
|
||||
|
||||
if err == io.EOF {
|
||||
err = ErrConnectionClosed
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// PendingRequests returns the current number of requests the client
|
||||
// is executing.
|
||||
//
|
||||
// This function may be used for balancing load among multiple HostClient
|
||||
// instances.
|
||||
func (c *HostClient) PendingRequests() int {
|
||||
return int(atomic.LoadUint64(&c.pendingRequests))
|
||||
}
|
||||
|
||||
func isIdempotent(req *Request) bool {
|
||||
return req.Header.IsGet() || req.Header.IsHead() || req.Header.IsPut()
|
||||
}
|
||||
|
||||
func (c *HostClient) do(req *Request, resp *Response) (bool, error) {
|
||||
nilResp := false
|
||||
if resp == nil {
|
||||
nilResp = true
|
||||
resp = AcquireResponse()
|
||||
}
|
||||
|
||||
ok, err := c.doNonNilReqResp(req, resp)
|
||||
|
||||
if nilResp {
|
||||
ReleaseResponse(resp)
|
||||
}
|
||||
|
||||
return ok, err
|
||||
}
|
||||
|
||||
func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) {
|
||||
if req == nil {
|
||||
panic("BUG: req cannot be nil")
|
||||
}
|
||||
if resp == nil {
|
||||
panic("BUG: resp cannot be nil")
|
||||
}
|
||||
|
||||
atomic.StoreUint32(&c.lastUseTime, uint32(CoarseTimeNow().Unix()-startTimeUnix))
|
||||
|
||||
// Free up resources occupied by response before sending the request,
|
||||
// so the GC may reclaim these resources (e.g. response body).
|
||||
resp.Reset()
|
||||
|
||||
cc, err := c.acquireConn()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
conn := cc.c
|
||||
|
||||
if c.WriteTimeout > 0 {
|
||||
// Optimization: update write deadline only if more than 25%
|
||||
// of the last write deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
currentTime := CoarseTimeNow()
|
||||
if currentTime.Sub(cc.lastWriteDeadlineTime) > (c.WriteTimeout >> 2) {
|
||||
if err = conn.SetWriteDeadline(currentTime.Add(c.WriteTimeout)); err != nil {
|
||||
c.closeConn(cc)
|
||||
return true, err
|
||||
}
|
||||
cc.lastWriteDeadlineTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
resetConnection := false
|
||||
if c.MaxConnDuration > 0 && time.Since(cc.createdTime) > c.MaxConnDuration && !req.ConnectionClose() {
|
||||
req.SetConnectionClose()
|
||||
resetConnection = true
|
||||
}
|
||||
|
||||
userAgentOld := req.Header.UserAgent()
|
||||
if len(userAgentOld) == 0 {
|
||||
req.Header.userAgent = c.getClientName()
|
||||
}
|
||||
bw := c.acquireWriter(conn)
|
||||
err = req.Write(bw)
|
||||
if len(userAgentOld) == 0 {
|
||||
req.Header.userAgent = userAgentOld
|
||||
}
|
||||
|
||||
if resetConnection {
|
||||
req.Header.ResetConnectionClose()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
err = bw.Flush()
|
||||
}
|
||||
if err != nil {
|
||||
c.releaseWriter(bw)
|
||||
c.closeConn(cc)
|
||||
return true, err
|
||||
}
|
||||
c.releaseWriter(bw)
|
||||
|
||||
if c.ReadTimeout > 0 {
|
||||
// Optimization: update read deadline only if more than 25%
|
||||
// of the last read deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
currentTime := CoarseTimeNow()
|
||||
if currentTime.Sub(cc.lastReadDeadlineTime) > (c.ReadTimeout >> 2) {
|
||||
if err = conn.SetReadDeadline(currentTime.Add(c.ReadTimeout)); err != nil {
|
||||
c.closeConn(cc)
|
||||
return true, err
|
||||
}
|
||||
cc.lastReadDeadlineTime = currentTime
|
||||
}
|
||||
}
|
||||
|
||||
if !req.Header.IsGet() && req.Header.IsHead() {
|
||||
resp.SkipBody = true
|
||||
}
|
||||
if c.DisableHeaderNamesNormalizing {
|
||||
resp.Header.DisableNormalizing()
|
||||
}
|
||||
|
||||
br := c.acquireReader(conn)
|
||||
if err = resp.ReadLimitBody(br, c.MaxResponseBodySize); err != nil {
|
||||
c.releaseReader(br)
|
||||
c.closeConn(cc)
|
||||
return true, err
|
||||
}
|
||||
c.releaseReader(br)
|
||||
|
||||
if resetConnection || req.ConnectionClose() || resp.ConnectionClose() {
|
||||
c.closeConn(cc)
|
||||
} else {
|
||||
c.releaseConn(cc)
|
||||
}
|
||||
|
||||
return false, err
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrNoFreeConns is returned when no free connections available
|
||||
// to the given host.
|
||||
//
|
||||
// Increase the allowed number of connections per host if you
|
||||
// see this error.
|
||||
ErrNoFreeConns = errors.New("no free connections available to host")
|
||||
|
||||
// ErrTimeout is returned from timed out calls.
|
||||
ErrTimeout = errors.New("timeout")
|
||||
|
||||
// ErrConnectionClosed may be returned from client methods if the server
|
||||
// closes connection before returning the first response byte.
|
||||
//
|
||||
// If you see this error, then either fix the server by returning
|
||||
// 'Connection: close' response header before closing the connection
|
||||
// or add 'Connection: close' request header before sending requests
|
||||
// to broken server.
|
||||
ErrConnectionClosed = errors.New("the server closed connection before returning the first response byte. " +
|
||||
"Make sure the server returns 'Connection: close' response header before closing the connection")
|
||||
)
|
||||
|
||||
func (c *HostClient) acquireConn() (*clientConn, error) {
|
||||
var cc *clientConn
|
||||
createConn := false
|
||||
startCleaner := false
|
||||
|
||||
var n int
|
||||
c.connsLock.Lock()
|
||||
n = len(c.conns)
|
||||
if n == 0 {
|
||||
maxConns := c.MaxConns
|
||||
if maxConns <= 0 {
|
||||
maxConns = DefaultMaxConnsPerHost
|
||||
}
|
||||
if c.connsCount < maxConns {
|
||||
c.connsCount++
|
||||
createConn = true
|
||||
if !c.connsCleanerRun {
|
||||
startCleaner = true
|
||||
c.connsCleanerRun = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
n--
|
||||
cc = c.conns[n]
|
||||
c.conns[n] = nil
|
||||
c.conns = c.conns[:n]
|
||||
}
|
||||
c.connsLock.Unlock()
|
||||
|
||||
if cc != nil {
|
||||
return cc, nil
|
||||
}
|
||||
if !createConn {
|
||||
return nil, ErrNoFreeConns
|
||||
}
|
||||
|
||||
if startCleaner {
|
||||
go c.connsCleaner()
|
||||
}
|
||||
|
||||
conn, err := c.dialHostHard()
|
||||
if err != nil {
|
||||
c.decConnsCount()
|
||||
return nil, err
|
||||
}
|
||||
cc = acquireClientConn(conn)
|
||||
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
func (c *HostClient) connsCleaner() {
|
||||
var (
|
||||
scratch []*clientConn
|
||||
maxIdleConnDuration = c.MaxIdleConnDuration
|
||||
)
|
||||
if maxIdleConnDuration <= 0 {
|
||||
maxIdleConnDuration = DefaultMaxIdleConnDuration
|
||||
}
|
||||
for {
|
||||
currentTime := time.Now()
|
||||
|
||||
// Determine idle connections to be closed.
|
||||
c.connsLock.Lock()
|
||||
conns := c.conns
|
||||
n := len(conns)
|
||||
i := 0
|
||||
for i < n && currentTime.Sub(conns[i].lastUseTime) > maxIdleConnDuration {
|
||||
i++
|
||||
}
|
||||
scratch = append(scratch[:0], conns[:i]...)
|
||||
if i > 0 {
|
||||
m := copy(conns, conns[i:])
|
||||
for i = m; i < n; i++ {
|
||||
conns[i] = nil
|
||||
}
|
||||
c.conns = conns[:m]
|
||||
}
|
||||
c.connsLock.Unlock()
|
||||
|
||||
// Close idle connections.
|
||||
for i, cc := range scratch {
|
||||
c.closeConn(cc)
|
||||
scratch[i] = nil
|
||||
}
|
||||
|
||||
// Determine whether to stop the connsCleaner.
|
||||
c.connsLock.Lock()
|
||||
mustStop := c.connsCount == 0
|
||||
if mustStop {
|
||||
c.connsCleanerRun = false
|
||||
}
|
||||
c.connsLock.Unlock()
|
||||
if mustStop {
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(maxIdleConnDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *HostClient) closeConn(cc *clientConn) {
|
||||
c.decConnsCount()
|
||||
cc.c.Close()
|
||||
releaseClientConn(cc)
|
||||
}
|
||||
|
||||
func (c *HostClient) decConnsCount() {
|
||||
c.connsLock.Lock()
|
||||
c.connsCount--
|
||||
c.connsLock.Unlock()
|
||||
}
|
||||
|
||||
func acquireClientConn(conn net.Conn) *clientConn {
|
||||
v := clientConnPool.Get()
|
||||
if v == nil {
|
||||
v = &clientConn{}
|
||||
}
|
||||
cc := v.(*clientConn)
|
||||
cc.c = conn
|
||||
cc.createdTime = CoarseTimeNow()
|
||||
return cc
|
||||
}
|
||||
|
||||
func releaseClientConn(cc *clientConn) {
|
||||
cc.c = nil
|
||||
clientConnPool.Put(cc)
|
||||
}
|
||||
|
||||
var clientConnPool sync.Pool
|
||||
|
||||
func (c *HostClient) releaseConn(cc *clientConn) {
|
||||
cc.lastUseTime = CoarseTimeNow()
|
||||
c.connsLock.Lock()
|
||||
c.conns = append(c.conns, cc)
|
||||
c.connsLock.Unlock()
|
||||
}
|
||||
|
||||
func (c *HostClient) acquireWriter(conn net.Conn) *bufio.Writer {
|
||||
v := c.writerPool.Get()
|
||||
if v == nil {
|
||||
n := c.WriteBufferSize
|
||||
if n <= 0 {
|
||||
n = defaultWriteBufferSize
|
||||
}
|
||||
return bufio.NewWriterSize(conn, n)
|
||||
}
|
||||
bw := v.(*bufio.Writer)
|
||||
bw.Reset(conn)
|
||||
return bw
|
||||
}
|
||||
|
||||
func (c *HostClient) releaseWriter(bw *bufio.Writer) {
|
||||
c.writerPool.Put(bw)
|
||||
}
|
||||
|
||||
func (c *HostClient) acquireReader(conn net.Conn) *bufio.Reader {
|
||||
v := c.readerPool.Get()
|
||||
if v == nil {
|
||||
n := c.ReadBufferSize
|
||||
if n <= 0 {
|
||||
n = defaultReadBufferSize
|
||||
}
|
||||
return bufio.NewReaderSize(conn, n)
|
||||
}
|
||||
br := v.(*bufio.Reader)
|
||||
br.Reset(conn)
|
||||
return br
|
||||
}
|
||||
|
||||
func (c *HostClient) releaseReader(br *bufio.Reader) {
|
||||
c.readerPool.Put(br)
|
||||
}
|
||||
|
||||
func newClientTLSConfig(c *tls.Config, addr string) *tls.Config {
|
||||
if c == nil {
|
||||
c = &tls.Config{}
|
||||
} else {
|
||||
// TODO: substitute this with c.Clone() after go1.8 becomes mainstream :)
|
||||
c = &tls.Config{
|
||||
Rand: c.Rand,
|
||||
Time: c.Time,
|
||||
Certificates: c.Certificates,
|
||||
NameToCertificate: c.NameToCertificate,
|
||||
GetCertificate: c.GetCertificate,
|
||||
RootCAs: c.RootCAs,
|
||||
NextProtos: c.NextProtos,
|
||||
ServerName: c.ServerName,
|
||||
|
||||
// Do not copy ClientAuth, since it is server-related stuff
|
||||
// Do not copy ClientCAs, since it is server-related stuff
|
||||
|
||||
InsecureSkipVerify: c.InsecureSkipVerify,
|
||||
CipherSuites: c.CipherSuites,
|
||||
|
||||
// Do not copy PreferServerCipherSuites - this is server stuff
|
||||
|
||||
SessionTicketsDisabled: c.SessionTicketsDisabled,
|
||||
|
||||
// Do not copy SessionTicketKey - this is server stuff
|
||||
|
||||
ClientSessionCache: c.ClientSessionCache,
|
||||
MinVersion: c.MinVersion,
|
||||
MaxVersion: c.MaxVersion,
|
||||
CurvePreferences: c.CurvePreferences,
|
||||
}
|
||||
}
|
||||
|
||||
if c.ClientSessionCache == nil {
|
||||
c.ClientSessionCache = tls.NewLRUClientSessionCache(0)
|
||||
}
|
||||
|
||||
if len(c.ServerName) == 0 {
|
||||
serverName := tlsServerName(addr)
|
||||
if serverName == "*" {
|
||||
c.InsecureSkipVerify = true
|
||||
} else {
|
||||
c.ServerName = serverName
|
||||
}
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func tlsServerName(addr string) string {
|
||||
if !strings.Contains(addr, ":") {
|
||||
return addr
|
||||
}
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "*"
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
func (c *HostClient) nextAddr() string {
|
||||
c.addrsLock.Lock()
|
||||
if c.addrs == nil {
|
||||
c.addrs = strings.Split(c.Addr, ",")
|
||||
}
|
||||
addr := c.addrs[0]
|
||||
if len(c.addrs) > 1 {
|
||||
addr = c.addrs[c.addrIdx%uint32(len(c.addrs))]
|
||||
c.addrIdx++
|
||||
}
|
||||
c.addrsLock.Unlock()
|
||||
return addr
|
||||
}
|
||||
|
||||
func (c *HostClient) dialHostHard() (conn net.Conn, err error) {
|
||||
// attempt to dial all the available hosts before giving up.
|
||||
|
||||
c.addrsLock.Lock()
|
||||
n := len(c.addrs)
|
||||
c.addrsLock.Unlock()
|
||||
|
||||
if n == 0 {
|
||||
// It looks like c.addrs isn't initialized yet.
|
||||
n = 1
|
||||
}
|
||||
|
||||
timeout := c.ReadTimeout + c.WriteTimeout
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultDialTimeout
|
||||
}
|
||||
deadline := time.Now().Add(timeout)
|
||||
for n > 0 {
|
||||
addr := c.nextAddr()
|
||||
tlsConfig := c.cachedTLSConfig(addr)
|
||||
conn, err = dialAddr(addr, c.Dial, c.DialDualStack, c.IsTLS, tlsConfig)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
if time.Since(deadline) >= 0 {
|
||||
break
|
||||
}
|
||||
n--
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (c *HostClient) cachedTLSConfig(addr string) *tls.Config {
|
||||
if !c.IsTLS {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.tlsConfigMapLock.Lock()
|
||||
if c.tlsConfigMap == nil {
|
||||
c.tlsConfigMap = make(map[string]*tls.Config)
|
||||
}
|
||||
cfg := c.tlsConfigMap[addr]
|
||||
if cfg == nil {
|
||||
cfg = newClientTLSConfig(c.TLSConfig, addr)
|
||||
c.tlsConfigMap[addr] = cfg
|
||||
}
|
||||
c.tlsConfigMapLock.Unlock()
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func dialAddr(addr string, dial DialFunc, dialDualStack, isTLS bool, tlsConfig *tls.Config) (net.Conn, error) {
|
||||
if dial == nil {
|
||||
if dialDualStack {
|
||||
dial = DialDualStack
|
||||
} else {
|
||||
dial = Dial
|
||||
}
|
||||
addr = addMissingPort(addr, isTLS)
|
||||
}
|
||||
conn, err := dial(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if conn == nil {
|
||||
panic("BUG: DialFunc returned (nil, nil)")
|
||||
}
|
||||
if isTLS {
|
||||
conn = tls.Client(conn, tlsConfig)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *HostClient) getClientName() []byte {
|
||||
v := c.clientName.Load()
|
||||
var clientName []byte
|
||||
if v == nil {
|
||||
clientName = []byte(c.Name)
|
||||
if len(clientName) == 0 {
|
||||
clientName = defaultUserAgent
|
||||
}
|
||||
c.clientName.Store(clientName)
|
||||
} else {
|
||||
clientName = v.([]byte)
|
||||
}
|
||||
return clientName
|
||||
}
|
||||
|
||||
func addMissingPort(addr string, isTLS bool) string {
|
||||
n := strings.Index(addr, ":")
|
||||
if n >= 0 {
|
||||
return addr
|
||||
}
|
||||
port := 80
|
||||
if isTLS {
|
||||
port = 443
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", addr, port)
|
||||
}
|
||||
|
||||
// PipelineClient pipelines requests over a limited set of concurrent
|
||||
// connections to the given Addr.
|
||||
//
|
||||
// This client may be used in highly loaded HTTP-based RPC systems for reducing
|
||||
// context switches and network level overhead.
|
||||
// See https://en.wikipedia.org/wiki/HTTP_pipelining for details.
|
||||
//
|
||||
// It is forbidden copying PipelineClient instances. Create new instances
|
||||
// instead.
|
||||
//
|
||||
// It is safe calling PipelineClient methods from concurrently running
|
||||
// goroutines.
|
||||
type PipelineClient struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Address of the host to connect to.
|
||||
Addr string
|
||||
|
||||
// The maximum number of concurrent connections to the Addr.
|
||||
//
|
||||
// A sinle connection is used by default.
|
||||
MaxConns int
|
||||
|
||||
// The maximum number of pending pipelined requests over
|
||||
// a single connection to Addr.
|
||||
//
|
||||
// DefaultMaxPendingRequests is used by default.
|
||||
MaxPendingRequests int
|
||||
|
||||
// The maximum delay before sending pipelined requests as a batch
|
||||
// to the server.
|
||||
//
|
||||
// By default requests are sent immediately to the server.
|
||||
MaxBatchDelay time.Duration
|
||||
|
||||
// Callback for connection establishing to the host.
|
||||
//
|
||||
// Default Dial is used if not set.
|
||||
Dial DialFunc
|
||||
|
||||
// Attempt to connect to both ipv4 and ipv6 host addresses
|
||||
// if set to true.
|
||||
//
|
||||
// This option is used only if default TCP dialer is used,
|
||||
// i.e. if Dial is blank.
|
||||
//
|
||||
// By default client connects only to ipv4 addresses,
|
||||
// since unfortunately ipv6 remains broken in many networks worldwide :)
|
||||
DialDualStack bool
|
||||
|
||||
// Whether to use TLS (aka SSL or HTTPS) for host connections.
|
||||
IsTLS bool
|
||||
|
||||
// Optional TLS config.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Idle connection to the host is closed after this duration.
|
||||
//
|
||||
// By default idle connection is closed after
|
||||
// DefaultMaxIdleConnDuration.
|
||||
MaxIdleConnDuration time.Duration
|
||||
|
||||
// Buffer size for responses' reading.
|
||||
// This also limits the maximum header size.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
ReadBufferSize int
|
||||
|
||||
// Buffer size for requests' writing.
|
||||
//
|
||||
// Default buffer size is used if 0.
|
||||
WriteBufferSize int
|
||||
|
||||
// Maximum duration for full response reading (including body).
|
||||
//
|
||||
// By default response read timeout is unlimited.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// Maximum duration for full request writing (including body).
|
||||
//
|
||||
// By default request write timeout is unlimited.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// Logger for logging client errors.
|
||||
//
|
||||
// By default standard logger from log package is used.
|
||||
Logger Logger
|
||||
|
||||
connClients []*pipelineConnClient
|
||||
connClientsLock sync.Mutex
|
||||
}
|
||||
|
||||
type pipelineConnClient struct {
|
||||
noCopy noCopy
|
||||
|
||||
Addr string
|
||||
MaxPendingRequests int
|
||||
MaxBatchDelay time.Duration
|
||||
Dial DialFunc
|
||||
DialDualStack bool
|
||||
IsTLS bool
|
||||
TLSConfig *tls.Config
|
||||
MaxIdleConnDuration time.Duration
|
||||
ReadBufferSize int
|
||||
WriteBufferSize int
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
Logger Logger
|
||||
|
||||
workPool sync.Pool
|
||||
|
||||
chLock sync.Mutex
|
||||
chW chan *pipelineWork
|
||||
chR chan *pipelineWork
|
||||
|
||||
tlsConfigLock sync.Mutex
|
||||
tlsConfig *tls.Config
|
||||
}
|
||||
|
||||
type pipelineWork struct {
|
||||
reqCopy Request
|
||||
respCopy Response
|
||||
req *Request
|
||||
resp *Response
|
||||
t *time.Timer
|
||||
deadline time.Time
|
||||
err error
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
// DoTimeout performs the given request and waits for response during
|
||||
// the given timeout duration.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned during
|
||||
// the given timeout.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *PipelineClient) DoTimeout(req *Request, resp *Response, timeout time.Duration) error {
|
||||
return c.DoDeadline(req, resp, time.Now().Add(timeout))
|
||||
}
|
||||
|
||||
// DoDeadline performs the given request and waits for response until
|
||||
// the given deadline.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// ErrTimeout is returned if the response wasn't returned until
|
||||
// the given deadline.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *PipelineClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
return c.getConnClient().DoDeadline(req, resp, deadline)
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) DoDeadline(req *Request, resp *Response, deadline time.Time) error {
|
||||
c.init()
|
||||
|
||||
timeout := -time.Since(deadline)
|
||||
if timeout < 0 {
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
w := acquirePipelineWork(&c.workPool, timeout)
|
||||
w.req = &w.reqCopy
|
||||
w.resp = &w.respCopy
|
||||
|
||||
// Make a copy of the request in order to avoid data races on timeouts
|
||||
req.copyToSkipBody(&w.reqCopy)
|
||||
swapRequestBody(req, &w.reqCopy)
|
||||
|
||||
// Put the request to outgoing queue
|
||||
select {
|
||||
case c.chW <- w:
|
||||
// Fast path: len(c.ch) < cap(c.ch)
|
||||
default:
|
||||
// Slow path
|
||||
select {
|
||||
case c.chW <- w:
|
||||
case <-w.t.C:
|
||||
releasePipelineWork(&c.workPool, w)
|
||||
return ErrTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the response
|
||||
var err error
|
||||
select {
|
||||
case <-w.done:
|
||||
if resp != nil {
|
||||
w.respCopy.copyToSkipBody(resp)
|
||||
swapResponseBody(resp, &w.respCopy)
|
||||
}
|
||||
err = w.err
|
||||
releasePipelineWork(&c.workPool, w)
|
||||
case <-w.t.C:
|
||||
err = ErrTimeout
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Do performs the given http request and sets the corresponding response.
|
||||
//
|
||||
// Request must contain at least non-zero RequestURI with full url (including
|
||||
// scheme and host) or non-zero Host header + RequestURI.
|
||||
//
|
||||
// The function doesn't follow redirects. Use Get* for following redirects.
|
||||
//
|
||||
// Response is ignored if resp is nil.
|
||||
//
|
||||
// It is recommended obtaining req and resp via AcquireRequest
|
||||
// and AcquireResponse in performance-critical code.
|
||||
func (c *PipelineClient) Do(req *Request, resp *Response) error {
|
||||
return c.getConnClient().Do(req, resp)
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) Do(req *Request, resp *Response) error {
|
||||
c.init()
|
||||
|
||||
w := acquirePipelineWork(&c.workPool, 0)
|
||||
w.req = req
|
||||
if resp != nil {
|
||||
w.resp = resp
|
||||
} else {
|
||||
w.resp = &w.respCopy
|
||||
}
|
||||
|
||||
// Put the request to outgoing queue
|
||||
select {
|
||||
case c.chW <- w:
|
||||
default:
|
||||
// Try substituting the oldest w with the current one.
|
||||
select {
|
||||
case wOld := <-c.chW:
|
||||
wOld.err = ErrPipelineOverflow
|
||||
wOld.done <- struct{}{}
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case c.chW <- w:
|
||||
default:
|
||||
releasePipelineWork(&c.workPool, w)
|
||||
return ErrPipelineOverflow
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for the response
|
||||
<-w.done
|
||||
err := w.err
|
||||
|
||||
releasePipelineWork(&c.workPool, w)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *PipelineClient) getConnClient() *pipelineConnClient {
|
||||
c.connClientsLock.Lock()
|
||||
cc := c.getConnClientUnlocked()
|
||||
c.connClientsLock.Unlock()
|
||||
return cc
|
||||
}
|
||||
|
||||
func (c *PipelineClient) getConnClientUnlocked() *pipelineConnClient {
|
||||
if len(c.connClients) == 0 {
|
||||
return c.newConnClient()
|
||||
}
|
||||
|
||||
// Return the client with the minimum number of pending requests.
|
||||
minCC := c.connClients[0]
|
||||
minReqs := minCC.PendingRequests()
|
||||
if minReqs == 0 {
|
||||
return minCC
|
||||
}
|
||||
for i := 1; i < len(c.connClients); i++ {
|
||||
cc := c.connClients[i]
|
||||
reqs := cc.PendingRequests()
|
||||
if reqs == 0 {
|
||||
return cc
|
||||
}
|
||||
if reqs < minReqs {
|
||||
minCC = cc
|
||||
minReqs = reqs
|
||||
}
|
||||
}
|
||||
|
||||
maxConns := c.MaxConns
|
||||
if maxConns <= 0 {
|
||||
maxConns = 1
|
||||
}
|
||||
if len(c.connClients) < maxConns {
|
||||
return c.newConnClient()
|
||||
}
|
||||
return minCC
|
||||
}
|
||||
|
||||
func (c *PipelineClient) newConnClient() *pipelineConnClient {
|
||||
cc := &pipelineConnClient{
|
||||
Addr: c.Addr,
|
||||
MaxPendingRequests: c.MaxPendingRequests,
|
||||
MaxBatchDelay: c.MaxBatchDelay,
|
||||
Dial: c.Dial,
|
||||
DialDualStack: c.DialDualStack,
|
||||
IsTLS: c.IsTLS,
|
||||
TLSConfig: c.TLSConfig,
|
||||
MaxIdleConnDuration: c.MaxIdleConnDuration,
|
||||
ReadBufferSize: c.ReadBufferSize,
|
||||
WriteBufferSize: c.WriteBufferSize,
|
||||
ReadTimeout: c.ReadTimeout,
|
||||
WriteTimeout: c.WriteTimeout,
|
||||
Logger: c.Logger,
|
||||
}
|
||||
c.connClients = append(c.connClients, cc)
|
||||
return cc
|
||||
}
|
||||
|
||||
// ErrPipelineOverflow may be returned from PipelineClient.Do*
|
||||
// if the requests' queue is overflown.
|
||||
var ErrPipelineOverflow = errors.New("pipelined requests' queue has been overflown. Increase MaxConns and/or MaxPendingRequests")
|
||||
|
||||
// DefaultMaxPendingRequests is the default value
|
||||
// for PipelineClient.MaxPendingRequests.
|
||||
const DefaultMaxPendingRequests = 1024
|
||||
|
||||
func (c *pipelineConnClient) init() {
|
||||
c.chLock.Lock()
|
||||
if c.chR == nil {
|
||||
maxPendingRequests := c.MaxPendingRequests
|
||||
if maxPendingRequests <= 0 {
|
||||
maxPendingRequests = DefaultMaxPendingRequests
|
||||
}
|
||||
c.chR = make(chan *pipelineWork, maxPendingRequests)
|
||||
if c.chW == nil {
|
||||
c.chW = make(chan *pipelineWork, maxPendingRequests)
|
||||
}
|
||||
go func() {
|
||||
if err := c.worker(); err != nil {
|
||||
c.logger().Printf("error in PipelineClient(%q): %s", c.Addr, err)
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||
// Throttle client reconnections on temporary errors
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
c.chLock.Lock()
|
||||
// Do not reset c.chW to nil, since it may contain
|
||||
// pending requests, which could be served on the next
|
||||
// connection to the host.
|
||||
c.chR = nil
|
||||
c.chLock.Unlock()
|
||||
}()
|
||||
}
|
||||
c.chLock.Unlock()
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) worker() error {
|
||||
tlsConfig := c.cachedTLSConfig()
|
||||
conn, err := dialAddr(c.Addr, c.Dial, c.DialDualStack, c.IsTLS, tlsConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start reader and writer
|
||||
stopW := make(chan struct{})
|
||||
doneW := make(chan error)
|
||||
go func() {
|
||||
doneW <- c.writer(conn, stopW)
|
||||
}()
|
||||
stopR := make(chan struct{})
|
||||
doneR := make(chan error)
|
||||
go func() {
|
||||
doneR <- c.reader(conn, stopR)
|
||||
}()
|
||||
|
||||
// Wait until reader and writer are stopped
|
||||
select {
|
||||
case err = <-doneW:
|
||||
conn.Close()
|
||||
close(stopR)
|
||||
<-doneR
|
||||
case err = <-doneR:
|
||||
conn.Close()
|
||||
close(stopW)
|
||||
<-doneW
|
||||
}
|
||||
|
||||
// Notify pending readers
|
||||
for len(c.chR) > 0 {
|
||||
w := <-c.chR
|
||||
w.err = errPipelineConnStopped
|
||||
w.done <- struct{}{}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) cachedTLSConfig() *tls.Config {
|
||||
if !c.IsTLS {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.tlsConfigLock.Lock()
|
||||
cfg := c.tlsConfig
|
||||
if cfg == nil {
|
||||
cfg = newClientTLSConfig(c.TLSConfig, c.Addr)
|
||||
c.tlsConfig = cfg
|
||||
}
|
||||
c.tlsConfigLock.Unlock()
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) writer(conn net.Conn, stopCh <-chan struct{}) error {
|
||||
writeBufferSize := c.WriteBufferSize
|
||||
if writeBufferSize <= 0 {
|
||||
writeBufferSize = defaultWriteBufferSize
|
||||
}
|
||||
bw := bufio.NewWriterSize(conn, writeBufferSize)
|
||||
defer bw.Flush()
|
||||
chR := c.chR
|
||||
chW := c.chW
|
||||
writeTimeout := c.WriteTimeout
|
||||
|
||||
maxIdleConnDuration := c.MaxIdleConnDuration
|
||||
if maxIdleConnDuration <= 0 {
|
||||
maxIdleConnDuration = DefaultMaxIdleConnDuration
|
||||
}
|
||||
maxBatchDelay := c.MaxBatchDelay
|
||||
|
||||
var (
|
||||
stopTimer = time.NewTimer(time.Hour)
|
||||
flushTimer = time.NewTimer(time.Hour)
|
||||
flushTimerCh <-chan time.Time
|
||||
instantTimerCh = make(chan time.Time)
|
||||
|
||||
w *pipelineWork
|
||||
err error
|
||||
|
||||
lastWriteDeadlineTime time.Time
|
||||
)
|
||||
close(instantTimerCh)
|
||||
for {
|
||||
againChW:
|
||||
select {
|
||||
case w = <-chW:
|
||||
// Fast path: len(chW) > 0
|
||||
default:
|
||||
// Slow path
|
||||
stopTimer.Reset(maxIdleConnDuration)
|
||||
select {
|
||||
case w = <-chW:
|
||||
case <-stopTimer.C:
|
||||
return nil
|
||||
case <-stopCh:
|
||||
return nil
|
||||
case <-flushTimerCh:
|
||||
if err = bw.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
flushTimerCh = nil
|
||||
goto againChW
|
||||
}
|
||||
}
|
||||
|
||||
if !w.deadline.IsZero() && time.Since(w.deadline) >= 0 {
|
||||
w.err = ErrTimeout
|
||||
w.done <- struct{}{}
|
||||
continue
|
||||
}
|
||||
|
||||
if writeTimeout > 0 {
|
||||
// Optimization: update write deadline only if more than 25%
|
||||
// of the last write deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
currentTime := CoarseTimeNow()
|
||||
if currentTime.Sub(lastWriteDeadlineTime) > (writeTimeout >> 2) {
|
||||
if err = conn.SetWriteDeadline(currentTime.Add(writeTimeout)); err != nil {
|
||||
w.err = err
|
||||
w.done <- struct{}{}
|
||||
return err
|
||||
}
|
||||
lastWriteDeadlineTime = currentTime
|
||||
}
|
||||
}
|
||||
if err = w.req.Write(bw); err != nil {
|
||||
w.err = err
|
||||
w.done <- struct{}{}
|
||||
return err
|
||||
}
|
||||
if flushTimerCh == nil && (len(chW) == 0 || len(chR) == cap(chR)) {
|
||||
if maxBatchDelay > 0 {
|
||||
flushTimer.Reset(maxBatchDelay)
|
||||
flushTimerCh = flushTimer.C
|
||||
} else {
|
||||
flushTimerCh = instantTimerCh
|
||||
}
|
||||
}
|
||||
|
||||
againChR:
|
||||
select {
|
||||
case chR <- w:
|
||||
// Fast path: len(chR) < cap(chR)
|
||||
default:
|
||||
// Slow path
|
||||
select {
|
||||
case chR <- w:
|
||||
case <-stopCh:
|
||||
w.err = errPipelineConnStopped
|
||||
w.done <- struct{}{}
|
||||
return nil
|
||||
case <-flushTimerCh:
|
||||
if err = bw.Flush(); err != nil {
|
||||
w.err = err
|
||||
w.done <- struct{}{}
|
||||
return err
|
||||
}
|
||||
flushTimerCh = nil
|
||||
goto againChR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) reader(conn net.Conn, stopCh <-chan struct{}) error {
|
||||
readBufferSize := c.ReadBufferSize
|
||||
if readBufferSize <= 0 {
|
||||
readBufferSize = defaultReadBufferSize
|
||||
}
|
||||
br := bufio.NewReaderSize(conn, readBufferSize)
|
||||
chR := c.chR
|
||||
readTimeout := c.ReadTimeout
|
||||
|
||||
var (
|
||||
w *pipelineWork
|
||||
err error
|
||||
|
||||
lastReadDeadlineTime time.Time
|
||||
)
|
||||
for {
|
||||
select {
|
||||
case w = <-chR:
|
||||
// Fast path: len(chR) > 0
|
||||
default:
|
||||
// Slow path
|
||||
select {
|
||||
case w = <-chR:
|
||||
case <-stopCh:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if readTimeout > 0 {
|
||||
// Optimization: update read deadline only if more than 25%
|
||||
// of the last read deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
currentTime := CoarseTimeNow()
|
||||
if currentTime.Sub(lastReadDeadlineTime) > (readTimeout >> 2) {
|
||||
if err = conn.SetReadDeadline(currentTime.Add(readTimeout)); err != nil {
|
||||
w.err = err
|
||||
w.done <- struct{}{}
|
||||
return err
|
||||
}
|
||||
lastReadDeadlineTime = currentTime
|
||||
}
|
||||
}
|
||||
if err = w.resp.Read(br); err != nil {
|
||||
w.err = err
|
||||
w.done <- struct{}{}
|
||||
return err
|
||||
}
|
||||
|
||||
w.done <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) logger() Logger {
|
||||
if c.Logger != nil {
|
||||
return c.Logger
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// PendingRequests returns the current number of pending requests pipelined
|
||||
// to the server.
|
||||
//
|
||||
// This number may exceed MaxPendingRequests*MaxConns by up to two times, since
|
||||
// each connection to the server may keep up to MaxPendingRequests requests
|
||||
// in the queue before sending them to the server.
|
||||
//
|
||||
// This function may be used for balancing load among multiple PipelineClient
|
||||
// instances.
|
||||
func (c *PipelineClient) PendingRequests() int {
|
||||
c.connClientsLock.Lock()
|
||||
n := 0
|
||||
for _, cc := range c.connClients {
|
||||
n += cc.PendingRequests()
|
||||
}
|
||||
c.connClientsLock.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func (c *pipelineConnClient) PendingRequests() int {
|
||||
c.init()
|
||||
|
||||
c.chLock.Lock()
|
||||
n := len(c.chR) + len(c.chW)
|
||||
c.chLock.Unlock()
|
||||
return n
|
||||
}
|
||||
|
||||
var errPipelineConnStopped = errors.New("pipeline connection has been stopped")
|
||||
|
||||
func acquirePipelineWork(pool *sync.Pool, timeout time.Duration) *pipelineWork {
|
||||
v := pool.Get()
|
||||
if v == nil {
|
||||
v = &pipelineWork{
|
||||
done: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
w := v.(*pipelineWork)
|
||||
if timeout > 0 {
|
||||
if w.t == nil {
|
||||
w.t = time.NewTimer(timeout)
|
||||
} else {
|
||||
w.t.Reset(timeout)
|
||||
}
|
||||
w.deadline = time.Now().Add(timeout)
|
||||
} else {
|
||||
w.deadline = zeroTime
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func releasePipelineWork(pool *sync.Pool, w *pipelineWork) {
|
||||
if w.t != nil {
|
||||
w.t.Stop()
|
||||
}
|
||||
w.reqCopy.Reset()
|
||||
w.respCopy.Reset()
|
||||
w.req = nil
|
||||
w.resp = nil
|
||||
w.err = nil
|
||||
pool.Put(w)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CoarseTimeNow returns the current time truncated to the nearest second.
|
||||
//
|
||||
// This is a faster alternative to time.Now().
|
||||
func CoarseTimeNow() time.Time {
|
||||
tp := coarseTime.Load().(*time.Time)
|
||||
return *tp
|
||||
}
|
||||
|
||||
func init() {
|
||||
t := time.Now().Truncate(time.Second)
|
||||
coarseTime.Store(&t)
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
t := time.Now().Truncate(time.Second)
|
||||
coarseTime.Store(&t)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var coarseTime atomic.Value
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"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 {
|
||||
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 {
|
||||
panic(fmt.Sprintf("BUG: unexpected error from gzip.NewWriterLevel(%d): %s", level, err))
|
||||
}
|
||||
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)
|
||||
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,
|
||||
*ByteBuffer,
|
||||
*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 stacklessWriteGzip = stackless.NewFunc(nonblockingWriteGzip)
|
||||
|
||||
func nonblockingWriteGzip(ctxv interface{}) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealGzipWriter(ctx.w, ctx.level)
|
||||
|
||||
_, err := zw.Write(ctx.p)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("BUG: gzip.Writer.Write for len(p)=%d returned unexpected error: %s", len(ctx.p), err))
|
||||
}
|
||||
|
||||
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)
|
||||
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,
|
||||
*ByteBuffer,
|
||||
*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 stacklessWriteDeflate = stackless.NewFunc(nonblockingWriteDeflate)
|
||||
|
||||
func nonblockingWriteDeflate(ctxv interface{}) {
|
||||
ctx := ctxv.(*compressCtx)
|
||||
zw := acquireRealDeflateWriter(ctx.w, ctx.level)
|
||||
|
||||
_, err := zw.Write(ctx.p)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("BUG: zlib.Writer.Write for len(p)=%d returned unexpected error: %s", len(ctx.p), err))
|
||||
}
|
||||
|
||||
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 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 {
|
||||
panic(fmt.Sprintf("BUG: unexpected error from zlib.NewWriterLevel(%d): %s", level, err))
|
||||
}
|
||||
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://golang.org/pkg/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 *os.File, minCompressRatio float64) bool {
|
||||
// Try compressing the first 4kb of of the file
|
||||
// and see if it can be compressed by more than
|
||||
// the given minCompressRatio.
|
||||
b := AcquireByteBuffer()
|
||||
zw := acquireStacklessGzipWriter(b, CompressDefaultCompression)
|
||||
lr := &io.LimitedReader{
|
||||
R: f,
|
||||
N: 4096,
|
||||
}
|
||||
_, err := copyZeroAlloc(zw, lr)
|
||||
releaseStacklessGzipWriter(zw, CompressDefaultCompression)
|
||||
f.Seek(0, 0)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
n := 4096 - lr.N
|
||||
zn := len(b.B)
|
||||
ReleaseByteBuffer(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
|
||||
}
|
||||
+396
@@ -0,0 +1,396 @@
|
||||
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
|
||||
)
|
||||
|
||||
// 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() interface{} {
|
||||
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
|
||||
domain []byte
|
||||
path []byte
|
||||
|
||||
httpOnly bool
|
||||
secure bool
|
||||
|
||||
bufKV argsKV
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// CopyTo copies src cookie to c.
|
||||
func (c *Cookie) CopyTo(src *Cookie) {
|
||||
c.Reset()
|
||||
c.key = append(c.key[:0], src.key...)
|
||||
c.value = append(c.value[:0], src.value...)
|
||||
c.expire = src.expire
|
||||
c.domain = append(c.domain[:0], src.domain...)
|
||||
c.path = append(c.path[:0], src.path...)
|
||||
c.httpOnly = src.httpOnly
|
||||
c.secure = src.secure
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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 domain is valid until the next Cookie modification method call.
|
||||
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...)
|
||||
}
|
||||
|
||||
// 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 next Cookie modification method call.
|
||||
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 next Cookie modification method call.
|
||||
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.domain = c.domain[:0]
|
||||
c.path = c.path[:0]
|
||||
c.httpOnly = false
|
||||
c.secure = 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.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...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Cookie returns cookie representation.
|
||||
//
|
||||
// The returned value is valid until the next call to Cookie methods.
|
||||
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[:0], kv.key...)
|
||||
c.value = append(c.value[:0], kv.value...)
|
||||
|
||||
for s.next(kv) {
|
||||
if len(kv.key) == 0 && len(kv.value) == 0 {
|
||||
continue
|
||||
}
|
||||
switch string(kv.key) {
|
||||
case "expires":
|
||||
v := b2s(kv.value)
|
||||
exptime, err := time.ParseInLocation(time.RFC1123, v, time.UTC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.expire = exptime
|
||||
case "domain":
|
||||
c.domain = append(c.domain[:0], kv.value...)
|
||||
case "path":
|
||||
c.path = append(c.path[:0], kv.value...)
|
||||
case "":
|
||||
switch string(kv.value) {
|
||||
case "HttpOnly":
|
||||
c.httpOnly = true
|
||||
case "secure":
|
||||
c.secure = true
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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...)
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Package fasthttp provides fast HTTP server and client API.
|
||||
|
||||
Fasthttp provides the following features:
|
||||
|
||||
* Optimized for speed. Easily handles more than 100K qps and more than 1M
|
||||
concurrent keep-alive connections on modern hardware.
|
||||
* Optimized for low memory usage.
|
||||
* Easy 'Connection: Upgrade' support via RequestCtx.Hijack.
|
||||
* Server supports requests' pipelining. Multiple requests may be read from
|
||||
a single network packet and multiple responses may be sent in a single
|
||||
network packet. This may be useful for highly loaded REST services.
|
||||
* 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.
|
||||
|
||||
* 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.
|
||||
|
||||
* Client supports automatic retry on idempotent requests' failure.
|
||||
* 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
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIBpQbZ6a5jL1Yh4wdP6yZk4MKjYWArD/QOLENFw8vbELoAoGCCqGSM49
|
||||
AwEHoUQDQgAEKQCZWgE2IBhb47ot8MIs1D4KSisHYlZ41IWyeutpjb0fjwwIhimh
|
||||
pl1Qld1/d2j3Z3vVyfa5yD+ncV7qCFZuSg==
|
||||
-----END EC PRIVATE KEY-----
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIBbTCCAROgAwIBAgIQPo718S+K+G7hc1SgTEU4QDAKBggqhkjOPQQDAjASMRAw
|
||||
DgYDVQQKEwdBY21lIENvMB4XDTE3MDQyMDIxMDExNFoXDTE4MDQyMDIxMDExNFow
|
||||
EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCkA
|
||||
mVoBNiAYW+O6LfDCLNQ+CkorB2JWeNSFsnrraY29H48MCIYpoaZdUJXdf3do92d7
|
||||
1cn2ucg/p3Fe6ghWbkqjSzBJMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr
|
||||
BgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggq
|
||||
hkjOPQQDAgNIADBFAiEAoLAIQkvSuIcHUqyWroA6yWYw2fznlRH/uO9/hMCxUCEC
|
||||
IClRYb/5O9eD/Eq/ozPnwNpsQHOeYefEhadJ/P82y0lG
|
||||
-----END CERTIFICATE-----
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
package fasthttputil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// InmemoryListener provides in-memory dialer<->net.Listener implementation.
|
||||
//
|
||||
// It may be used either for fast in-process client<->server communcations
|
||||
// without network stack overhead or for client<->server tests.
|
||||
type InmemoryListener struct {
|
||||
lock sync.Mutex
|
||||
closed bool
|
||||
conns chan net.Conn
|
||||
}
|
||||
|
||||
// NewInmemoryListener returns new in-memory dialer<->net.Listener.
|
||||
func NewInmemoryListener() *InmemoryListener {
|
||||
return &InmemoryListener{
|
||||
conns: make(chan net.Conn, 1024),
|
||||
}
|
||||
}
|
||||
|
||||
// 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, fmt.Errorf("InmemoryListener is already closed: use of closed network connection")
|
||||
}
|
||||
return c, 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 = fmt.Errorf("InmemoryListener is already closed")
|
||||
}
|
||||
ln.lock.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
// Addr implements net.Listener's Addr.
|
||||
func (ln *InmemoryListener) Addr() net.Addr {
|
||||
return &net.UnixAddr{
|
||||
Name: "InmemoryListener",
|
||||
Net: "memory",
|
||||
}
|
||||
}
|
||||
|
||||
// Dial creates new client<->server connection, enqueues server side
|
||||
// of the connection to Accept and returns client side of the connection.
|
||||
//
|
||||
// It is safe calling Dial from concurrently running goroutines.
|
||||
func (ln *InmemoryListener) Dial() (net.Conn, error) {
|
||||
pc := NewPipeConns()
|
||||
cConn := pc.Conn1()
|
||||
sConn := pc.Conn2()
|
||||
ln.lock.Lock()
|
||||
if !ln.closed {
|
||||
ln.conns <- sConn
|
||||
} else {
|
||||
sConn.Close()
|
||||
cConn.Close()
|
||||
cConn = nil
|
||||
}
|
||||
ln.lock.Unlock()
|
||||
|
||||
if cConn == nil {
|
||||
return nil, fmt.Errorf("InmemoryListener is already closed")
|
||||
}
|
||||
return cConn, nil
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
package fasthttputil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewPipeConns returns new bi-directonal connection pipe.
|
||||
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.
|
||||
//
|
||||
type PipeConns struct {
|
||||
c1 pipeConn
|
||||
c2 pipeConn
|
||||
stopCh chan struct{}
|
||||
stopChLock sync.Mutex
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
select {
|
||||
case c.b = <-c.rCh:
|
||||
case <-c.readDeadlineCh:
|
||||
c.readDeadlineCh = closedDeadlineCh
|
||||
// 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")
|
||||
|
||||
// ErrTimeout is returned from Read() or Write() on timeout.
|
||||
ErrTimeout = errors.New("timeout")
|
||||
)
|
||||
|
||||
func (c *pipeConn) Close() error {
|
||||
return c.pc.Close()
|
||||
}
|
||||
|
||||
func (c *pipeConn) LocalAddr() net.Addr {
|
||||
return pipeAddr(0)
|
||||
}
|
||||
|
||||
func (c *pipeConn) RemoteAddr() net.Addr {
|
||||
return pipeAddr(0)
|
||||
}
|
||||
|
||||
func (c *pipeConn) SetDeadline(deadline time.Time) error {
|
||||
c.SetReadDeadline(deadline)
|
||||
c.SetWriteDeadline(deadline)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *pipeConn) SetReadDeadline(deadline time.Time) error {
|
||||
if c.readDeadlineTimer == nil {
|
||||
c.readDeadlineTimer = time.NewTimer(time.Hour)
|
||||
}
|
||||
c.readDeadlineCh = updateTimer(c.readDeadlineTimer, deadline)
|
||||
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.Since(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() interface{} {
|
||||
return &byteBuffer{
|
||||
b: make([]byte, 1024),
|
||||
}
|
||||
},
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG
|
||||
3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U
|
||||
wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0
|
||||
FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf
|
||||
IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg
|
||||
GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF
|
||||
sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2
|
||||
sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D
|
||||
uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb
|
||||
K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3
|
||||
YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+
|
||||
DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk
|
||||
B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV
|
||||
Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x
|
||||
IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY
|
||||
wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj
|
||||
wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D
|
||||
FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m
|
||||
tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX
|
||||
fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU
|
||||
ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk
|
||||
K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT
|
||||
6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt
|
||||
9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN
|
||||
Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV
|
||||
c257YgaWmjK9uB0Y2r2VxS0G
|
||||
-----END PRIVATE KEY-----
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV
|
||||
BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV
|
||||
MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D
|
||||
K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te
|
||||
+z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij
|
||||
L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1
|
||||
xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY
|
||||
6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98
|
||||
L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2
|
||||
45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li
|
||||
K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6
|
||||
X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI
|
||||
whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd
|
||||
-----END CERTIFICATE-----
|
||||
+1252
@@ -0,0 +1,1252 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzip"
|
||||
)
|
||||
|
||||
// ServeFileBytesUncompressed returns HTTP response containing file contents
|
||||
// from the given path.
|
||||
//
|
||||
// Directory contents is returned if path points to directory.
|
||||
//
|
||||
// ServeFileBytes may be used for saving network traffic when serving files
|
||||
// with good compression ratio.
|
||||
//
|
||||
// See also RequestCtx.SendFileBytes.
|
||||
func ServeFileBytesUncompressed(ctx *RequestCtx, path []byte) {
|
||||
ServeFileUncompressed(ctx, b2s(path))
|
||||
}
|
||||
|
||||
// ServeFileUncompressed returns HTTP response containing file contents
|
||||
// from the given path.
|
||||
//
|
||||
// Directory contents is returned if path points to directory.
|
||||
//
|
||||
// ServeFile may be used for saving network traffic when serving files
|
||||
// with good compression ratio.
|
||||
//
|
||||
// See also RequestCtx.SendFile.
|
||||
func ServeFileUncompressed(ctx *RequestCtx, path string) {
|
||||
ctx.Request.Header.DelBytes(strAcceptEncoding)
|
||||
ServeFile(ctx, path)
|
||||
}
|
||||
|
||||
// ServeFileBytes returns HTTP response containing compressed file contents
|
||||
// from the given path.
|
||||
//
|
||||
// HTTP response may contain uncompressed file contents in the following cases:
|
||||
//
|
||||
// * Missing 'Accept-Encoding: gzip' request header.
|
||||
// * No write access to directory containing the file.
|
||||
//
|
||||
// Directory contents is returned if path points to directory.
|
||||
//
|
||||
// Use ServeFileBytesUncompressed is you don't need serving compressed
|
||||
// file contents.
|
||||
//
|
||||
// See also RequestCtx.SendFileBytes.
|
||||
func ServeFileBytes(ctx *RequestCtx, path []byte) {
|
||||
ServeFile(ctx, b2s(path))
|
||||
}
|
||||
|
||||
// ServeFile returns HTTP response containing compressed file contents
|
||||
// from the given path.
|
||||
//
|
||||
// HTTP response may contain uncompressed file contents in the following cases:
|
||||
//
|
||||
// * Missing 'Accept-Encoding: gzip' request header.
|
||||
// * No write access to directory containing the file.
|
||||
//
|
||||
// Directory contents is returned if path points to directory.
|
||||
//
|
||||
// Use ServeFileUncompressed is you don't need serving compressed file contents.
|
||||
//
|
||||
// See also RequestCtx.SendFile.
|
||||
func ServeFile(ctx *RequestCtx, path string) {
|
||||
rootFSOnce.Do(func() {
|
||||
rootFSHandler = rootFS.NewRequestHandler()
|
||||
})
|
||||
if len(path) == 0 || path[0] != '/' {
|
||||
// extend relative path to absolute path
|
||||
var err error
|
||||
if path, err = filepath.Abs(path); err != nil {
|
||||
ctx.Logger().Printf("cannot resolve path %q to absolute file path: %s", path, err)
|
||||
ctx.Error("Internal Server Error", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Request.SetRequestURI(path)
|
||||
rootFSHandler(ctx)
|
||||
}
|
||||
|
||||
var (
|
||||
rootFSOnce sync.Once
|
||||
rootFS = &FS{
|
||||
Root: "/",
|
||||
GenerateIndexPages: true,
|
||||
Compress: true,
|
||||
AcceptByteRange: true,
|
||||
}
|
||||
rootFSHandler RequestHandler
|
||||
)
|
||||
|
||||
// PathRewriteFunc must return new request path based on arbitrary ctx
|
||||
// info such as ctx.Path().
|
||||
//
|
||||
// Path rewriter is used in FS for translating the current request
|
||||
// to the local filesystem path relative to FS.Root.
|
||||
//
|
||||
// The returned path must not contain '/../' substrings due to security reasons,
|
||||
// since such paths may refer files outside FS.Root.
|
||||
//
|
||||
// The returned path may refer to ctx members. For example, ctx.Path().
|
||||
type PathRewriteFunc func(ctx *RequestCtx) []byte
|
||||
|
||||
// NewVHostPathRewriter returns path rewriter, which strips slashesCount
|
||||
// leading slashes from the path and prepends the path with request's host,
|
||||
// thus simplifying virtual hosting for static files.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// * host=foobar.com, slashesCount=0, original path="/foo/bar".
|
||||
// Resulting path: "/foobar.com/foo/bar"
|
||||
//
|
||||
// * host=img.aaa.com, slashesCount=1, original path="/images/123/456.jpg"
|
||||
// Resulting path: "/img.aaa.com/123/456.jpg"
|
||||
//
|
||||
func NewVHostPathRewriter(slashesCount int) PathRewriteFunc {
|
||||
return func(ctx *RequestCtx) []byte {
|
||||
path := stripLeadingSlashes(ctx.Path(), slashesCount)
|
||||
host := ctx.Host()
|
||||
if n := bytes.IndexByte(host, '/'); n >= 0 {
|
||||
host = nil
|
||||
}
|
||||
if len(host) == 0 {
|
||||
host = strInvalidHost
|
||||
}
|
||||
b := AcquireByteBuffer()
|
||||
b.B = append(b.B, '/')
|
||||
b.B = append(b.B, host...)
|
||||
b.B = append(b.B, path...)
|
||||
ctx.URI().SetPathBytes(b.B)
|
||||
ReleaseByteBuffer(b)
|
||||
|
||||
return ctx.Path()
|
||||
}
|
||||
}
|
||||
|
||||
var strInvalidHost = []byte("invalid-host")
|
||||
|
||||
// NewPathSlashesStripper returns path rewriter, which strips slashesCount
|
||||
// leading slashes from the path.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// * slashesCount = 0, original path: "/foo/bar", result: "/foo/bar"
|
||||
// * slashesCount = 1, original path: "/foo/bar", result: "/bar"
|
||||
// * slashesCount = 2, original path: "/foo/bar", result: ""
|
||||
//
|
||||
// The returned path rewriter may be used as FS.PathRewrite .
|
||||
func NewPathSlashesStripper(slashesCount int) PathRewriteFunc {
|
||||
return func(ctx *RequestCtx) []byte {
|
||||
return stripLeadingSlashes(ctx.Path(), slashesCount)
|
||||
}
|
||||
}
|
||||
|
||||
// NewPathPrefixStripper returns path rewriter, which removes prefixSize bytes
|
||||
// from the path prefix.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// * prefixSize = 0, original path: "/foo/bar", result: "/foo/bar"
|
||||
// * prefixSize = 3, original path: "/foo/bar", result: "o/bar"
|
||||
// * prefixSize = 7, original path: "/foo/bar", result: "r"
|
||||
//
|
||||
// The returned path rewriter may be used as FS.PathRewrite .
|
||||
func NewPathPrefixStripper(prefixSize int) PathRewriteFunc {
|
||||
return func(ctx *RequestCtx) []byte {
|
||||
path := ctx.Path()
|
||||
if len(path) >= prefixSize {
|
||||
path = path[prefixSize:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
// FS represents settings for request handler serving static files
|
||||
// from the local filesystem.
|
||||
//
|
||||
// It is prohibited copying FS values. Create new values instead.
|
||||
type FS struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Path to the root directory to serve files from.
|
||||
Root string
|
||||
|
||||
// List of index file names to try opening during directory access.
|
||||
//
|
||||
// For example:
|
||||
//
|
||||
// * index.html
|
||||
// * index.htm
|
||||
// * my-super-index.xml
|
||||
//
|
||||
// By default the list is empty.
|
||||
IndexNames []string
|
||||
|
||||
// Index pages for directories without files matching IndexNames
|
||||
// are automatically generated if set.
|
||||
//
|
||||
// Directory index generation may be quite slow for directories
|
||||
// with many files (more than 1K), so it is discouraged enabling
|
||||
// index pages' generation for such directories.
|
||||
//
|
||||
// By default index pages aren't generated.
|
||||
GenerateIndexPages bool
|
||||
|
||||
// Transparently compresses responses if set to true.
|
||||
//
|
||||
// The server tries minimizing CPU usage by caching compressed files.
|
||||
// It adds CompressedFileSuffix suffix to the original file name and
|
||||
// tries saving the resulting compressed file under the new file name.
|
||||
// So it is advisable to give the server write access to Root
|
||||
// and to all inner folders in order to minimze CPU usage when serving
|
||||
// compressed responses.
|
||||
//
|
||||
// Transparent compression is disabled by default.
|
||||
Compress bool
|
||||
|
||||
// Enables byte range requests if set to true.
|
||||
//
|
||||
// Byte range requests are disabled by default.
|
||||
AcceptByteRange bool
|
||||
|
||||
// Path rewriting function.
|
||||
//
|
||||
// By default request path is not modified.
|
||||
PathRewrite PathRewriteFunc
|
||||
|
||||
// Expiration duration for inactive file handlers.
|
||||
//
|
||||
// FSHandlerCacheDuration is used by default.
|
||||
CacheDuration time.Duration
|
||||
|
||||
// Suffix to add to the name of cached compressed file.
|
||||
//
|
||||
// This value has sense only if Compress is set.
|
||||
//
|
||||
// FSCompressedFileSuffix is used by default.
|
||||
CompressedFileSuffix string
|
||||
|
||||
once sync.Once
|
||||
h RequestHandler
|
||||
}
|
||||
|
||||
// FSCompressedFileSuffix is the suffix FS adds to the original file names
|
||||
// when trying to store compressed file under the new file name.
|
||||
// See FS.Compress for details.
|
||||
const FSCompressedFileSuffix = ".fasthttp.gz"
|
||||
|
||||
// FSHandlerCacheDuration is the default expiration duration for inactive
|
||||
// file handlers opened by FS.
|
||||
const FSHandlerCacheDuration = 10 * time.Second
|
||||
|
||||
// FSHandler returns request handler serving static files from
|
||||
// the given root folder.
|
||||
//
|
||||
// stripSlashes indicates how many leading slashes must be stripped
|
||||
// from requested path before searching requested file in the root folder.
|
||||
// Examples:
|
||||
//
|
||||
// * stripSlashes = 0, original path: "/foo/bar", result: "/foo/bar"
|
||||
// * stripSlashes = 1, original path: "/foo/bar", result: "/bar"
|
||||
// * stripSlashes = 2, original path: "/foo/bar", result: ""
|
||||
//
|
||||
// The returned request handler automatically generates index pages
|
||||
// for directories without index.html.
|
||||
//
|
||||
// The returned handler caches requested file handles
|
||||
// for FSHandlerCacheDuration.
|
||||
// Make sure your program has enough 'max open files' limit aka
|
||||
// 'ulimit -n' if root folder contains many files.
|
||||
//
|
||||
// Do not create multiple request handler instances for the same
|
||||
// (root, stripSlashes) arguments - just reuse a single instance.
|
||||
// Otherwise goroutine leak will occur.
|
||||
func FSHandler(root string, stripSlashes int) RequestHandler {
|
||||
fs := &FS{
|
||||
Root: root,
|
||||
IndexNames: []string{"index.html"},
|
||||
GenerateIndexPages: true,
|
||||
AcceptByteRange: true,
|
||||
}
|
||||
if stripSlashes > 0 {
|
||||
fs.PathRewrite = NewPathSlashesStripper(stripSlashes)
|
||||
}
|
||||
return fs.NewRequestHandler()
|
||||
}
|
||||
|
||||
// NewRequestHandler returns new request handler with the given FS settings.
|
||||
//
|
||||
// The returned handler caches requested file handles
|
||||
// for FS.CacheDuration.
|
||||
// Make sure your program has enough 'max open files' limit aka
|
||||
// 'ulimit -n' if FS.Root folder contains many files.
|
||||
//
|
||||
// Do not create multiple request handlers from a single FS instance -
|
||||
// just reuse a single request handler.
|
||||
func (fs *FS) NewRequestHandler() RequestHandler {
|
||||
fs.once.Do(fs.initRequestHandler)
|
||||
return fs.h
|
||||
}
|
||||
|
||||
func (fs *FS) initRequestHandler() {
|
||||
root := fs.Root
|
||||
|
||||
// serve files from the current working directory if root is empty
|
||||
if len(root) == 0 {
|
||||
root = "."
|
||||
}
|
||||
|
||||
// strip trailing slashes from the root path
|
||||
for len(root) > 0 && root[len(root)-1] == '/' {
|
||||
root = root[:len(root)-1]
|
||||
}
|
||||
|
||||
cacheDuration := fs.CacheDuration
|
||||
if cacheDuration <= 0 {
|
||||
cacheDuration = FSHandlerCacheDuration
|
||||
}
|
||||
compressedFileSuffix := fs.CompressedFileSuffix
|
||||
if len(compressedFileSuffix) == 0 {
|
||||
compressedFileSuffix = FSCompressedFileSuffix
|
||||
}
|
||||
|
||||
h := &fsHandler{
|
||||
root: root,
|
||||
indexNames: fs.IndexNames,
|
||||
pathRewrite: fs.PathRewrite,
|
||||
generateIndexPages: fs.GenerateIndexPages,
|
||||
compress: fs.Compress,
|
||||
acceptByteRange: fs.AcceptByteRange,
|
||||
cacheDuration: cacheDuration,
|
||||
compressedFileSuffix: compressedFileSuffix,
|
||||
cache: make(map[string]*fsFile),
|
||||
compressedCache: make(map[string]*fsFile),
|
||||
}
|
||||
|
||||
go func() {
|
||||
var pendingFiles []*fsFile
|
||||
for {
|
||||
time.Sleep(cacheDuration / 2)
|
||||
pendingFiles = h.cleanCache(pendingFiles)
|
||||
}
|
||||
}()
|
||||
|
||||
fs.h = h.handleRequest
|
||||
}
|
||||
|
||||
type fsHandler struct {
|
||||
root string
|
||||
indexNames []string
|
||||
pathRewrite PathRewriteFunc
|
||||
generateIndexPages bool
|
||||
compress bool
|
||||
acceptByteRange bool
|
||||
cacheDuration time.Duration
|
||||
compressedFileSuffix string
|
||||
|
||||
cache map[string]*fsFile
|
||||
compressedCache map[string]*fsFile
|
||||
cacheLock sync.Mutex
|
||||
|
||||
smallFileReaderPool sync.Pool
|
||||
}
|
||||
|
||||
type fsFile struct {
|
||||
h *fsHandler
|
||||
f *os.File
|
||||
dirIndex []byte
|
||||
contentType string
|
||||
contentLength int
|
||||
compressed bool
|
||||
|
||||
lastModified time.Time
|
||||
lastModifiedStr []byte
|
||||
|
||||
t time.Time
|
||||
readersCount int
|
||||
|
||||
bigFiles []*bigFileReader
|
||||
bigFilesLock sync.Mutex
|
||||
}
|
||||
|
||||
func (ff *fsFile) NewReader() (io.Reader, error) {
|
||||
if ff.isBig() {
|
||||
r, err := ff.bigFileReader()
|
||||
if err != nil {
|
||||
ff.decReadersCount()
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
return ff.smallFileReader(), nil
|
||||
}
|
||||
|
||||
func (ff *fsFile) smallFileReader() io.Reader {
|
||||
v := ff.h.smallFileReaderPool.Get()
|
||||
if v == nil {
|
||||
v = &fsSmallFileReader{}
|
||||
}
|
||||
r := v.(*fsSmallFileReader)
|
||||
r.ff = ff
|
||||
r.endPos = ff.contentLength
|
||||
if r.startPos > 0 {
|
||||
panic("BUG: fsSmallFileReader with non-nil startPos found in the pool")
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// files bigger than this size are sent with sendfile
|
||||
const maxSmallFileSize = 2 * 4096
|
||||
|
||||
func (ff *fsFile) isBig() bool {
|
||||
return ff.contentLength > maxSmallFileSize && len(ff.dirIndex) == 0
|
||||
}
|
||||
|
||||
func (ff *fsFile) bigFileReader() (io.Reader, error) {
|
||||
if ff.f == nil {
|
||||
panic("BUG: ff.f must be non-nil in bigFileReader")
|
||||
}
|
||||
|
||||
var r io.Reader
|
||||
|
||||
ff.bigFilesLock.Lock()
|
||||
n := len(ff.bigFiles)
|
||||
if n > 0 {
|
||||
r = ff.bigFiles[n-1]
|
||||
ff.bigFiles = ff.bigFiles[:n-1]
|
||||
}
|
||||
ff.bigFilesLock.Unlock()
|
||||
|
||||
if r != nil {
|
||||
return r, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(ff.f.Name())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open already opened file: %s", err)
|
||||
}
|
||||
return &bigFileReader{
|
||||
f: f,
|
||||
ff: ff,
|
||||
r: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (ff *fsFile) Release() {
|
||||
if ff.f != nil {
|
||||
ff.f.Close()
|
||||
|
||||
if ff.isBig() {
|
||||
ff.bigFilesLock.Lock()
|
||||
for _, r := range ff.bigFiles {
|
||||
r.f.Close()
|
||||
}
|
||||
ff.bigFilesLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ff *fsFile) decReadersCount() {
|
||||
ff.h.cacheLock.Lock()
|
||||
ff.readersCount--
|
||||
if ff.readersCount < 0 {
|
||||
panic("BUG: negative fsFile.readersCount!")
|
||||
}
|
||||
ff.h.cacheLock.Unlock()
|
||||
}
|
||||
|
||||
// bigFileReader attempts to trigger sendfile
|
||||
// for sending big files over the wire.
|
||||
type bigFileReader struct {
|
||||
f *os.File
|
||||
ff *fsFile
|
||||
r io.Reader
|
||||
lr io.LimitedReader
|
||||
}
|
||||
|
||||
func (r *bigFileReader) UpdateByteRange(startPos, endPos int) error {
|
||||
if _, err := r.f.Seek(int64(startPos), 0); err != nil {
|
||||
return err
|
||||
}
|
||||
r.r = &r.lr
|
||||
r.lr.R = r.f
|
||||
r.lr.N = int64(endPos - startPos + 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *bigFileReader) Read(p []byte) (int, error) {
|
||||
return r.r.Read(p)
|
||||
}
|
||||
|
||||
func (r *bigFileReader) WriteTo(w io.Writer) (int64, error) {
|
||||
if rf, ok := w.(io.ReaderFrom); ok {
|
||||
// fast path. Senfile must be triggered
|
||||
return rf.ReadFrom(r.r)
|
||||
}
|
||||
|
||||
// slow path
|
||||
return copyZeroAlloc(w, r.r)
|
||||
}
|
||||
|
||||
func (r *bigFileReader) Close() error {
|
||||
r.r = r.f
|
||||
n, err := r.f.Seek(0, 0)
|
||||
if err == nil {
|
||||
if n != 0 {
|
||||
panic("BUG: File.Seek(0,0) returned (non-zero, nil)")
|
||||
}
|
||||
|
||||
ff := r.ff
|
||||
ff.bigFilesLock.Lock()
|
||||
ff.bigFiles = append(ff.bigFiles, r)
|
||||
ff.bigFilesLock.Unlock()
|
||||
} else {
|
||||
r.f.Close()
|
||||
}
|
||||
r.ff.decReadersCount()
|
||||
return err
|
||||
}
|
||||
|
||||
type fsSmallFileReader struct {
|
||||
ff *fsFile
|
||||
startPos int
|
||||
endPos int
|
||||
}
|
||||
|
||||
func (r *fsSmallFileReader) Close() error {
|
||||
ff := r.ff
|
||||
ff.decReadersCount()
|
||||
r.ff = nil
|
||||
r.startPos = 0
|
||||
r.endPos = 0
|
||||
ff.h.smallFileReaderPool.Put(r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fsSmallFileReader) UpdateByteRange(startPos, endPos int) error {
|
||||
r.startPos = startPos
|
||||
r.endPos = endPos + 1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fsSmallFileReader) Read(p []byte) (int, error) {
|
||||
tailLen := r.endPos - r.startPos
|
||||
if tailLen <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if len(p) > tailLen {
|
||||
p = p[:tailLen]
|
||||
}
|
||||
|
||||
ff := r.ff
|
||||
if ff.f != nil {
|
||||
n, err := ff.f.ReadAt(p, int64(r.startPos))
|
||||
r.startPos += n
|
||||
return n, err
|
||||
}
|
||||
|
||||
n := copy(p, ff.dirIndex[r.startPos:])
|
||||
r.startPos += n
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *fsSmallFileReader) WriteTo(w io.Writer) (int64, error) {
|
||||
ff := r.ff
|
||||
|
||||
var n int
|
||||
var err error
|
||||
if ff.f == nil {
|
||||
n, err = w.Write(ff.dirIndex[r.startPos:r.endPos])
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
if rf, ok := w.(io.ReaderFrom); ok {
|
||||
return rf.ReadFrom(r)
|
||||
}
|
||||
|
||||
curPos := r.startPos
|
||||
bufv := copyBufPool.Get()
|
||||
buf := bufv.([]byte)
|
||||
for err == nil {
|
||||
tailLen := r.endPos - curPos
|
||||
if tailLen <= 0 {
|
||||
break
|
||||
}
|
||||
if len(buf) > tailLen {
|
||||
buf = buf[:tailLen]
|
||||
}
|
||||
n, err = ff.f.ReadAt(buf, int64(curPos))
|
||||
nw, errw := w.Write(buf[:n])
|
||||
curPos += nw
|
||||
if errw == nil && nw != n {
|
||||
panic("BUG: Write(p) returned (n, nil), where n != len(p)")
|
||||
}
|
||||
if err == nil {
|
||||
err = errw
|
||||
}
|
||||
}
|
||||
copyBufPool.Put(bufv)
|
||||
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
return int64(curPos - r.startPos), err
|
||||
}
|
||||
|
||||
func (h *fsHandler) cleanCache(pendingFiles []*fsFile) []*fsFile {
|
||||
var filesToRelease []*fsFile
|
||||
|
||||
h.cacheLock.Lock()
|
||||
|
||||
// Close files which couldn't be closed before due to non-zero
|
||||
// readers count on the previous run.
|
||||
var remainingFiles []*fsFile
|
||||
for _, ff := range pendingFiles {
|
||||
if ff.readersCount > 0 {
|
||||
remainingFiles = append(remainingFiles, ff)
|
||||
} else {
|
||||
filesToRelease = append(filesToRelease, ff)
|
||||
}
|
||||
}
|
||||
pendingFiles = remainingFiles
|
||||
|
||||
pendingFiles, filesToRelease = cleanCacheNolock(h.cache, pendingFiles, filesToRelease, h.cacheDuration)
|
||||
pendingFiles, filesToRelease = cleanCacheNolock(h.compressedCache, pendingFiles, filesToRelease, h.cacheDuration)
|
||||
|
||||
h.cacheLock.Unlock()
|
||||
|
||||
for _, ff := range filesToRelease {
|
||||
ff.Release()
|
||||
}
|
||||
|
||||
return pendingFiles
|
||||
}
|
||||
|
||||
func cleanCacheNolock(cache map[string]*fsFile, pendingFiles, filesToRelease []*fsFile, cacheDuration time.Duration) ([]*fsFile, []*fsFile) {
|
||||
t := time.Now()
|
||||
for k, ff := range cache {
|
||||
if t.Sub(ff.t) > cacheDuration {
|
||||
if ff.readersCount > 0 {
|
||||
// There are pending readers on stale file handle,
|
||||
// so we cannot close it. Put it into pendingFiles
|
||||
// so it will be closed later.
|
||||
pendingFiles = append(pendingFiles, ff)
|
||||
} else {
|
||||
filesToRelease = append(filesToRelease, ff)
|
||||
}
|
||||
delete(cache, k)
|
||||
}
|
||||
}
|
||||
return pendingFiles, filesToRelease
|
||||
}
|
||||
|
||||
func (h *fsHandler) handleRequest(ctx *RequestCtx) {
|
||||
var path []byte
|
||||
if h.pathRewrite != nil {
|
||||
path = h.pathRewrite(ctx)
|
||||
} else {
|
||||
path = ctx.Path()
|
||||
}
|
||||
path = stripTrailingSlashes(path)
|
||||
|
||||
if n := bytes.IndexByte(path, 0); n >= 0 {
|
||||
ctx.Logger().Printf("cannot serve path with nil byte at position %d: %q", n, path)
|
||||
ctx.Error("Are you a hacker?", StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if h.pathRewrite != nil {
|
||||
// There is no need to check for '/../' if path = ctx.Path(),
|
||||
// since ctx.Path must normalize and sanitize the path.
|
||||
|
||||
if n := bytes.Index(path, strSlashDotDotSlash); n >= 0 {
|
||||
ctx.Logger().Printf("cannot serve path with '/../' at position %d due to security reasons: %q", n, path)
|
||||
ctx.Error("Internal Server Error", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mustCompress := false
|
||||
fileCache := h.cache
|
||||
byteRange := ctx.Request.Header.peek(strRange)
|
||||
if len(byteRange) == 0 && h.compress && ctx.Request.Header.HasAcceptEncodingBytes(strGzip) {
|
||||
mustCompress = true
|
||||
fileCache = h.compressedCache
|
||||
}
|
||||
|
||||
h.cacheLock.Lock()
|
||||
ff, ok := fileCache[string(path)]
|
||||
if ok {
|
||||
ff.readersCount++
|
||||
}
|
||||
h.cacheLock.Unlock()
|
||||
|
||||
if !ok {
|
||||
pathStr := string(path)
|
||||
filePath := h.root + pathStr
|
||||
var err error
|
||||
ff, err = h.openFSFile(filePath, mustCompress)
|
||||
if mustCompress && err == errNoCreatePermission {
|
||||
ctx.Logger().Printf("insufficient permissions for saving compressed file for %q. Serving uncompressed file. "+
|
||||
"Allow write access to the directory with this file in order to improve fasthttp performance", filePath)
|
||||
mustCompress = false
|
||||
ff, err = h.openFSFile(filePath, mustCompress)
|
||||
}
|
||||
if err == errDirIndexRequired {
|
||||
ff, err = h.openIndexFile(ctx, filePath, mustCompress)
|
||||
if err != nil {
|
||||
ctx.Logger().Printf("cannot open dir index %q: %s", filePath, err)
|
||||
ctx.Error("Directory index is forbidden", StatusForbidden)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
ctx.Logger().Printf("cannot open file %q: %s", filePath, err)
|
||||
ctx.Error("Cannot open requested path", StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h.cacheLock.Lock()
|
||||
ff1, ok := fileCache[pathStr]
|
||||
if !ok {
|
||||
fileCache[pathStr] = ff
|
||||
ff.readersCount++
|
||||
} else {
|
||||
ff1.readersCount++
|
||||
}
|
||||
h.cacheLock.Unlock()
|
||||
|
||||
if ok {
|
||||
// The file has been already opened by another
|
||||
// goroutine, so close the current file and use
|
||||
// the file opened by another goroutine instead.
|
||||
ff.Release()
|
||||
ff = ff1
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.IfModifiedSince(ff.lastModified) {
|
||||
ff.decReadersCount()
|
||||
ctx.NotModified()
|
||||
return
|
||||
}
|
||||
|
||||
r, err := ff.NewReader()
|
||||
if err != nil {
|
||||
ctx.Logger().Printf("cannot obtain file reader for path=%q: %s", path, err)
|
||||
ctx.Error("Internal Server Error", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hdr := &ctx.Response.Header
|
||||
if ff.compressed {
|
||||
hdr.SetCanonical(strContentEncoding, strGzip)
|
||||
}
|
||||
|
||||
statusCode := StatusOK
|
||||
contentLength := ff.contentLength
|
||||
if h.acceptByteRange {
|
||||
hdr.SetCanonical(strAcceptRanges, strBytes)
|
||||
if len(byteRange) > 0 {
|
||||
startPos, endPos, err := ParseByteRange(byteRange, contentLength)
|
||||
if err != nil {
|
||||
r.(io.Closer).Close()
|
||||
ctx.Logger().Printf("cannot parse byte range %q for path=%q: %s", byteRange, path, err)
|
||||
ctx.Error("Range Not Satisfiable", StatusRequestedRangeNotSatisfiable)
|
||||
return
|
||||
}
|
||||
|
||||
if err = r.(byteRangeUpdater).UpdateByteRange(startPos, endPos); err != nil {
|
||||
r.(io.Closer).Close()
|
||||
ctx.Logger().Printf("cannot seek byte range %q for path=%q: %s", byteRange, path, err)
|
||||
ctx.Error("Internal Server Error", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
hdr.SetContentRange(startPos, endPos, contentLength)
|
||||
contentLength = endPos - startPos + 1
|
||||
statusCode = StatusPartialContent
|
||||
}
|
||||
}
|
||||
|
||||
hdr.SetCanonical(strLastModified, ff.lastModifiedStr)
|
||||
if !ctx.IsHead() {
|
||||
ctx.SetBodyStream(r, contentLength)
|
||||
} else {
|
||||
ctx.Response.ResetBody()
|
||||
ctx.Response.SkipBody = true
|
||||
ctx.Response.Header.SetContentLength(contentLength)
|
||||
if rc, ok := r.(io.Closer); ok {
|
||||
if err := rc.Close(); err != nil {
|
||||
ctx.Logger().Printf("cannot close file reader: %s", err)
|
||||
ctx.Error("Internal Server Error", StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.SetContentType(ff.contentType)
|
||||
ctx.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
type byteRangeUpdater interface {
|
||||
UpdateByteRange(startPos, endPos int) error
|
||||
}
|
||||
|
||||
// ParseByteRange parses 'Range: bytes=...' header value.
|
||||
//
|
||||
// It follows https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35 .
|
||||
func ParseByteRange(byteRange []byte, contentLength int) (startPos, endPos int, err error) {
|
||||
b := byteRange
|
||||
if !bytes.HasPrefix(b, strBytes) {
|
||||
return 0, 0, fmt.Errorf("unsupported range units: %q. Expecting %q", byteRange, strBytes)
|
||||
}
|
||||
|
||||
b = b[len(strBytes):]
|
||||
if len(b) == 0 || b[0] != '=' {
|
||||
return 0, 0, fmt.Errorf("missing byte range in %q", byteRange)
|
||||
}
|
||||
b = b[1:]
|
||||
|
||||
n := bytes.IndexByte(b, '-')
|
||||
if n < 0 {
|
||||
return 0, 0, fmt.Errorf("missing the end position of byte range in %q", byteRange)
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
v, err := ParseUint(b[n+1:])
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
startPos := contentLength - v
|
||||
if startPos < 0 {
|
||||
startPos = 0
|
||||
}
|
||||
return startPos, contentLength - 1, nil
|
||||
}
|
||||
|
||||
if startPos, err = ParseUint(b[:n]); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if startPos >= contentLength {
|
||||
return 0, 0, fmt.Errorf("the start position of byte range cannot exceed %d. byte range %q", contentLength-1, byteRange)
|
||||
}
|
||||
|
||||
b = b[n+1:]
|
||||
if len(b) == 0 {
|
||||
return startPos, contentLength - 1, nil
|
||||
}
|
||||
|
||||
if endPos, err = ParseUint(b); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if endPos >= contentLength {
|
||||
endPos = contentLength - 1
|
||||
}
|
||||
if endPos < startPos {
|
||||
return 0, 0, fmt.Errorf("the start position of byte range cannot exceed the end position. byte range %q", byteRange)
|
||||
}
|
||||
return startPos, endPos, nil
|
||||
}
|
||||
|
||||
func (h *fsHandler) openIndexFile(ctx *RequestCtx, dirPath string, mustCompress bool) (*fsFile, error) {
|
||||
for _, indexName := range h.indexNames {
|
||||
indexFilePath := dirPath + "/" + indexName
|
||||
ff, err := h.openFSFile(indexFilePath, mustCompress)
|
||||
if err == nil {
|
||||
return ff, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("cannot open file %q: %s", indexFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !h.generateIndexPages {
|
||||
return nil, fmt.Errorf("cannot access directory without index page. Directory %q", dirPath)
|
||||
}
|
||||
|
||||
return h.createDirIndex(ctx.URI(), dirPath, mustCompress)
|
||||
}
|
||||
|
||||
var (
|
||||
errDirIndexRequired = errors.New("directory index required")
|
||||
errNoCreatePermission = errors.New("no 'create file' permissions")
|
||||
)
|
||||
|
||||
func (h *fsHandler) createDirIndex(base *URI, dirPath string, mustCompress bool) (*fsFile, error) {
|
||||
w := &ByteBuffer{}
|
||||
|
||||
basePathEscaped := html.EscapeString(string(base.Path()))
|
||||
fmt.Fprintf(w, "<html><head><title>%s</title><style>.dir { font-weight: bold }</style></head><body>", basePathEscaped)
|
||||
fmt.Fprintf(w, "<h1>%s</h1>", basePathEscaped)
|
||||
fmt.Fprintf(w, "<ul>")
|
||||
|
||||
if len(basePathEscaped) > 1 {
|
||||
var parentURI URI
|
||||
base.CopyTo(&parentURI)
|
||||
parentURI.Update(string(base.Path()) + "/..")
|
||||
parentPathEscaped := html.EscapeString(string(parentURI.Path()))
|
||||
fmt.Fprintf(w, `<li><a href="%s" class="dir">..</a></li>`, parentPathEscaped)
|
||||
}
|
||||
|
||||
f, err := os.Open(dirPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileinfos, err := f.Readdir(0)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fm := make(map[string]os.FileInfo, len(fileinfos))
|
||||
var filenames []string
|
||||
for _, fi := range fileinfos {
|
||||
name := fi.Name()
|
||||
if strings.HasSuffix(name, h.compressedFileSuffix) {
|
||||
// Do not show compressed files on index page.
|
||||
continue
|
||||
}
|
||||
fm[name] = fi
|
||||
filenames = append(filenames, name)
|
||||
}
|
||||
|
||||
var u URI
|
||||
base.CopyTo(&u)
|
||||
u.Update(string(u.Path()) + "/")
|
||||
|
||||
sort.Sort(sort.StringSlice(filenames))
|
||||
for _, name := range filenames {
|
||||
u.Update(name)
|
||||
pathEscaped := html.EscapeString(string(u.Path()))
|
||||
fi := fm[name]
|
||||
auxStr := "dir"
|
||||
className := "dir"
|
||||
if !fi.IsDir() {
|
||||
auxStr = fmt.Sprintf("file, %d bytes", fi.Size())
|
||||
className = "file"
|
||||
}
|
||||
fmt.Fprintf(w, `<li><a href="%s" class="%s">%s</a>, %s, last modified %s</li>`,
|
||||
pathEscaped, className, html.EscapeString(name), auxStr, fsModTime(fi.ModTime()))
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "</ul></body></html>")
|
||||
|
||||
if mustCompress {
|
||||
var zbuf ByteBuffer
|
||||
zbuf.B = AppendGzipBytesLevel(zbuf.B, w.B, CompressDefaultCompression)
|
||||
w = &zbuf
|
||||
}
|
||||
|
||||
dirIndex := w.B
|
||||
lastModified := time.Now()
|
||||
ff := &fsFile{
|
||||
h: h,
|
||||
dirIndex: dirIndex,
|
||||
contentType: "text/html; charset=utf-8",
|
||||
contentLength: len(dirIndex),
|
||||
compressed: mustCompress,
|
||||
lastModified: lastModified,
|
||||
lastModifiedStr: AppendHTTPDate(nil, lastModified),
|
||||
|
||||
t: lastModified,
|
||||
}
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
const (
|
||||
fsMinCompressRatio = 0.8
|
||||
fsMaxCompressibleFileSize = 8 * 1024 * 1024
|
||||
)
|
||||
|
||||
func (h *fsHandler) compressAndOpenFSFile(filePath string) (*fsFile, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot obtain info for file %q: %s", filePath, err)
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
f.Close()
|
||||
return nil, errDirIndexRequired
|
||||
}
|
||||
|
||||
if strings.HasSuffix(filePath, h.compressedFileSuffix) ||
|
||||
fileInfo.Size() > fsMaxCompressibleFileSize ||
|
||||
!isFileCompressible(f, fsMinCompressRatio) {
|
||||
return h.newFSFile(f, fileInfo, false)
|
||||
}
|
||||
|
||||
compressedFilePath := filePath + h.compressedFileSuffix
|
||||
absPath, err := filepath.Abs(compressedFilePath)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot determine absolute path for %q: %s", compressedFilePath, err)
|
||||
}
|
||||
|
||||
flock := getFileLock(absPath)
|
||||
flock.Lock()
|
||||
ff, err := h.compressFileNolock(f, fileInfo, filePath, compressedFilePath)
|
||||
flock.Unlock()
|
||||
|
||||
return ff, err
|
||||
}
|
||||
|
||||
func (h *fsHandler) compressFileNolock(f *os.File, fileInfo os.FileInfo, filePath, compressedFilePath string) (*fsFile, error) {
|
||||
// Attempt to open compressed file created by another concurrent
|
||||
// goroutine.
|
||||
// It is safe opening such a file, since the file creation
|
||||
// is guarded by file mutex - see getFileLock call.
|
||||
if _, err := os.Stat(compressedFilePath); err == nil {
|
||||
f.Close()
|
||||
return h.newCompressedFSFile(compressedFilePath)
|
||||
}
|
||||
|
||||
// Create temporary file, so concurrent goroutines don't use
|
||||
// it until it is created.
|
||||
tmpFilePath := compressedFilePath + ".tmp"
|
||||
zf, err := os.Create(tmpFilePath)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
if !os.IsPermission(err) {
|
||||
return nil, fmt.Errorf("cannot create temporary file %q: %s", tmpFilePath, err)
|
||||
}
|
||||
return nil, errNoCreatePermission
|
||||
}
|
||||
|
||||
zw := acquireStacklessGzipWriter(zf, CompressDefaultCompression)
|
||||
_, err = copyZeroAlloc(zw, f)
|
||||
if err1 := zw.Flush(); err == nil {
|
||||
err = err1
|
||||
}
|
||||
releaseStacklessGzipWriter(zw, CompressDefaultCompression)
|
||||
zf.Close()
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error when compressing file %q to %q: %s", filePath, tmpFilePath, err)
|
||||
}
|
||||
if err = os.Chtimes(tmpFilePath, time.Now(), fileInfo.ModTime()); err != nil {
|
||||
return nil, fmt.Errorf("cannot change modification time to %s for tmp file %q: %s",
|
||||
fileInfo.ModTime(), tmpFilePath, err)
|
||||
}
|
||||
if err = os.Rename(tmpFilePath, compressedFilePath); err != nil {
|
||||
return nil, fmt.Errorf("cannot move compressed file from %q to %q: %s", tmpFilePath, compressedFilePath, err)
|
||||
}
|
||||
return h.newCompressedFSFile(compressedFilePath)
|
||||
}
|
||||
|
||||
func (h *fsHandler) newCompressedFSFile(filePath string) (*fsFile, error) {
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot open compressed file %q: %s", filePath, err)
|
||||
}
|
||||
fileInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot obtain info for compressed file %q: %s", filePath, err)
|
||||
}
|
||||
return h.newFSFile(f, fileInfo, true)
|
||||
}
|
||||
|
||||
func (h *fsHandler) openFSFile(filePath string, mustCompress bool) (*fsFile, error) {
|
||||
filePathOriginal := filePath
|
||||
if mustCompress {
|
||||
filePath += h.compressedFileSuffix
|
||||
}
|
||||
|
||||
f, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
if mustCompress && os.IsNotExist(err) {
|
||||
return h.compressAndOpenFSFile(filePathOriginal)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fileInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot obtain info for file %q: %s", filePath, err)
|
||||
}
|
||||
|
||||
if fileInfo.IsDir() {
|
||||
f.Close()
|
||||
if mustCompress {
|
||||
return nil, fmt.Errorf("directory with unexpected suffix found: %q. Suffix: %q",
|
||||
filePath, h.compressedFileSuffix)
|
||||
}
|
||||
return nil, errDirIndexRequired
|
||||
}
|
||||
|
||||
if mustCompress {
|
||||
fileInfoOriginal, err := os.Stat(filePathOriginal)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("cannot obtain info for original file %q: %s", filePathOriginal, err)
|
||||
}
|
||||
|
||||
if fileInfoOriginal.ModTime() != fileInfo.ModTime() {
|
||||
// The compressed file became stale. Re-create it.
|
||||
f.Close()
|
||||
os.Remove(filePath)
|
||||
return h.compressAndOpenFSFile(filePathOriginal)
|
||||
}
|
||||
}
|
||||
|
||||
return h.newFSFile(f, fileInfo, mustCompress)
|
||||
}
|
||||
|
||||
func (h *fsHandler) newFSFile(f *os.File, fileInfo os.FileInfo, compressed bool) (*fsFile, error) {
|
||||
n := fileInfo.Size()
|
||||
contentLength := int(n)
|
||||
if n != int64(contentLength) {
|
||||
f.Close()
|
||||
return nil, fmt.Errorf("too big file: %d bytes", n)
|
||||
}
|
||||
|
||||
// detect content-type
|
||||
ext := fileExtension(fileInfo.Name(), compressed, h.compressedFileSuffix)
|
||||
contentType := mime.TypeByExtension(ext)
|
||||
if len(contentType) == 0 {
|
||||
data, err := readFileHeader(f, compressed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read header of the file %q: %s", f.Name(), err)
|
||||
}
|
||||
contentType = http.DetectContentType(data)
|
||||
}
|
||||
|
||||
lastModified := fileInfo.ModTime()
|
||||
ff := &fsFile{
|
||||
h: h,
|
||||
f: f,
|
||||
contentType: contentType,
|
||||
contentLength: contentLength,
|
||||
compressed: compressed,
|
||||
lastModified: lastModified,
|
||||
lastModifiedStr: AppendHTTPDate(nil, lastModified),
|
||||
|
||||
t: time.Now(),
|
||||
}
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func readFileHeader(f *os.File, compressed bool) ([]byte, error) {
|
||||
r := io.Reader(f)
|
||||
var zr *gzip.Reader
|
||||
if compressed {
|
||||
var err error
|
||||
if zr, err = acquireGzipReader(f); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = zr
|
||||
}
|
||||
|
||||
lr := &io.LimitedReader{
|
||||
R: r,
|
||||
N: 512,
|
||||
}
|
||||
data, err := ioutil.ReadAll(lr)
|
||||
f.Seek(0, 0)
|
||||
|
||||
if zr != nil {
|
||||
releaseGzipReader(zr)
|
||||
}
|
||||
|
||||
return data, err
|
||||
}
|
||||
|
||||
func stripLeadingSlashes(path []byte, stripSlashes int) []byte {
|
||||
for stripSlashes > 0 && len(path) > 0 {
|
||||
if path[0] != '/' {
|
||||
panic("BUG: path must start with slash")
|
||||
}
|
||||
n := bytes.IndexByte(path[1:], '/')
|
||||
if n < 0 {
|
||||
path = path[:0]
|
||||
break
|
||||
}
|
||||
path = path[n+1:]
|
||||
stripSlashes--
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func stripTrailingSlashes(path []byte) []byte {
|
||||
for len(path) > 0 && path[len(path)-1] == '/' {
|
||||
path = path[:len(path)-1]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func fileExtension(path string, compressed bool, compressedFileSuffix string) string {
|
||||
if compressed && strings.HasSuffix(path, compressedFileSuffix) {
|
||||
path = path[:len(path)-len(compressedFileSuffix)]
|
||||
}
|
||||
n := strings.LastIndexByte(path, '.')
|
||||
if n < 0 {
|
||||
return ""
|
||||
}
|
||||
return path[n:]
|
||||
}
|
||||
|
||||
// FileLastModified returns last modified time for the file.
|
||||
func FileLastModified(path string) (time.Time, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return zeroTime, err
|
||||
}
|
||||
fileInfo, err := f.Stat()
|
||||
f.Close()
|
||||
if err != nil {
|
||||
return zeroTime, err
|
||||
}
|
||||
return fsModTime(fileInfo.ModTime()), nil
|
||||
}
|
||||
|
||||
func fsModTime(t time.Time) time.Time {
|
||||
return t.In(time.UTC).Truncate(time.Second)
|
||||
}
|
||||
|
||||
var (
|
||||
filesLockMap = make(map[string]*sync.Mutex)
|
||||
filesLockMapLock sync.Mutex
|
||||
)
|
||||
|
||||
func getFileLock(absPath string) *sync.Mutex {
|
||||
filesLockMapLock.Lock()
|
||||
flock := filesLockMap[absPath]
|
||||
if flock == nil {
|
||||
flock = &sync.Mutex{}
|
||||
filesLockMap[absPath] = flock
|
||||
}
|
||||
filesLockMapLock.Unlock()
|
||||
return flock
|
||||
}
|
||||
+2091
@@ -0,0 +1,2091 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResponseHeader represents HTTP response header.
|
||||
//
|
||||
// It is forbidden copying ResponseHeader instances.
|
||||
// Create new instances instead and use CopyTo.
|
||||
//
|
||||
// ResponseHeader instance MUST NOT be used from concurrently running
|
||||
// goroutines.
|
||||
type ResponseHeader struct {
|
||||
noCopy noCopy
|
||||
|
||||
disableNormalizing bool
|
||||
noHTTP11 bool
|
||||
connectionClose bool
|
||||
|
||||
statusCode int
|
||||
contentLength int
|
||||
contentLengthBytes []byte
|
||||
|
||||
contentType []byte
|
||||
server []byte
|
||||
|
||||
h []argsKV
|
||||
bufKV argsKV
|
||||
|
||||
cookies []argsKV
|
||||
}
|
||||
|
||||
// RequestHeader represents HTTP request header.
|
||||
//
|
||||
// It is forbidden copying RequestHeader instances.
|
||||
// Create new instances instead and use CopyTo.
|
||||
//
|
||||
// RequestHeader instance MUST NOT be used from concurrently running
|
||||
// goroutines.
|
||||
type RequestHeader struct {
|
||||
noCopy noCopy
|
||||
|
||||
disableNormalizing bool
|
||||
noHTTP11 bool
|
||||
connectionClose bool
|
||||
isGet bool
|
||||
|
||||
// These two fields have been moved close to other bool fields
|
||||
// for reducing RequestHeader object size.
|
||||
cookiesCollected bool
|
||||
rawHeadersParsed bool
|
||||
|
||||
contentLength int
|
||||
contentLengthBytes []byte
|
||||
|
||||
method []byte
|
||||
requestURI []byte
|
||||
host []byte
|
||||
contentType []byte
|
||||
userAgent []byte
|
||||
|
||||
h []argsKV
|
||||
bufKV argsKV
|
||||
|
||||
cookies []argsKV
|
||||
|
||||
rawHeaders []byte
|
||||
}
|
||||
|
||||
// SetContentRange sets 'Content-Range: bytes startPos-endPos/contentLength'
|
||||
// header.
|
||||
func (h *ResponseHeader) SetContentRange(startPos, endPos, contentLength int) {
|
||||
b := h.bufKV.value[:0]
|
||||
b = append(b, strBytes...)
|
||||
b = append(b, ' ')
|
||||
b = AppendUint(b, startPos)
|
||||
b = append(b, '-')
|
||||
b = AppendUint(b, endPos)
|
||||
b = append(b, '/')
|
||||
b = AppendUint(b, contentLength)
|
||||
h.bufKV.value = b
|
||||
|
||||
h.SetCanonical(strContentRange, h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetByteRange sets 'Range: bytes=startPos-endPos' header.
|
||||
//
|
||||
// * If startPos is negative, then 'bytes=-startPos' value is set.
|
||||
// * If endPos is negative, then 'bytes=startPos-' value is set.
|
||||
func (h *RequestHeader) SetByteRange(startPos, endPos int) {
|
||||
h.parseRawHeaders()
|
||||
|
||||
b := h.bufKV.value[:0]
|
||||
b = append(b, strBytes...)
|
||||
b = append(b, '=')
|
||||
if startPos >= 0 {
|
||||
b = AppendUint(b, startPos)
|
||||
} else {
|
||||
endPos = -startPos
|
||||
}
|
||||
b = append(b, '-')
|
||||
if endPos >= 0 {
|
||||
b = AppendUint(b, endPos)
|
||||
}
|
||||
h.bufKV.value = b
|
||||
|
||||
h.SetCanonical(strRange, h.bufKV.value)
|
||||
}
|
||||
|
||||
// StatusCode returns response status code.
|
||||
func (h *ResponseHeader) StatusCode() int {
|
||||
if h.statusCode == 0 {
|
||||
return StatusOK
|
||||
}
|
||||
return h.statusCode
|
||||
}
|
||||
|
||||
// SetStatusCode sets response status code.
|
||||
func (h *ResponseHeader) SetStatusCode(statusCode int) {
|
||||
h.statusCode = statusCode
|
||||
}
|
||||
|
||||
// SetLastModified sets 'Last-Modified' header to the given value.
|
||||
func (h *ResponseHeader) SetLastModified(t time.Time) {
|
||||
h.bufKV.value = AppendHTTPDate(h.bufKV.value[:0], t)
|
||||
h.SetCanonical(strLastModified, h.bufKV.value)
|
||||
}
|
||||
|
||||
// ConnectionClose returns true if 'Connection: close' header is set.
|
||||
func (h *ResponseHeader) ConnectionClose() bool {
|
||||
return h.connectionClose
|
||||
}
|
||||
|
||||
// SetConnectionClose sets 'Connection: close' header.
|
||||
func (h *ResponseHeader) SetConnectionClose() {
|
||||
h.connectionClose = true
|
||||
}
|
||||
|
||||
// ResetConnectionClose clears 'Connection: close' header if it exists.
|
||||
func (h *ResponseHeader) ResetConnectionClose() {
|
||||
if h.connectionClose {
|
||||
h.connectionClose = false
|
||||
h.h = delAllArgsBytes(h.h, strConnection)
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectionClose returns true if 'Connection: close' header is set.
|
||||
func (h *RequestHeader) ConnectionClose() bool {
|
||||
h.parseRawHeaders()
|
||||
return h.connectionClose
|
||||
}
|
||||
|
||||
func (h *RequestHeader) connectionCloseFast() bool {
|
||||
// h.parseRawHeaders() isn't called for performance reasons.
|
||||
// Use ConnectionClose for triggering raw headers parsing.
|
||||
return h.connectionClose
|
||||
}
|
||||
|
||||
// SetConnectionClose sets 'Connection: close' header.
|
||||
func (h *RequestHeader) SetConnectionClose() {
|
||||
// h.parseRawHeaders() isn't called for performance reasons.
|
||||
h.connectionClose = true
|
||||
}
|
||||
|
||||
// ResetConnectionClose clears 'Connection: close' header if it exists.
|
||||
func (h *RequestHeader) ResetConnectionClose() {
|
||||
h.parseRawHeaders()
|
||||
if h.connectionClose {
|
||||
h.connectionClose = false
|
||||
h.h = delAllArgsBytes(h.h, strConnection)
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectionUpgrade returns true if 'Connection: Upgrade' header is set.
|
||||
func (h *ResponseHeader) ConnectionUpgrade() bool {
|
||||
return hasHeaderValue(h.Peek("Connection"), strUpgrade)
|
||||
}
|
||||
|
||||
// ConnectionUpgrade returns true if 'Connection: Upgrade' header is set.
|
||||
func (h *RequestHeader) ConnectionUpgrade() bool {
|
||||
h.parseRawHeaders()
|
||||
return hasHeaderValue(h.Peek("Connection"), strUpgrade)
|
||||
}
|
||||
|
||||
// ContentLength returns Content-Length header value.
|
||||
//
|
||||
// It may be negative:
|
||||
// -1 means Transfer-Encoding: chunked.
|
||||
// -2 means Transfer-Encoding: identity.
|
||||
func (h *ResponseHeader) ContentLength() int {
|
||||
return h.contentLength
|
||||
}
|
||||
|
||||
// SetContentLength sets Content-Length header value.
|
||||
//
|
||||
// Content-Length may be negative:
|
||||
// -1 means Transfer-Encoding: chunked.
|
||||
// -2 means Transfer-Encoding: identity.
|
||||
func (h *ResponseHeader) SetContentLength(contentLength int) {
|
||||
if h.mustSkipContentLength() {
|
||||
return
|
||||
}
|
||||
h.contentLength = contentLength
|
||||
if contentLength >= 0 {
|
||||
h.contentLengthBytes = AppendUint(h.contentLengthBytes[:0], contentLength)
|
||||
h.h = delAllArgsBytes(h.h, strTransferEncoding)
|
||||
} else {
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
value := strChunked
|
||||
if contentLength == -2 {
|
||||
h.SetConnectionClose()
|
||||
value = strIdentity
|
||||
}
|
||||
h.h = setArgBytes(h.h, strTransferEncoding, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) mustSkipContentLength() bool {
|
||||
// From http/1.1 specs:
|
||||
// All 1xx (informational), 204 (no content), and 304 (not modified) responses MUST NOT include a message-body
|
||||
statusCode := h.StatusCode()
|
||||
|
||||
// Fast path.
|
||||
if statusCode < 100 || statusCode == StatusOK {
|
||||
return false
|
||||
}
|
||||
|
||||
// Slow path.
|
||||
return statusCode == StatusNotModified || statusCode == StatusNoContent || statusCode < 200
|
||||
}
|
||||
|
||||
// ContentLength returns Content-Length header value.
|
||||
//
|
||||
// It may be negative:
|
||||
// -1 means Transfer-Encoding: chunked.
|
||||
func (h *RequestHeader) ContentLength() int {
|
||||
if h.noBody() {
|
||||
return 0
|
||||
}
|
||||
h.parseRawHeaders()
|
||||
return h.contentLength
|
||||
}
|
||||
|
||||
// SetContentLength sets Content-Length header value.
|
||||
//
|
||||
// Negative content-length sets 'Transfer-Encoding: chunked' header.
|
||||
func (h *RequestHeader) SetContentLength(contentLength int) {
|
||||
h.parseRawHeaders()
|
||||
h.contentLength = contentLength
|
||||
if contentLength >= 0 {
|
||||
h.contentLengthBytes = AppendUint(h.contentLengthBytes[:0], contentLength)
|
||||
h.h = delAllArgsBytes(h.h, strTransferEncoding)
|
||||
} else {
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
h.h = setArgBytes(h.h, strTransferEncoding, strChunked)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) isCompressibleContentType() bool {
|
||||
contentType := h.ContentType()
|
||||
return bytes.HasPrefix(contentType, strTextSlash) ||
|
||||
bytes.HasPrefix(contentType, strApplicationSlash)
|
||||
}
|
||||
|
||||
// ContentType returns Content-Type header value.
|
||||
func (h *ResponseHeader) ContentType() []byte {
|
||||
contentType := h.contentType
|
||||
if len(h.contentType) == 0 {
|
||||
contentType = defaultContentType
|
||||
}
|
||||
return contentType
|
||||
}
|
||||
|
||||
// SetContentType sets Content-Type header value.
|
||||
func (h *ResponseHeader) SetContentType(contentType string) {
|
||||
h.contentType = append(h.contentType[:0], contentType...)
|
||||
}
|
||||
|
||||
// SetContentTypeBytes sets Content-Type header value.
|
||||
func (h *ResponseHeader) SetContentTypeBytes(contentType []byte) {
|
||||
h.contentType = append(h.contentType[:0], contentType...)
|
||||
}
|
||||
|
||||
// Server returns Server header value.
|
||||
func (h *ResponseHeader) Server() []byte {
|
||||
return h.server
|
||||
}
|
||||
|
||||
// SetServer sets Server header value.
|
||||
func (h *ResponseHeader) SetServer(server string) {
|
||||
h.server = append(h.server[:0], server...)
|
||||
}
|
||||
|
||||
// SetServerBytes sets Server header value.
|
||||
func (h *ResponseHeader) SetServerBytes(server []byte) {
|
||||
h.server = append(h.server[:0], server...)
|
||||
}
|
||||
|
||||
// ContentType returns Content-Type header value.
|
||||
func (h *RequestHeader) ContentType() []byte {
|
||||
h.parseRawHeaders()
|
||||
return h.contentType
|
||||
}
|
||||
|
||||
// SetContentType sets Content-Type header value.
|
||||
func (h *RequestHeader) SetContentType(contentType string) {
|
||||
h.parseRawHeaders()
|
||||
h.contentType = append(h.contentType[:0], contentType...)
|
||||
}
|
||||
|
||||
// SetContentTypeBytes sets Content-Type header value.
|
||||
func (h *RequestHeader) SetContentTypeBytes(contentType []byte) {
|
||||
h.parseRawHeaders()
|
||||
h.contentType = append(h.contentType[:0], contentType...)
|
||||
}
|
||||
|
||||
// SetMultipartFormBoundary sets the following Content-Type:
|
||||
// 'multipart/form-data; boundary=...'
|
||||
// where ... is substituted by the given boundary.
|
||||
func (h *RequestHeader) SetMultipartFormBoundary(boundary string) {
|
||||
h.parseRawHeaders()
|
||||
|
||||
b := h.bufKV.value[:0]
|
||||
b = append(b, strMultipartFormData...)
|
||||
b = append(b, ';', ' ')
|
||||
b = append(b, strBoundary...)
|
||||
b = append(b, '=')
|
||||
b = append(b, boundary...)
|
||||
h.bufKV.value = b
|
||||
|
||||
h.SetContentTypeBytes(h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetMultipartFormBoundaryBytes sets the following Content-Type:
|
||||
// 'multipart/form-data; boundary=...'
|
||||
// where ... is substituted by the given boundary.
|
||||
func (h *RequestHeader) SetMultipartFormBoundaryBytes(boundary []byte) {
|
||||
h.parseRawHeaders()
|
||||
|
||||
b := h.bufKV.value[:0]
|
||||
b = append(b, strMultipartFormData...)
|
||||
b = append(b, ';', ' ')
|
||||
b = append(b, strBoundary...)
|
||||
b = append(b, '=')
|
||||
b = append(b, boundary...)
|
||||
h.bufKV.value = b
|
||||
|
||||
h.SetContentTypeBytes(h.bufKV.value)
|
||||
}
|
||||
|
||||
// MultipartFormBoundary returns boundary part
|
||||
// from 'multipart/form-data; boundary=...' Content-Type.
|
||||
func (h *RequestHeader) MultipartFormBoundary() []byte {
|
||||
b := h.ContentType()
|
||||
if !bytes.HasPrefix(b, strMultipartFormData) {
|
||||
return nil
|
||||
}
|
||||
b = b[len(strMultipartFormData):]
|
||||
if len(b) == 0 || b[0] != ';' {
|
||||
return nil
|
||||
}
|
||||
|
||||
var n int
|
||||
for len(b) > 0 {
|
||||
n++
|
||||
for len(b) > n && b[n] == ' ' {
|
||||
n++
|
||||
}
|
||||
b = b[n:]
|
||||
if !bytes.HasPrefix(b, strBoundary) {
|
||||
if n = bytes.IndexByte(b, ';'); n < 0 {
|
||||
return nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
b = b[len(strBoundary):]
|
||||
if len(b) == 0 || b[0] != '=' {
|
||||
return nil
|
||||
}
|
||||
b = b[1:]
|
||||
if n = bytes.IndexByte(b, ';'); n >= 0 {
|
||||
b = b[:n]
|
||||
}
|
||||
if len(b) > 1 && b[0] == '"' && b[len(b)-1] == '"' {
|
||||
b = b[1 : len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Host returns Host header value.
|
||||
func (h *RequestHeader) Host() []byte {
|
||||
if len(h.host) > 0 {
|
||||
return h.host
|
||||
}
|
||||
if !h.rawHeadersParsed {
|
||||
// fast path without employing full headers parsing.
|
||||
host := peekRawHeader(h.rawHeaders, strHost)
|
||||
if len(host) > 0 {
|
||||
h.host = append(h.host[:0], host...)
|
||||
return h.host
|
||||
}
|
||||
}
|
||||
|
||||
// slow path.
|
||||
h.parseRawHeaders()
|
||||
return h.host
|
||||
}
|
||||
|
||||
// SetHost sets Host header value.
|
||||
func (h *RequestHeader) SetHost(host string) {
|
||||
h.parseRawHeaders()
|
||||
h.host = append(h.host[:0], host...)
|
||||
}
|
||||
|
||||
// SetHostBytes sets Host header value.
|
||||
func (h *RequestHeader) SetHostBytes(host []byte) {
|
||||
h.parseRawHeaders()
|
||||
h.host = append(h.host[:0], host...)
|
||||
}
|
||||
|
||||
// UserAgent returns User-Agent header value.
|
||||
func (h *RequestHeader) UserAgent() []byte {
|
||||
h.parseRawHeaders()
|
||||
return h.userAgent
|
||||
}
|
||||
|
||||
// SetUserAgent sets User-Agent header value.
|
||||
func (h *RequestHeader) SetUserAgent(userAgent string) {
|
||||
h.parseRawHeaders()
|
||||
h.userAgent = append(h.userAgent[:0], userAgent...)
|
||||
}
|
||||
|
||||
// SetUserAgentBytes sets User-Agent header value.
|
||||
func (h *RequestHeader) SetUserAgentBytes(userAgent []byte) {
|
||||
h.parseRawHeaders()
|
||||
h.userAgent = append(h.userAgent[:0], userAgent...)
|
||||
}
|
||||
|
||||
// Referer returns Referer header value.
|
||||
func (h *RequestHeader) Referer() []byte {
|
||||
return h.PeekBytes(strReferer)
|
||||
}
|
||||
|
||||
// SetReferer sets Referer header value.
|
||||
func (h *RequestHeader) SetReferer(referer string) {
|
||||
h.SetBytesK(strReferer, referer)
|
||||
}
|
||||
|
||||
// SetRefererBytes sets Referer header value.
|
||||
func (h *RequestHeader) SetRefererBytes(referer []byte) {
|
||||
h.SetCanonical(strReferer, referer)
|
||||
}
|
||||
|
||||
// Method returns HTTP request method.
|
||||
func (h *RequestHeader) Method() []byte {
|
||||
if len(h.method) == 0 {
|
||||
return strGet
|
||||
}
|
||||
return h.method
|
||||
}
|
||||
|
||||
// SetMethod sets HTTP request method.
|
||||
func (h *RequestHeader) SetMethod(method string) {
|
||||
h.method = append(h.method[:0], method...)
|
||||
}
|
||||
|
||||
// SetMethodBytes sets HTTP request method.
|
||||
func (h *RequestHeader) SetMethodBytes(method []byte) {
|
||||
h.method = append(h.method[:0], method...)
|
||||
}
|
||||
|
||||
// RequestURI returns RequestURI from the first HTTP request line.
|
||||
func (h *RequestHeader) RequestURI() []byte {
|
||||
requestURI := h.requestURI
|
||||
if len(requestURI) == 0 {
|
||||
requestURI = strSlash
|
||||
}
|
||||
return requestURI
|
||||
}
|
||||
|
||||
// SetRequestURI sets RequestURI for the first HTTP request line.
|
||||
// RequestURI must be properly encoded.
|
||||
// Use URI.RequestURI for constructing proper RequestURI if unsure.
|
||||
func (h *RequestHeader) SetRequestURI(requestURI string) {
|
||||
h.requestURI = append(h.requestURI[:0], requestURI...)
|
||||
}
|
||||
|
||||
// SetRequestURIBytes sets RequestURI for the first HTTP request line.
|
||||
// RequestURI must be properly encoded.
|
||||
// Use URI.RequestURI for constructing proper RequestURI if unsure.
|
||||
func (h *RequestHeader) SetRequestURIBytes(requestURI []byte) {
|
||||
h.requestURI = append(h.requestURI[:0], requestURI...)
|
||||
}
|
||||
|
||||
// IsGet returns true if request method is GET.
|
||||
func (h *RequestHeader) IsGet() bool {
|
||||
// Optimize fast path for GET requests.
|
||||
if !h.isGet {
|
||||
h.isGet = bytes.Equal(h.Method(), strGet)
|
||||
}
|
||||
return h.isGet
|
||||
}
|
||||
|
||||
// IsPost returns true if request methos is POST.
|
||||
func (h *RequestHeader) IsPost() bool {
|
||||
return bytes.Equal(h.Method(), strPost)
|
||||
}
|
||||
|
||||
// IsPut returns true if request method is PUT.
|
||||
func (h *RequestHeader) IsPut() bool {
|
||||
return bytes.Equal(h.Method(), strPut)
|
||||
}
|
||||
|
||||
// IsHead returns true if request method is HEAD.
|
||||
func (h *RequestHeader) IsHead() bool {
|
||||
// Fast path
|
||||
if h.isGet {
|
||||
return false
|
||||
}
|
||||
return bytes.Equal(h.Method(), strHead)
|
||||
}
|
||||
|
||||
// IsDelete returns true if request method is DELETE.
|
||||
func (h *RequestHeader) IsDelete() bool {
|
||||
return bytes.Equal(h.Method(), strDelete)
|
||||
}
|
||||
|
||||
// IsHTTP11 returns true if the request is HTTP/1.1.
|
||||
func (h *RequestHeader) IsHTTP11() bool {
|
||||
return !h.noHTTP11
|
||||
}
|
||||
|
||||
// IsHTTP11 returns true if the response is HTTP/1.1.
|
||||
func (h *ResponseHeader) IsHTTP11() bool {
|
||||
return !h.noHTTP11
|
||||
}
|
||||
|
||||
// HasAcceptEncoding returns true if the header contains
|
||||
// the given Accept-Encoding value.
|
||||
func (h *RequestHeader) HasAcceptEncoding(acceptEncoding string) bool {
|
||||
h.bufKV.value = append(h.bufKV.value[:0], acceptEncoding...)
|
||||
return h.HasAcceptEncodingBytes(h.bufKV.value)
|
||||
}
|
||||
|
||||
// HasAcceptEncodingBytes returns true if the header contains
|
||||
// the given Accept-Encoding value.
|
||||
func (h *RequestHeader) HasAcceptEncodingBytes(acceptEncoding []byte) bool {
|
||||
ae := h.peek(strAcceptEncoding)
|
||||
n := bytes.Index(ae, acceptEncoding)
|
||||
if n < 0 {
|
||||
return false
|
||||
}
|
||||
b := ae[n+len(acceptEncoding):]
|
||||
if len(b) > 0 && b[0] != ',' {
|
||||
return false
|
||||
}
|
||||
if n == 0 {
|
||||
return true
|
||||
}
|
||||
return ae[n-1] == ' '
|
||||
}
|
||||
|
||||
// Len returns the number of headers set,
|
||||
// i.e. the number of times f is called in VisitAll.
|
||||
func (h *ResponseHeader) Len() int {
|
||||
n := 0
|
||||
h.VisitAll(func(k, v []byte) { n++ })
|
||||
return n
|
||||
}
|
||||
|
||||
// Len returns the number of headers set,
|
||||
// i.e. the number of times f is called in VisitAll.
|
||||
func (h *RequestHeader) Len() int {
|
||||
n := 0
|
||||
h.VisitAll(func(k, v []byte) { n++ })
|
||||
return n
|
||||
}
|
||||
|
||||
// DisableNormalizing disables header names' normalization.
|
||||
//
|
||||
// By default all the header names are normalized by uppercasing
|
||||
// the first letter and all the first letters following dashes,
|
||||
// while lowercasing all the other letters.
|
||||
// Examples:
|
||||
//
|
||||
// * CONNECTION -> Connection
|
||||
// * conteNT-tYPE -> Content-Type
|
||||
// * foo-bar-baz -> Foo-Bar-Baz
|
||||
//
|
||||
// Disable header names' normalization only if know what are you doing.
|
||||
func (h *RequestHeader) DisableNormalizing() {
|
||||
h.disableNormalizing = true
|
||||
}
|
||||
|
||||
// DisableNormalizing disables header names' normalization.
|
||||
//
|
||||
// By default all the header names are normalized by uppercasing
|
||||
// the first letter and all the first letters following dashes,
|
||||
// while lowercasing all the other letters.
|
||||
// Examples:
|
||||
//
|
||||
// * CONNECTION -> Connection
|
||||
// * conteNT-tYPE -> Content-Type
|
||||
// * foo-bar-baz -> Foo-Bar-Baz
|
||||
//
|
||||
// Disable header names' normalization only if know what are you doing.
|
||||
func (h *ResponseHeader) DisableNormalizing() {
|
||||
h.disableNormalizing = true
|
||||
}
|
||||
|
||||
// Reset clears response header.
|
||||
func (h *ResponseHeader) Reset() {
|
||||
h.disableNormalizing = false
|
||||
h.resetSkipNormalize()
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) resetSkipNormalize() {
|
||||
h.noHTTP11 = false
|
||||
h.connectionClose = false
|
||||
|
||||
h.statusCode = 0
|
||||
h.contentLength = 0
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
|
||||
h.contentType = h.contentType[:0]
|
||||
h.server = h.server[:0]
|
||||
|
||||
h.h = h.h[:0]
|
||||
h.cookies = h.cookies[:0]
|
||||
}
|
||||
|
||||
// Reset clears request header.
|
||||
func (h *RequestHeader) Reset() {
|
||||
h.disableNormalizing = false
|
||||
h.resetSkipNormalize()
|
||||
}
|
||||
|
||||
func (h *RequestHeader) resetSkipNormalize() {
|
||||
h.noHTTP11 = false
|
||||
h.connectionClose = false
|
||||
h.isGet = false
|
||||
|
||||
h.contentLength = 0
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
|
||||
h.method = h.method[:0]
|
||||
h.requestURI = h.requestURI[:0]
|
||||
h.host = h.host[:0]
|
||||
h.contentType = h.contentType[:0]
|
||||
h.userAgent = h.userAgent[:0]
|
||||
|
||||
h.h = h.h[:0]
|
||||
h.cookies = h.cookies[:0]
|
||||
h.cookiesCollected = false
|
||||
|
||||
h.rawHeaders = h.rawHeaders[:0]
|
||||
h.rawHeadersParsed = false
|
||||
}
|
||||
|
||||
// CopyTo copies all the headers to dst.
|
||||
func (h *ResponseHeader) CopyTo(dst *ResponseHeader) {
|
||||
dst.Reset()
|
||||
|
||||
dst.disableNormalizing = h.disableNormalizing
|
||||
dst.noHTTP11 = h.noHTTP11
|
||||
dst.connectionClose = h.connectionClose
|
||||
|
||||
dst.statusCode = h.statusCode
|
||||
dst.contentLength = h.contentLength
|
||||
dst.contentLengthBytes = append(dst.contentLengthBytes[:0], h.contentLengthBytes...)
|
||||
dst.contentType = append(dst.contentType[:0], h.contentType...)
|
||||
dst.server = append(dst.server[:0], h.server...)
|
||||
dst.h = copyArgs(dst.h, h.h)
|
||||
dst.cookies = copyArgs(dst.cookies, h.cookies)
|
||||
}
|
||||
|
||||
// CopyTo copies all the headers to dst.
|
||||
func (h *RequestHeader) CopyTo(dst *RequestHeader) {
|
||||
dst.Reset()
|
||||
|
||||
dst.disableNormalizing = h.disableNormalizing
|
||||
dst.noHTTP11 = h.noHTTP11
|
||||
dst.connectionClose = h.connectionClose
|
||||
dst.isGet = h.isGet
|
||||
|
||||
dst.contentLength = h.contentLength
|
||||
dst.contentLengthBytes = append(dst.contentLengthBytes[:0], h.contentLengthBytes...)
|
||||
dst.method = append(dst.method[:0], h.method...)
|
||||
dst.requestURI = append(dst.requestURI[:0], h.requestURI...)
|
||||
dst.host = append(dst.host[:0], h.host...)
|
||||
dst.contentType = append(dst.contentType[:0], h.contentType...)
|
||||
dst.userAgent = append(dst.userAgent[:0], h.userAgent...)
|
||||
dst.h = copyArgs(dst.h, h.h)
|
||||
dst.cookies = copyArgs(dst.cookies, h.cookies)
|
||||
dst.cookiesCollected = h.cookiesCollected
|
||||
dst.rawHeaders = append(dst.rawHeaders[:0], h.rawHeaders...)
|
||||
dst.rawHeadersParsed = h.rawHeadersParsed
|
||||
}
|
||||
|
||||
// VisitAll calls f for each header.
|
||||
//
|
||||
// f must not retain references to key and/or value after returning.
|
||||
// Copy key and/or value contents before returning if you need retaining them.
|
||||
func (h *ResponseHeader) VisitAll(f func(key, value []byte)) {
|
||||
if len(h.contentLengthBytes) > 0 {
|
||||
f(strContentLength, h.contentLengthBytes)
|
||||
}
|
||||
contentType := h.ContentType()
|
||||
if len(contentType) > 0 {
|
||||
f(strContentType, contentType)
|
||||
}
|
||||
server := h.Server()
|
||||
if len(server) > 0 {
|
||||
f(strServer, server)
|
||||
}
|
||||
if len(h.cookies) > 0 {
|
||||
visitArgs(h.cookies, func(k, v []byte) {
|
||||
f(strSetCookie, v)
|
||||
})
|
||||
}
|
||||
visitArgs(h.h, f)
|
||||
if h.ConnectionClose() {
|
||||
f(strConnection, strClose)
|
||||
}
|
||||
}
|
||||
|
||||
// VisitAllCookie calls f for each response cookie.
|
||||
//
|
||||
// Cookie name is passed in key and the whole Set-Cookie header value
|
||||
// is passed in value on each f invocation. Value may be parsed
|
||||
// with Cookie.ParseBytes().
|
||||
//
|
||||
// f must not retain references to key and/or value after returning.
|
||||
func (h *ResponseHeader) VisitAllCookie(f func(key, value []byte)) {
|
||||
visitArgs(h.cookies, f)
|
||||
}
|
||||
|
||||
// VisitAllCookie calls f for each request cookie.
|
||||
//
|
||||
// f must not retain references to key and/or value after returning.
|
||||
func (h *RequestHeader) VisitAllCookie(f func(key, value []byte)) {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
visitArgs(h.cookies, f)
|
||||
}
|
||||
|
||||
// VisitAll calls f for each header.
|
||||
//
|
||||
// f must not retain references to key and/or value after returning.
|
||||
// Copy key and/or value contents before returning if you need retaining them.
|
||||
func (h *RequestHeader) VisitAll(f func(key, value []byte)) {
|
||||
h.parseRawHeaders()
|
||||
host := h.Host()
|
||||
if len(host) > 0 {
|
||||
f(strHost, host)
|
||||
}
|
||||
if len(h.contentLengthBytes) > 0 {
|
||||
f(strContentLength, h.contentLengthBytes)
|
||||
}
|
||||
contentType := h.ContentType()
|
||||
if len(contentType) > 0 {
|
||||
f(strContentType, contentType)
|
||||
}
|
||||
userAgent := h.UserAgent()
|
||||
if len(userAgent) > 0 {
|
||||
f(strUserAgent, userAgent)
|
||||
}
|
||||
|
||||
h.collectCookies()
|
||||
if len(h.cookies) > 0 {
|
||||
h.bufKV.value = appendRequestCookieBytes(h.bufKV.value[:0], h.cookies)
|
||||
f(strCookie, h.bufKV.value)
|
||||
}
|
||||
visitArgs(h.h, f)
|
||||
if h.ConnectionClose() {
|
||||
f(strConnection, strClose)
|
||||
}
|
||||
}
|
||||
|
||||
// Del deletes header with the given key.
|
||||
func (h *ResponseHeader) Del(key string) {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.del(k)
|
||||
}
|
||||
|
||||
// DelBytes deletes header with the given key.
|
||||
func (h *ResponseHeader) DelBytes(key []byte) {
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
h.del(h.bufKV.key)
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) del(key []byte) {
|
||||
switch string(key) {
|
||||
case "Content-Type":
|
||||
h.contentType = h.contentType[:0]
|
||||
case "Server":
|
||||
h.server = h.server[:0]
|
||||
case "Set-Cookie":
|
||||
h.cookies = h.cookies[:0]
|
||||
case "Content-Length":
|
||||
h.contentLength = 0
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
case "Connection":
|
||||
h.connectionClose = false
|
||||
}
|
||||
h.h = delAllArgsBytes(h.h, key)
|
||||
}
|
||||
|
||||
// Del deletes header with the given key.
|
||||
func (h *RequestHeader) Del(key string) {
|
||||
h.parseRawHeaders()
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.del(k)
|
||||
}
|
||||
|
||||
// DelBytes deletes header with the given key.
|
||||
func (h *RequestHeader) DelBytes(key []byte) {
|
||||
h.parseRawHeaders()
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
h.del(h.bufKV.key)
|
||||
}
|
||||
|
||||
func (h *RequestHeader) del(key []byte) {
|
||||
switch string(key) {
|
||||
case "Host":
|
||||
h.host = h.host[:0]
|
||||
case "Content-Type":
|
||||
h.contentType = h.contentType[:0]
|
||||
case "User-Agent":
|
||||
h.userAgent = h.userAgent[:0]
|
||||
case "Cookie":
|
||||
h.cookies = h.cookies[:0]
|
||||
case "Content-Length":
|
||||
h.contentLength = 0
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
case "Connection":
|
||||
h.connectionClose = false
|
||||
}
|
||||
h.h = delAllArgsBytes(h.h, key)
|
||||
}
|
||||
|
||||
// Add adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use Set for setting a single header for the given key.
|
||||
func (h *ResponseHeader) Add(key, value string) {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.h = appendArg(h.h, b2s(k), value)
|
||||
}
|
||||
|
||||
// AddBytesK adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesK for setting a single header for the given key.
|
||||
func (h *ResponseHeader) AddBytesK(key []byte, value string) {
|
||||
h.Add(b2s(key), value)
|
||||
}
|
||||
|
||||
// AddBytesV adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesV for setting a single header for the given key.
|
||||
func (h *ResponseHeader) AddBytesV(key string, value []byte) {
|
||||
h.Add(key, b2s(value))
|
||||
}
|
||||
|
||||
// AddBytesKV adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesKV for setting a single header for the given key.
|
||||
func (h *ResponseHeader) AddBytesKV(key, value []byte) {
|
||||
h.Add(b2s(key), b2s(value))
|
||||
}
|
||||
|
||||
// Set sets the given 'key: value' header.
|
||||
//
|
||||
// Use Add for setting multiple header values under the same key.
|
||||
func (h *ResponseHeader) Set(key, value string) {
|
||||
initHeaderKV(&h.bufKV, key, value, h.disableNormalizing)
|
||||
h.SetCanonical(h.bufKV.key, h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetBytesK sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesK for setting multiple header values under the same key.
|
||||
func (h *ResponseHeader) SetBytesK(key []byte, value string) {
|
||||
h.bufKV.value = append(h.bufKV.value[:0], value...)
|
||||
h.SetBytesKV(key, h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetBytesV sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesV for setting multiple header values under the same key.
|
||||
func (h *ResponseHeader) SetBytesV(key string, value []byte) {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.SetCanonical(k, value)
|
||||
}
|
||||
|
||||
// SetBytesKV sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesKV for setting multiple header values under the same key.
|
||||
func (h *ResponseHeader) SetBytesKV(key, value []byte) {
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
h.SetCanonical(h.bufKV.key, value)
|
||||
}
|
||||
|
||||
// SetCanonical sets the given 'key: value' header assuming that
|
||||
// key is in canonical form.
|
||||
func (h *ResponseHeader) SetCanonical(key, value []byte) {
|
||||
switch string(key) {
|
||||
case "Content-Type":
|
||||
h.SetContentTypeBytes(value)
|
||||
case "Server":
|
||||
h.SetServerBytes(value)
|
||||
case "Set-Cookie":
|
||||
var kv *argsKV
|
||||
h.cookies, kv = allocArg(h.cookies)
|
||||
kv.key = getCookieKey(kv.key, value)
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
case "Content-Length":
|
||||
if contentLength, err := parseContentLength(value); err == nil {
|
||||
h.contentLength = contentLength
|
||||
h.contentLengthBytes = append(h.contentLengthBytes[:0], value...)
|
||||
}
|
||||
case "Connection":
|
||||
if bytes.Equal(strClose, value) {
|
||||
h.SetConnectionClose()
|
||||
} else {
|
||||
h.ResetConnectionClose()
|
||||
h.h = setArgBytes(h.h, key, value)
|
||||
}
|
||||
case "Transfer-Encoding":
|
||||
// Transfer-Encoding is managed automatically.
|
||||
case "Date":
|
||||
// Date is managed automatically.
|
||||
default:
|
||||
h.h = setArgBytes(h.h, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// SetCookie sets the given response cookie.
|
||||
//
|
||||
// It is save re-using the cookie after the function returns.
|
||||
func (h *ResponseHeader) SetCookie(cookie *Cookie) {
|
||||
h.cookies = setArgBytes(h.cookies, cookie.Key(), cookie.Cookie())
|
||||
}
|
||||
|
||||
// SetCookie sets 'key: value' cookies.
|
||||
func (h *RequestHeader) SetCookie(key, value string) {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
h.cookies = setArg(h.cookies, key, value)
|
||||
}
|
||||
|
||||
// SetCookieBytesK sets 'key: value' cookies.
|
||||
func (h *RequestHeader) SetCookieBytesK(key []byte, value string) {
|
||||
h.SetCookie(b2s(key), value)
|
||||
}
|
||||
|
||||
// SetCookieBytesKV sets 'key: value' cookies.
|
||||
func (h *RequestHeader) SetCookieBytesKV(key, value []byte) {
|
||||
h.SetCookie(b2s(key), b2s(value))
|
||||
}
|
||||
|
||||
// DelClientCookie instructs the client to remove the given cookie.
|
||||
//
|
||||
// Use DelCookie if you want just removing the cookie from response header.
|
||||
func (h *ResponseHeader) DelClientCookie(key string) {
|
||||
h.DelCookie(key)
|
||||
|
||||
c := AcquireCookie()
|
||||
c.SetKey(key)
|
||||
c.SetExpire(CookieExpireDelete)
|
||||
h.SetCookie(c)
|
||||
ReleaseCookie(c)
|
||||
}
|
||||
|
||||
// DelClientCookieBytes instructs the client to remove the given cookie.
|
||||
//
|
||||
// Use DelCookieBytes if you want just removing the cookie from response header.
|
||||
func (h *ResponseHeader) DelClientCookieBytes(key []byte) {
|
||||
h.DelClientCookie(b2s(key))
|
||||
}
|
||||
|
||||
// DelCookie removes cookie under the given key from response header.
|
||||
//
|
||||
// Note that DelCookie doesn't remove the cookie from the client.
|
||||
// Use DelClientCookie instead.
|
||||
func (h *ResponseHeader) DelCookie(key string) {
|
||||
h.cookies = delAllArgs(h.cookies, key)
|
||||
}
|
||||
|
||||
// DelCookieBytes removes cookie under the given key from response header.
|
||||
//
|
||||
// Note that DelCookieBytes doesn't remove the cookie from the client.
|
||||
// Use DelClientCookieBytes instead.
|
||||
func (h *ResponseHeader) DelCookieBytes(key []byte) {
|
||||
h.DelCookie(b2s(key))
|
||||
}
|
||||
|
||||
// DelCookie removes cookie under the given key.
|
||||
func (h *RequestHeader) DelCookie(key string) {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
h.cookies = delAllArgs(h.cookies, key)
|
||||
}
|
||||
|
||||
// DelCookieBytes removes cookie under the given key.
|
||||
func (h *RequestHeader) DelCookieBytes(key []byte) {
|
||||
h.DelCookie(b2s(key))
|
||||
}
|
||||
|
||||
// DelAllCookies removes all the cookies from response headers.
|
||||
func (h *ResponseHeader) DelAllCookies() {
|
||||
h.cookies = h.cookies[:0]
|
||||
}
|
||||
|
||||
// DelAllCookies removes all the cookies from request headers.
|
||||
func (h *RequestHeader) DelAllCookies() {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
h.cookies = h.cookies[:0]
|
||||
}
|
||||
|
||||
// Add adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use Set for setting a single header for the given key.
|
||||
func (h *RequestHeader) Add(key, value string) {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.h = appendArg(h.h, b2s(k), value)
|
||||
}
|
||||
|
||||
// AddBytesK adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesK for setting a single header for the given key.
|
||||
func (h *RequestHeader) AddBytesK(key []byte, value string) {
|
||||
h.Add(b2s(key), value)
|
||||
}
|
||||
|
||||
// AddBytesV adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesV for setting a single header for the given key.
|
||||
func (h *RequestHeader) AddBytesV(key string, value []byte) {
|
||||
h.Add(key, b2s(value))
|
||||
}
|
||||
|
||||
// AddBytesKV adds the given 'key: value' header.
|
||||
//
|
||||
// Multiple headers with the same key may be added with this function.
|
||||
// Use SetBytesKV for setting a single header for the given key.
|
||||
func (h *RequestHeader) AddBytesKV(key, value []byte) {
|
||||
h.Add(b2s(key), b2s(value))
|
||||
}
|
||||
|
||||
// Set sets the given 'key: value' header.
|
||||
//
|
||||
// Use Add for setting multiple header values under the same key.
|
||||
func (h *RequestHeader) Set(key, value string) {
|
||||
initHeaderKV(&h.bufKV, key, value, h.disableNormalizing)
|
||||
h.SetCanonical(h.bufKV.key, h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetBytesK sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesK for setting multiple header values under the same key.
|
||||
func (h *RequestHeader) SetBytesK(key []byte, value string) {
|
||||
h.bufKV.value = append(h.bufKV.value[:0], value...)
|
||||
h.SetBytesKV(key, h.bufKV.value)
|
||||
}
|
||||
|
||||
// SetBytesV sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesV for setting multiple header values under the same key.
|
||||
func (h *RequestHeader) SetBytesV(key string, value []byte) {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
h.SetCanonical(k, value)
|
||||
}
|
||||
|
||||
// SetBytesKV sets the given 'key: value' header.
|
||||
//
|
||||
// Use AddBytesKV for setting multiple header values under the same key.
|
||||
func (h *RequestHeader) SetBytesKV(key, value []byte) {
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
h.SetCanonical(h.bufKV.key, value)
|
||||
}
|
||||
|
||||
// SetCanonical sets the given 'key: value' header assuming that
|
||||
// key is in canonical form.
|
||||
func (h *RequestHeader) SetCanonical(key, value []byte) {
|
||||
h.parseRawHeaders()
|
||||
switch string(key) {
|
||||
case "Host":
|
||||
h.SetHostBytes(value)
|
||||
case "Content-Type":
|
||||
h.SetContentTypeBytes(value)
|
||||
case "User-Agent":
|
||||
h.SetUserAgentBytes(value)
|
||||
case "Cookie":
|
||||
h.collectCookies()
|
||||
h.cookies = parseRequestCookies(h.cookies, value)
|
||||
case "Content-Length":
|
||||
if contentLength, err := parseContentLength(value); err == nil {
|
||||
h.contentLength = contentLength
|
||||
h.contentLengthBytes = append(h.contentLengthBytes[:0], value...)
|
||||
}
|
||||
case "Connection":
|
||||
if bytes.Equal(strClose, value) {
|
||||
h.SetConnectionClose()
|
||||
} else {
|
||||
h.ResetConnectionClose()
|
||||
h.h = setArgBytes(h.h, key, value)
|
||||
}
|
||||
case "Transfer-Encoding":
|
||||
// Transfer-Encoding is managed automatically.
|
||||
default:
|
||||
h.h = setArgBytes(h.h, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// Peek returns header value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next call to ResponseHeader.
|
||||
// Do not store references to returned value. Make copies instead.
|
||||
func (h *ResponseHeader) Peek(key string) []byte {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
return h.peek(k)
|
||||
}
|
||||
|
||||
// PeekBytes returns header value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next call to ResponseHeader.
|
||||
// Do not store references to returned value. Make copies instead.
|
||||
func (h *ResponseHeader) PeekBytes(key []byte) []byte {
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
return h.peek(h.bufKV.key)
|
||||
}
|
||||
|
||||
// Peek returns header value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next call to RequestHeader.
|
||||
// Do not store references to returned value. Make copies instead.
|
||||
func (h *RequestHeader) Peek(key string) []byte {
|
||||
k := getHeaderKeyBytes(&h.bufKV, key, h.disableNormalizing)
|
||||
return h.peek(k)
|
||||
}
|
||||
|
||||
// PeekBytes returns header value for the given key.
|
||||
//
|
||||
// Returned value is valid until the next call to RequestHeader.
|
||||
// Do not store references to returned value. Make copies instead.
|
||||
func (h *RequestHeader) PeekBytes(key []byte) []byte {
|
||||
h.bufKV.key = append(h.bufKV.key[:0], key...)
|
||||
normalizeHeaderKey(h.bufKV.key, h.disableNormalizing)
|
||||
return h.peek(h.bufKV.key)
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) peek(key []byte) []byte {
|
||||
switch string(key) {
|
||||
case "Content-Type":
|
||||
return h.ContentType()
|
||||
case "Server":
|
||||
return h.Server()
|
||||
case "Connection":
|
||||
if h.ConnectionClose() {
|
||||
return strClose
|
||||
}
|
||||
return peekArgBytes(h.h, key)
|
||||
case "Content-Length":
|
||||
return h.contentLengthBytes
|
||||
default:
|
||||
return peekArgBytes(h.h, key)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RequestHeader) peek(key []byte) []byte {
|
||||
h.parseRawHeaders()
|
||||
switch string(key) {
|
||||
case "Host":
|
||||
return h.Host()
|
||||
case "Content-Type":
|
||||
return h.ContentType()
|
||||
case "User-Agent":
|
||||
return h.UserAgent()
|
||||
case "Connection":
|
||||
if h.ConnectionClose() {
|
||||
return strClose
|
||||
}
|
||||
return peekArgBytes(h.h, key)
|
||||
case "Content-Length":
|
||||
return h.contentLengthBytes
|
||||
default:
|
||||
return peekArgBytes(h.h, key)
|
||||
}
|
||||
}
|
||||
|
||||
// Cookie returns cookie for the given key.
|
||||
func (h *RequestHeader) Cookie(key string) []byte {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
return peekArgStr(h.cookies, key)
|
||||
}
|
||||
|
||||
// CookieBytes returns cookie for the given key.
|
||||
func (h *RequestHeader) CookieBytes(key []byte) []byte {
|
||||
h.parseRawHeaders()
|
||||
h.collectCookies()
|
||||
return peekArgBytes(h.cookies, key)
|
||||
}
|
||||
|
||||
// Cookie fills cookie for the given cookie.Key.
|
||||
//
|
||||
// Returns false if cookie with the given cookie.Key is missing.
|
||||
func (h *ResponseHeader) Cookie(cookie *Cookie) bool {
|
||||
v := peekArgBytes(h.cookies, cookie.Key())
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
cookie.ParseBytes(v)
|
||||
return true
|
||||
}
|
||||
|
||||
// Read reads response header from r.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (h *ResponseHeader) Read(r *bufio.Reader) error {
|
||||
n := 1
|
||||
for {
|
||||
err := h.tryRead(r, n)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err != errNeedMore {
|
||||
h.resetSkipNormalize()
|
||||
return err
|
||||
}
|
||||
n = r.Buffered() + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) tryRead(r *bufio.Reader, n int) error {
|
||||
h.resetSkipNormalize()
|
||||
b, err := r.Peek(n)
|
||||
if len(b) == 0 {
|
||||
// treat all errors on the first byte read as EOF
|
||||
if n == 1 || err == io.EOF {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// This is for go 1.6 bug. See https://github.com/golang/go/issues/14121 .
|
||||
if err == bufio.ErrBufferFull {
|
||||
return &ErrSmallBuffer{
|
||||
error: fmt.Errorf("error when reading response headers: %s", errSmallBuffer),
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("error when reading response headers: %s", err)
|
||||
}
|
||||
b = mustPeekBuffered(r)
|
||||
headersLen, errParse := h.parse(b)
|
||||
if errParse != nil {
|
||||
return headerError("response", err, errParse, b)
|
||||
}
|
||||
mustDiscard(r, headersLen)
|
||||
return nil
|
||||
}
|
||||
|
||||
func headerError(typ string, err, errParse error, b []byte) error {
|
||||
if errParse != errNeedMore {
|
||||
return headerErrorMsg(typ, errParse, b)
|
||||
}
|
||||
if err == nil {
|
||||
return errNeedMore
|
||||
}
|
||||
|
||||
// Buggy servers may leave trailing CRLFs after http body.
|
||||
// Treat this case as EOF.
|
||||
if isOnlyCRLF(b) {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
if err != bufio.ErrBufferFull {
|
||||
return headerErrorMsg(typ, err, b)
|
||||
}
|
||||
return &ErrSmallBuffer{
|
||||
error: headerErrorMsg(typ, errSmallBuffer, b),
|
||||
}
|
||||
}
|
||||
|
||||
func headerErrorMsg(typ string, err error, b []byte) error {
|
||||
return fmt.Errorf("error when reading %s headers: %s. Buffer size=%d, contents: %s", typ, err, len(b), bufferSnippet(b))
|
||||
}
|
||||
|
||||
// Read reads request header from r.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (h *RequestHeader) Read(r *bufio.Reader) error {
|
||||
n := 1
|
||||
for {
|
||||
err := h.tryRead(r, n)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if err != errNeedMore {
|
||||
h.resetSkipNormalize()
|
||||
return err
|
||||
}
|
||||
n = r.Buffered() + 1
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RequestHeader) tryRead(r *bufio.Reader, n int) error {
|
||||
h.resetSkipNormalize()
|
||||
b, err := r.Peek(n)
|
||||
if len(b) == 0 {
|
||||
// treat all errors on the first byte read as EOF
|
||||
if n == 1 || err == io.EOF {
|
||||
return io.EOF
|
||||
}
|
||||
|
||||
// This is for go 1.6 bug. See https://github.com/golang/go/issues/14121 .
|
||||
if err == bufio.ErrBufferFull {
|
||||
return &ErrSmallBuffer{
|
||||
error: fmt.Errorf("error when reading request headers: %s", errSmallBuffer),
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("error when reading request headers: %s", err)
|
||||
}
|
||||
b = mustPeekBuffered(r)
|
||||
headersLen, errParse := h.parse(b)
|
||||
if errParse != nil {
|
||||
return headerError("request", err, errParse, b)
|
||||
}
|
||||
mustDiscard(r, headersLen)
|
||||
return nil
|
||||
}
|
||||
|
||||
func bufferSnippet(b []byte) string {
|
||||
n := len(b)
|
||||
start := 200
|
||||
end := n - start
|
||||
if start >= end {
|
||||
start = n
|
||||
end = n
|
||||
}
|
||||
bStart, bEnd := b[:start], b[end:]
|
||||
if len(bEnd) == 0 {
|
||||
return fmt.Sprintf("%q", b)
|
||||
}
|
||||
return fmt.Sprintf("%q...%q", bStart, bEnd)
|
||||
}
|
||||
|
||||
func isOnlyCRLF(b []byte) bool {
|
||||
for _, ch := range b {
|
||||
if ch != '\r' && ch != '\n' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func init() {
|
||||
refreshServerDate()
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
refreshServerDate()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
var serverDate atomic.Value
|
||||
|
||||
func refreshServerDate() {
|
||||
b := AppendHTTPDate(nil, time.Now())
|
||||
serverDate.Store(b)
|
||||
}
|
||||
|
||||
// Write writes response header to w.
|
||||
func (h *ResponseHeader) Write(w *bufio.Writer) error {
|
||||
_, err := w.Write(h.Header())
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteTo writes response header to w.
|
||||
//
|
||||
// WriteTo implements io.WriterTo interface.
|
||||
func (h *ResponseHeader) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(h.Header())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Header returns response header representation.
|
||||
//
|
||||
// The returned value is valid until the next call to ResponseHeader methods.
|
||||
func (h *ResponseHeader) Header() []byte {
|
||||
h.bufKV.value = h.AppendBytes(h.bufKV.value[:0])
|
||||
return h.bufKV.value
|
||||
}
|
||||
|
||||
// String returns response header representation.
|
||||
func (h *ResponseHeader) String() string {
|
||||
return string(h.Header())
|
||||
}
|
||||
|
||||
// AppendBytes appends response header representation to dst and returns
|
||||
// the extended dst.
|
||||
func (h *ResponseHeader) AppendBytes(dst []byte) []byte {
|
||||
statusCode := h.StatusCode()
|
||||
if statusCode < 0 {
|
||||
statusCode = StatusOK
|
||||
}
|
||||
dst = append(dst, statusLine(statusCode)...)
|
||||
|
||||
server := h.Server()
|
||||
if len(server) == 0 {
|
||||
server = defaultServerName
|
||||
}
|
||||
dst = appendHeaderLine(dst, strServer, server)
|
||||
dst = appendHeaderLine(dst, strDate, serverDate.Load().([]byte))
|
||||
|
||||
// Append Content-Type only for non-zero responses
|
||||
// or if it is explicitly set.
|
||||
// See https://github.com/valyala/fasthttp/issues/28 .
|
||||
if h.ContentLength() != 0 || len(h.contentType) > 0 {
|
||||
dst = appendHeaderLine(dst, strContentType, h.ContentType())
|
||||
}
|
||||
|
||||
if len(h.contentLengthBytes) > 0 {
|
||||
dst = appendHeaderLine(dst, strContentLength, h.contentLengthBytes)
|
||||
}
|
||||
|
||||
for i, n := 0, len(h.h); i < n; i++ {
|
||||
kv := &h.h[i]
|
||||
if !bytes.Equal(kv.key, strDate) {
|
||||
dst = appendHeaderLine(dst, kv.key, kv.value)
|
||||
}
|
||||
}
|
||||
|
||||
n := len(h.cookies)
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &h.cookies[i]
|
||||
dst = appendHeaderLine(dst, strSetCookie, kv.value)
|
||||
}
|
||||
}
|
||||
|
||||
if h.ConnectionClose() {
|
||||
dst = appendHeaderLine(dst, strConnection, strClose)
|
||||
}
|
||||
|
||||
return append(dst, strCRLF...)
|
||||
}
|
||||
|
||||
// Write writes request header to w.
|
||||
func (h *RequestHeader) Write(w *bufio.Writer) error {
|
||||
_, err := w.Write(h.Header())
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteTo writes request header to w.
|
||||
//
|
||||
// WriteTo implements io.WriterTo interface.
|
||||
func (h *RequestHeader) WriteTo(w io.Writer) (int64, error) {
|
||||
n, err := w.Write(h.Header())
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Header returns request header representation.
|
||||
//
|
||||
// The returned representation is valid until the next call to RequestHeader methods.
|
||||
func (h *RequestHeader) Header() []byte {
|
||||
h.bufKV.value = h.AppendBytes(h.bufKV.value[:0])
|
||||
return h.bufKV.value
|
||||
}
|
||||
|
||||
// String returns request header representation.
|
||||
func (h *RequestHeader) String() string {
|
||||
return string(h.Header())
|
||||
}
|
||||
|
||||
// AppendBytes appends request header representation to dst and returns
|
||||
// the extended dst.
|
||||
func (h *RequestHeader) AppendBytes(dst []byte) []byte {
|
||||
// there is no need in h.parseRawHeaders() here - raw headers are specially handled below.
|
||||
dst = append(dst, h.Method()...)
|
||||
dst = append(dst, ' ')
|
||||
dst = append(dst, h.RequestURI()...)
|
||||
dst = append(dst, ' ')
|
||||
dst = append(dst, strHTTP11...)
|
||||
dst = append(dst, strCRLF...)
|
||||
|
||||
if !h.rawHeadersParsed && len(h.rawHeaders) > 0 {
|
||||
return append(dst, h.rawHeaders...)
|
||||
}
|
||||
|
||||
userAgent := h.UserAgent()
|
||||
if len(userAgent) == 0 {
|
||||
userAgent = defaultUserAgent
|
||||
}
|
||||
dst = appendHeaderLine(dst, strUserAgent, userAgent)
|
||||
|
||||
host := h.Host()
|
||||
if len(host) > 0 {
|
||||
dst = appendHeaderLine(dst, strHost, host)
|
||||
}
|
||||
|
||||
contentType := h.ContentType()
|
||||
if !h.noBody() {
|
||||
if len(contentType) == 0 {
|
||||
contentType = strPostArgsContentType
|
||||
}
|
||||
dst = appendHeaderLine(dst, strContentType, contentType)
|
||||
|
||||
if len(h.contentLengthBytes) > 0 {
|
||||
dst = appendHeaderLine(dst, strContentLength, h.contentLengthBytes)
|
||||
}
|
||||
} else if len(contentType) > 0 {
|
||||
dst = appendHeaderLine(dst, strContentType, contentType)
|
||||
}
|
||||
|
||||
for i, n := 0, len(h.h); i < n; i++ {
|
||||
kv := &h.h[i]
|
||||
dst = appendHeaderLine(dst, kv.key, kv.value)
|
||||
}
|
||||
|
||||
// there is no need in h.collectCookies() here, since if cookies aren't collected yet,
|
||||
// they all are located in h.h.
|
||||
n := len(h.cookies)
|
||||
if n > 0 {
|
||||
dst = append(dst, strCookie...)
|
||||
dst = append(dst, strColonSpace...)
|
||||
dst = appendRequestCookieBytes(dst, h.cookies)
|
||||
dst = append(dst, strCRLF...)
|
||||
}
|
||||
|
||||
if h.ConnectionClose() {
|
||||
dst = appendHeaderLine(dst, strConnection, strClose)
|
||||
}
|
||||
|
||||
return append(dst, strCRLF...)
|
||||
}
|
||||
|
||||
func appendHeaderLine(dst, key, value []byte) []byte {
|
||||
dst = append(dst, key...)
|
||||
dst = append(dst, strColonSpace...)
|
||||
dst = append(dst, value...)
|
||||
return append(dst, strCRLF...)
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) parse(buf []byte) (int, error) {
|
||||
m, err := h.parseFirstLine(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
n, err := h.parseHeaders(buf[m:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return m + n, nil
|
||||
}
|
||||
|
||||
func (h *RequestHeader) noBody() bool {
|
||||
return h.IsGet() || h.IsHead()
|
||||
}
|
||||
|
||||
func (h *RequestHeader) parse(buf []byte) (int, error) {
|
||||
m, err := h.parseFirstLine(buf)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var n int
|
||||
if !h.noBody() || h.noHTTP11 {
|
||||
n, err = h.parseHeaders(buf[m:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
h.rawHeadersParsed = true
|
||||
} else {
|
||||
var rawHeaders []byte
|
||||
rawHeaders, n, err = readRawHeaders(h.rawHeaders[:0], buf[m:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
h.rawHeaders = rawHeaders
|
||||
}
|
||||
return m + n, nil
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) parseFirstLine(buf []byte) (int, error) {
|
||||
bNext := buf
|
||||
var b []byte
|
||||
var err error
|
||||
for len(b) == 0 {
|
||||
if b, bNext, err = nextLine(bNext); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// parse protocol
|
||||
n := bytes.IndexByte(b, ' ')
|
||||
if n < 0 {
|
||||
return 0, fmt.Errorf("cannot find whitespace in the first line of response %q", buf)
|
||||
}
|
||||
h.noHTTP11 = !bytes.Equal(b[:n], strHTTP11)
|
||||
b = b[n+1:]
|
||||
|
||||
// parse status code
|
||||
h.statusCode, n, err = parseUintBuf(b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("cannot parse response status code: %s. Response %q", err, buf)
|
||||
}
|
||||
if len(b) > n && b[n] != ' ' {
|
||||
return 0, fmt.Errorf("unexpected char at the end of status code. Response %q", buf)
|
||||
}
|
||||
|
||||
return len(buf) - len(bNext), nil
|
||||
}
|
||||
|
||||
func (h *RequestHeader) parseFirstLine(buf []byte) (int, error) {
|
||||
bNext := buf
|
||||
var b []byte
|
||||
var err error
|
||||
for len(b) == 0 {
|
||||
if b, bNext, err = nextLine(bNext); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// parse method
|
||||
n := bytes.IndexByte(b, ' ')
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("cannot find http request method in %q", buf)
|
||||
}
|
||||
h.method = append(h.method[:0], b[:n]...)
|
||||
b = b[n+1:]
|
||||
|
||||
// parse requestURI
|
||||
n = bytes.LastIndexByte(b, ' ')
|
||||
if n < 0 {
|
||||
h.noHTTP11 = true
|
||||
n = len(b)
|
||||
} else if n == 0 {
|
||||
return 0, fmt.Errorf("requestURI cannot be empty in %q", buf)
|
||||
} else if !bytes.Equal(b[n+1:], strHTTP11) {
|
||||
h.noHTTP11 = true
|
||||
}
|
||||
h.requestURI = append(h.requestURI[:0], b[:n]...)
|
||||
|
||||
return len(buf) - len(bNext), nil
|
||||
}
|
||||
|
||||
func peekRawHeader(buf, key []byte) []byte {
|
||||
n := bytes.Index(buf, key)
|
||||
if n < 0 {
|
||||
return nil
|
||||
}
|
||||
if n > 0 && buf[n-1] != '\n' {
|
||||
return nil
|
||||
}
|
||||
n += len(key)
|
||||
if n >= len(buf) {
|
||||
return nil
|
||||
}
|
||||
if buf[n] != ':' {
|
||||
return nil
|
||||
}
|
||||
n++
|
||||
if buf[n] != ' ' {
|
||||
return nil
|
||||
}
|
||||
n++
|
||||
buf = buf[n:]
|
||||
n = bytes.IndexByte(buf, '\n')
|
||||
if n < 0 {
|
||||
return nil
|
||||
}
|
||||
if n > 0 && buf[n-1] == '\r' {
|
||||
n--
|
||||
}
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
func readRawHeaders(dst, buf []byte) ([]byte, int, error) {
|
||||
n := bytes.IndexByte(buf, '\n')
|
||||
if n < 0 {
|
||||
return nil, 0, errNeedMore
|
||||
}
|
||||
if (n == 1 && buf[0] == '\r') || n == 0 {
|
||||
// empty headers
|
||||
return dst, n + 1, nil
|
||||
}
|
||||
|
||||
n++
|
||||
b := buf
|
||||
m := n
|
||||
for {
|
||||
b = b[m:]
|
||||
m = bytes.IndexByte(b, '\n')
|
||||
if m < 0 {
|
||||
return nil, 0, errNeedMore
|
||||
}
|
||||
m++
|
||||
n += m
|
||||
if (m == 2 && b[0] == '\r') || m == 1 {
|
||||
dst = append(dst, buf[:n]...)
|
||||
return dst, n, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ResponseHeader) parseHeaders(buf []byte) (int, error) {
|
||||
// 'identity' content-length by default
|
||||
h.contentLength = -2
|
||||
|
||||
var s headerScanner
|
||||
s.b = buf
|
||||
s.disableNormalizing = h.disableNormalizing
|
||||
var err error
|
||||
var kv *argsKV
|
||||
for s.next() {
|
||||
switch string(s.key) {
|
||||
case "Content-Type":
|
||||
h.contentType = append(h.contentType[:0], s.value...)
|
||||
case "Server":
|
||||
h.server = append(h.server[:0], s.value...)
|
||||
case "Content-Length":
|
||||
if h.contentLength != -1 {
|
||||
if h.contentLength, err = parseContentLength(s.value); err != nil {
|
||||
h.contentLength = -2
|
||||
} else {
|
||||
h.contentLengthBytes = append(h.contentLengthBytes[:0], s.value...)
|
||||
}
|
||||
}
|
||||
case "Transfer-Encoding":
|
||||
if !bytes.Equal(s.value, strIdentity) {
|
||||
h.contentLength = -1
|
||||
h.h = setArgBytes(h.h, strTransferEncoding, strChunked)
|
||||
}
|
||||
case "Set-Cookie":
|
||||
h.cookies, kv = allocArg(h.cookies)
|
||||
kv.key = getCookieKey(kv.key, s.value)
|
||||
kv.value = append(kv.value[:0], s.value...)
|
||||
case "Connection":
|
||||
if bytes.Equal(s.value, strClose) {
|
||||
h.connectionClose = true
|
||||
} else {
|
||||
h.connectionClose = false
|
||||
h.h = appendArgBytes(h.h, s.key, s.value)
|
||||
}
|
||||
default:
|
||||
h.h = appendArgBytes(h.h, s.key, s.value)
|
||||
}
|
||||
}
|
||||
if s.err != nil {
|
||||
h.connectionClose = true
|
||||
return 0, s.err
|
||||
}
|
||||
|
||||
if h.contentLength < 0 {
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
}
|
||||
if h.contentLength == -2 && !h.ConnectionUpgrade() && !h.mustSkipContentLength() {
|
||||
h.h = setArgBytes(h.h, strTransferEncoding, strIdentity)
|
||||
h.connectionClose = true
|
||||
}
|
||||
if h.noHTTP11 && !h.connectionClose {
|
||||
// close connection for non-http/1.1 response unless 'Connection: keep-alive' is set.
|
||||
v := peekArgBytes(h.h, strConnection)
|
||||
h.connectionClose = !hasHeaderValue(v, strKeepAlive) && !hasHeaderValue(v, strKeepAliveCamelCase)
|
||||
}
|
||||
|
||||
return len(buf) - len(s.b), nil
|
||||
}
|
||||
|
||||
func (h *RequestHeader) parseHeaders(buf []byte) (int, error) {
|
||||
h.contentLength = -2
|
||||
|
||||
var s headerScanner
|
||||
s.b = buf
|
||||
s.disableNormalizing = h.disableNormalizing
|
||||
var err error
|
||||
for s.next() {
|
||||
switch string(s.key) {
|
||||
case "Host":
|
||||
h.host = append(h.host[:0], s.value...)
|
||||
case "User-Agent":
|
||||
h.userAgent = append(h.userAgent[:0], s.value...)
|
||||
case "Content-Type":
|
||||
h.contentType = append(h.contentType[:0], s.value...)
|
||||
case "Content-Length":
|
||||
if h.contentLength != -1 {
|
||||
if h.contentLength, err = parseContentLength(s.value); err != nil {
|
||||
h.contentLength = -2
|
||||
} else {
|
||||
h.contentLengthBytes = append(h.contentLengthBytes[:0], s.value...)
|
||||
}
|
||||
}
|
||||
case "Transfer-Encoding":
|
||||
if !bytes.Equal(s.value, strIdentity) {
|
||||
h.contentLength = -1
|
||||
h.h = setArgBytes(h.h, strTransferEncoding, strChunked)
|
||||
}
|
||||
case "Connection":
|
||||
if bytes.Equal(s.value, strClose) {
|
||||
h.connectionClose = true
|
||||
} else {
|
||||
h.connectionClose = false
|
||||
h.h = appendArgBytes(h.h, s.key, s.value)
|
||||
}
|
||||
default:
|
||||
h.h = appendArgBytes(h.h, s.key, s.value)
|
||||
}
|
||||
}
|
||||
if s.err != nil {
|
||||
h.connectionClose = true
|
||||
return 0, s.err
|
||||
}
|
||||
|
||||
if h.contentLength < 0 {
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
}
|
||||
if h.noBody() {
|
||||
h.contentLength = 0
|
||||
h.contentLengthBytes = h.contentLengthBytes[:0]
|
||||
}
|
||||
if h.noHTTP11 && !h.connectionClose {
|
||||
// close connection for non-http/1.1 request unless 'Connection: keep-alive' is set.
|
||||
v := peekArgBytes(h.h, strConnection)
|
||||
h.connectionClose = !hasHeaderValue(v, strKeepAlive) && !hasHeaderValue(v, strKeepAliveCamelCase)
|
||||
}
|
||||
|
||||
return len(buf) - len(s.b), nil
|
||||
}
|
||||
|
||||
func (h *RequestHeader) parseRawHeaders() {
|
||||
if h.rawHeadersParsed {
|
||||
return
|
||||
}
|
||||
h.rawHeadersParsed = true
|
||||
if len(h.rawHeaders) == 0 {
|
||||
return
|
||||
}
|
||||
h.parseHeaders(h.rawHeaders)
|
||||
}
|
||||
|
||||
func (h *RequestHeader) collectCookies() {
|
||||
if h.cookiesCollected {
|
||||
return
|
||||
}
|
||||
|
||||
for i, n := 0, len(h.h); i < n; i++ {
|
||||
kv := &h.h[i]
|
||||
if bytes.Equal(kv.key, strCookie) {
|
||||
h.cookies = parseRequestCookies(h.cookies, kv.value)
|
||||
tmp := *kv
|
||||
copy(h.h[i:], h.h[i+1:])
|
||||
n--
|
||||
i--
|
||||
h.h[n] = tmp
|
||||
h.h = h.h[:n]
|
||||
}
|
||||
}
|
||||
h.cookiesCollected = true
|
||||
}
|
||||
|
||||
func parseContentLength(b []byte) (int, error) {
|
||||
v, n, err := parseUintBuf(b)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
if n != len(b) {
|
||||
return -1, fmt.Errorf("non-numeric chars at the end of Content-Length")
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type headerScanner struct {
|
||||
b []byte
|
||||
key []byte
|
||||
value []byte
|
||||
err error
|
||||
|
||||
disableNormalizing bool
|
||||
}
|
||||
|
||||
func (s *headerScanner) next() bool {
|
||||
bLen := len(s.b)
|
||||
if bLen >= 2 && s.b[0] == '\r' && s.b[1] == '\n' {
|
||||
s.b = s.b[2:]
|
||||
return false
|
||||
}
|
||||
if bLen >= 1 && s.b[0] == '\n' {
|
||||
s.b = s.b[1:]
|
||||
return false
|
||||
}
|
||||
n := bytes.IndexByte(s.b, ':')
|
||||
if n < 0 {
|
||||
s.err = errNeedMore
|
||||
return false
|
||||
}
|
||||
s.key = s.b[:n]
|
||||
normalizeHeaderKey(s.key, s.disableNormalizing)
|
||||
n++
|
||||
for len(s.b) > n && s.b[n] == ' ' {
|
||||
n++
|
||||
}
|
||||
s.b = s.b[n:]
|
||||
n = bytes.IndexByte(s.b, '\n')
|
||||
if n < 0 {
|
||||
s.err = errNeedMore
|
||||
return false
|
||||
}
|
||||
s.value = s.b[:n]
|
||||
s.b = s.b[n+1:]
|
||||
|
||||
if n > 0 && s.value[n-1] == '\r' {
|
||||
n--
|
||||
}
|
||||
for n > 0 && s.value[n-1] == ' ' {
|
||||
n--
|
||||
}
|
||||
s.value = s.value[:n]
|
||||
return true
|
||||
}
|
||||
|
||||
type headerValueScanner struct {
|
||||
b []byte
|
||||
value []byte
|
||||
}
|
||||
|
||||
func (s *headerValueScanner) next() bool {
|
||||
b := s.b
|
||||
if len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
n := bytes.IndexByte(b, ',')
|
||||
if n < 0 {
|
||||
s.value = stripSpace(b)
|
||||
s.b = b[len(b):]
|
||||
return true
|
||||
}
|
||||
s.value = stripSpace(b[:n])
|
||||
s.b = b[n+1:]
|
||||
return true
|
||||
}
|
||||
|
||||
func stripSpace(b []byte) []byte {
|
||||
for len(b) > 0 && b[0] == ' ' {
|
||||
b = b[1:]
|
||||
}
|
||||
for len(b) > 0 && b[len(b)-1] == ' ' {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func hasHeaderValue(s, value []byte) bool {
|
||||
var vs headerValueScanner
|
||||
vs.b = s
|
||||
for vs.next() {
|
||||
if bytes.Equal(vs.value, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nextLine(b []byte) ([]byte, []byte, error) {
|
||||
nNext := bytes.IndexByte(b, '\n')
|
||||
if nNext < 0 {
|
||||
return nil, nil, errNeedMore
|
||||
}
|
||||
n := nNext
|
||||
if n > 0 && b[n-1] == '\r' {
|
||||
n--
|
||||
}
|
||||
return b[:n], b[nNext+1:], nil
|
||||
}
|
||||
|
||||
func initHeaderKV(kv *argsKV, key, value string, disableNormalizing bool) {
|
||||
kv.key = getHeaderKeyBytes(kv, key, disableNormalizing)
|
||||
kv.value = append(kv.value[:0], value...)
|
||||
}
|
||||
|
||||
func getHeaderKeyBytes(kv *argsKV, key string, disableNormalizing bool) []byte {
|
||||
kv.key = append(kv.key[:0], key...)
|
||||
normalizeHeaderKey(kv.key, disableNormalizing)
|
||||
return kv.key
|
||||
}
|
||||
|
||||
func normalizeHeaderKey(b []byte, disableNormalizing bool) {
|
||||
if disableNormalizing {
|
||||
return
|
||||
}
|
||||
|
||||
n := len(b)
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
b[0] = toUpperTable[b[0]]
|
||||
for i := 1; i < n; i++ {
|
||||
p := &b[i]
|
||||
if *p == '-' {
|
||||
i++
|
||||
if i < n {
|
||||
b[i] = toUpperTable[b[i]]
|
||||
}
|
||||
continue
|
||||
}
|
||||
*p = toLowerTable[*p]
|
||||
}
|
||||
}
|
||||
|
||||
// AppendNormalizedHeaderKey appends normalized header key (name) to dst
|
||||
// and returns the resulting dst.
|
||||
//
|
||||
// Normalized header key starts with uppercase letter. The first letters
|
||||
// after dashes are also uppercased. All the other letters are lowercased.
|
||||
// Examples:
|
||||
//
|
||||
// * coNTENT-TYPe -> Content-Type
|
||||
// * HOST -> Host
|
||||
// * foo-bar-baz -> Foo-Bar-Baz
|
||||
func AppendNormalizedHeaderKey(dst []byte, key string) []byte {
|
||||
dst = append(dst, key...)
|
||||
normalizeHeaderKey(dst[len(dst)-len(key):], false)
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendNormalizedHeaderKeyBytes appends normalized header key (name) to dst
|
||||
// and returns the resulting dst.
|
||||
//
|
||||
// Normalized header key starts with uppercase letter. The first letters
|
||||
// after dashes are also uppercased. All the other letters are lowercased.
|
||||
// Examples:
|
||||
//
|
||||
// * coNTENT-TYPe -> Content-Type
|
||||
// * HOST -> Host
|
||||
// * foo-bar-baz -> Foo-Bar-Baz
|
||||
func AppendNormalizedHeaderKeyBytes(dst, key []byte) []byte {
|
||||
return AppendNormalizedHeaderKey(dst, b2s(key))
|
||||
}
|
||||
|
||||
var (
|
||||
errNeedMore = errors.New("need more data: cannot find trailing lf")
|
||||
errSmallBuffer = errors.New("small read buffer. Increase ReadBufferSize")
|
||||
)
|
||||
|
||||
// ErrSmallBuffer is returned when the provided buffer size is too small
|
||||
// for reading request and/or response headers.
|
||||
//
|
||||
// ReadBufferSize value from Server or clients should reduce the number
|
||||
// of such errors.
|
||||
type ErrSmallBuffer struct {
|
||||
error
|
||||
}
|
||||
|
||||
func mustPeekBuffered(r *bufio.Reader) []byte {
|
||||
buf, err := r.Peek(r.Buffered())
|
||||
if len(buf) == 0 || err != nil {
|
||||
panic(fmt.Sprintf("bufio.Reader.Peek() returned unexpected data (%q, %v)", buf, err))
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func mustDiscard(r *bufio.Reader, n int) {
|
||||
if _, err := r.Discard(n); err != nil {
|
||||
panic(fmt.Sprintf("bufio.Reader.Discard(%d) failed: %s", n, err))
|
||||
}
|
||||
}
|
||||
+1714
@@ -0,0 +1,1714 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/valyala/bytebufferpool"
|
||||
)
|
||||
|
||||
// Request represents HTTP request.
|
||||
//
|
||||
// It is forbidden copying Request instances. Create new instances
|
||||
// and use CopyTo instead.
|
||||
//
|
||||
// Request instance MUST NOT be used from concurrently running goroutines.
|
||||
type Request struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Request header
|
||||
//
|
||||
// Copying Header by value is forbidden. Use pointer to Header instead.
|
||||
Header RequestHeader
|
||||
|
||||
uri URI
|
||||
postArgs Args
|
||||
|
||||
bodyStream io.Reader
|
||||
w requestBodyWriter
|
||||
body *bytebufferpool.ByteBuffer
|
||||
|
||||
multipartForm *multipart.Form
|
||||
multipartFormBoundary string
|
||||
|
||||
// Group bool members in order to reduce Request object size.
|
||||
parsedURI bool
|
||||
parsedPostArgs bool
|
||||
|
||||
keepBodyBuffer bool
|
||||
|
||||
isTLS bool
|
||||
}
|
||||
|
||||
// Response represents HTTP response.
|
||||
//
|
||||
// It is forbidden copying Response instances. Create new instances
|
||||
// and use CopyTo instead.
|
||||
//
|
||||
// Response instance MUST NOT be used from concurrently running goroutines.
|
||||
type Response struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Response header
|
||||
//
|
||||
// Copying Header by value is forbidden. Use pointer to Header instead.
|
||||
Header ResponseHeader
|
||||
|
||||
bodyStream io.Reader
|
||||
w responseBodyWriter
|
||||
body *bytebufferpool.ByteBuffer
|
||||
|
||||
// Response.Read() skips reading body if set to true.
|
||||
// Use it for reading HEAD responses.
|
||||
//
|
||||
// Response.Write() skips writing body if set to true.
|
||||
// Use it for writing HEAD responses.
|
||||
SkipBody bool
|
||||
|
||||
keepBodyBuffer bool
|
||||
}
|
||||
|
||||
// SetHost sets host for the request.
|
||||
func (req *Request) SetHost(host string) {
|
||||
req.URI().SetHost(host)
|
||||
}
|
||||
|
||||
// SetHostBytes sets host for the request.
|
||||
func (req *Request) SetHostBytes(host []byte) {
|
||||
req.URI().SetHostBytes(host)
|
||||
}
|
||||
|
||||
// Host returns the host for the given request.
|
||||
func (req *Request) Host() []byte {
|
||||
return req.URI().Host()
|
||||
}
|
||||
|
||||
// SetRequestURI sets RequestURI.
|
||||
func (req *Request) SetRequestURI(requestURI string) {
|
||||
req.Header.SetRequestURI(requestURI)
|
||||
req.parsedURI = false
|
||||
}
|
||||
|
||||
// SetRequestURIBytes sets RequestURI.
|
||||
func (req *Request) SetRequestURIBytes(requestURI []byte) {
|
||||
req.Header.SetRequestURIBytes(requestURI)
|
||||
req.parsedURI = false
|
||||
}
|
||||
|
||||
// RequestURI returns request's URI.
|
||||
func (req *Request) RequestURI() []byte {
|
||||
if req.parsedURI {
|
||||
requestURI := req.uri.RequestURI()
|
||||
req.SetRequestURIBytes(requestURI)
|
||||
}
|
||||
return req.Header.RequestURI()
|
||||
}
|
||||
|
||||
// StatusCode returns response status code.
|
||||
func (resp *Response) StatusCode() int {
|
||||
return resp.Header.StatusCode()
|
||||
}
|
||||
|
||||
// SetStatusCode sets response status code.
|
||||
func (resp *Response) SetStatusCode(statusCode int) {
|
||||
resp.Header.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
// ConnectionClose returns true if 'Connection: close' header is set.
|
||||
func (resp *Response) ConnectionClose() bool {
|
||||
return resp.Header.ConnectionClose()
|
||||
}
|
||||
|
||||
// SetConnectionClose sets 'Connection: close' header.
|
||||
func (resp *Response) SetConnectionClose() {
|
||||
resp.Header.SetConnectionClose()
|
||||
}
|
||||
|
||||
// ConnectionClose returns true if 'Connection: close' header is set.
|
||||
func (req *Request) ConnectionClose() bool {
|
||||
return req.Header.ConnectionClose()
|
||||
}
|
||||
|
||||
// SetConnectionClose sets 'Connection: close' header.
|
||||
func (req *Request) SetConnectionClose() {
|
||||
req.Header.SetConnectionClose()
|
||||
}
|
||||
|
||||
// SendFile registers file on the given path to be used as response body
|
||||
// when Write is called.
|
||||
//
|
||||
// Note that SendFile doesn't set Content-Type, so set it yourself
|
||||
// with Header.SetContentType.
|
||||
func (resp *Response) SendFile(path string) error {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileInfo, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
size64 := fileInfo.Size()
|
||||
size := int(size64)
|
||||
if int64(size) != size64 {
|
||||
size = -1
|
||||
}
|
||||
|
||||
resp.Header.SetLastModified(fileInfo.ModTime())
|
||||
resp.SetBodyStream(f, size)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetBodyStream sets request body stream and, optionally body size.
|
||||
//
|
||||
// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes
|
||||
// before returning io.EOF.
|
||||
//
|
||||
// If bodySize < 0, then bodyStream is read until io.EOF.
|
||||
//
|
||||
// bodyStream.Close() is called after finishing reading all body data
|
||||
// if it implements io.Closer.
|
||||
//
|
||||
// Note that GET and HEAD requests cannot have body.
|
||||
//
|
||||
// See also SetBodyStreamWriter.
|
||||
func (req *Request) SetBodyStream(bodyStream io.Reader, bodySize int) {
|
||||
req.ResetBody()
|
||||
req.bodyStream = bodyStream
|
||||
req.Header.SetContentLength(bodySize)
|
||||
}
|
||||
|
||||
// SetBodyStream sets response body stream and, optionally body size.
|
||||
//
|
||||
// If bodySize is >= 0, then the bodyStream must provide exactly bodySize bytes
|
||||
// before returning io.EOF.
|
||||
//
|
||||
// If bodySize < 0, then bodyStream is read until io.EOF.
|
||||
//
|
||||
// bodyStream.Close() is called after finishing reading all body data
|
||||
// if it implements io.Closer.
|
||||
//
|
||||
// See also SetBodyStreamWriter.
|
||||
func (resp *Response) SetBodyStream(bodyStream io.Reader, bodySize int) {
|
||||
resp.ResetBody()
|
||||
resp.bodyStream = bodyStream
|
||||
resp.Header.SetContentLength(bodySize)
|
||||
}
|
||||
|
||||
// IsBodyStream returns true if body is set via SetBodyStream*
|
||||
func (req *Request) IsBodyStream() bool {
|
||||
return req.bodyStream != nil
|
||||
}
|
||||
|
||||
// IsBodyStream returns true if body is set via SetBodyStream*
|
||||
func (resp *Response) IsBodyStream() bool {
|
||||
return resp.bodyStream != nil
|
||||
}
|
||||
|
||||
// SetBodyStreamWriter registers the given sw for populating request body.
|
||||
//
|
||||
// This function may be used in the following cases:
|
||||
//
|
||||
// * if request body is too big (more than 10MB).
|
||||
// * if request body is streamed from slow external sources.
|
||||
// * if request body must be streamed to the server in chunks
|
||||
// (aka `http client push` or `chunked transfer-encoding`).
|
||||
//
|
||||
// Note that GET and HEAD requests cannot have body.
|
||||
//
|
||||
/// See also SetBodyStream.
|
||||
func (req *Request) SetBodyStreamWriter(sw StreamWriter) {
|
||||
sr := NewStreamReader(sw)
|
||||
req.SetBodyStream(sr, -1)
|
||||
}
|
||||
|
||||
// SetBodyStreamWriter registers the given sw for populating response body.
|
||||
//
|
||||
// This function may be used in the following cases:
|
||||
//
|
||||
// * if response body is too big (more than 10MB).
|
||||
// * if response body is streamed from slow external sources.
|
||||
// * if response body must be streamed to the client in chunks
|
||||
// (aka `http server push` or `chunked transfer-encoding`).
|
||||
//
|
||||
// See also SetBodyStream.
|
||||
func (resp *Response) SetBodyStreamWriter(sw StreamWriter) {
|
||||
sr := NewStreamReader(sw)
|
||||
resp.SetBodyStream(sr, -1)
|
||||
}
|
||||
|
||||
// BodyWriter returns writer for populating response body.
|
||||
//
|
||||
// If used inside RequestHandler, the returned writer must not be used
|
||||
// after returning from RequestHandler. Use RequestCtx.Write
|
||||
// or SetBodyStreamWriter in this case.
|
||||
func (resp *Response) BodyWriter() io.Writer {
|
||||
resp.w.r = resp
|
||||
return &resp.w
|
||||
}
|
||||
|
||||
// BodyWriter returns writer for populating request body.
|
||||
func (req *Request) BodyWriter() io.Writer {
|
||||
req.w.r = req
|
||||
return &req.w
|
||||
}
|
||||
|
||||
type responseBodyWriter struct {
|
||||
r *Response
|
||||
}
|
||||
|
||||
func (w *responseBodyWriter) Write(p []byte) (int, error) {
|
||||
w.r.AppendBody(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
type requestBodyWriter struct {
|
||||
r *Request
|
||||
}
|
||||
|
||||
func (w *requestBodyWriter) Write(p []byte) (int, error) {
|
||||
w.r.AppendBody(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Body returns response body.
|
||||
//
|
||||
// The returned body is valid until the response modification.
|
||||
func (resp *Response) Body() []byte {
|
||||
if resp.bodyStream != nil {
|
||||
bodyBuf := resp.bodyBuffer()
|
||||
bodyBuf.Reset()
|
||||
_, err := copyZeroAlloc(bodyBuf, resp.bodyStream)
|
||||
resp.closeBodyStream()
|
||||
if err != nil {
|
||||
bodyBuf.SetString(err.Error())
|
||||
}
|
||||
}
|
||||
return resp.bodyBytes()
|
||||
}
|
||||
|
||||
func (resp *Response) bodyBytes() []byte {
|
||||
if resp.body == nil {
|
||||
return nil
|
||||
}
|
||||
return resp.body.B
|
||||
}
|
||||
|
||||
func (req *Request) bodyBytes() []byte {
|
||||
if req.body == nil {
|
||||
return nil
|
||||
}
|
||||
return req.body.B
|
||||
}
|
||||
|
||||
func (resp *Response) bodyBuffer() *bytebufferpool.ByteBuffer {
|
||||
if resp.body == nil {
|
||||
resp.body = responseBodyPool.Get()
|
||||
}
|
||||
return resp.body
|
||||
}
|
||||
|
||||
func (req *Request) bodyBuffer() *bytebufferpool.ByteBuffer {
|
||||
if req.body == nil {
|
||||
req.body = requestBodyPool.Get()
|
||||
}
|
||||
return req.body
|
||||
}
|
||||
|
||||
var (
|
||||
responseBodyPool bytebufferpool.Pool
|
||||
requestBodyPool bytebufferpool.Pool
|
||||
)
|
||||
|
||||
// BodyGunzip returns un-gzipped body data.
|
||||
//
|
||||
// This method may be used if the request header contains
|
||||
// 'Content-Encoding: gzip' for reading un-gzipped body.
|
||||
// Use Body for reading gzipped request body.
|
||||
func (req *Request) BodyGunzip() ([]byte, error) {
|
||||
return gunzipData(req.Body())
|
||||
}
|
||||
|
||||
// BodyGunzip returns un-gzipped body data.
|
||||
//
|
||||
// This method may be used if the response header contains
|
||||
// 'Content-Encoding: gzip' for reading un-gzipped body.
|
||||
// Use Body for reading gzipped response body.
|
||||
func (resp *Response) BodyGunzip() ([]byte, error) {
|
||||
return gunzipData(resp.Body())
|
||||
}
|
||||
|
||||
func gunzipData(p []byte) ([]byte, error) {
|
||||
var bb ByteBuffer
|
||||
_, err := WriteGunzip(&bb, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.B, nil
|
||||
}
|
||||
|
||||
// BodyInflate returns inflated body data.
|
||||
//
|
||||
// This method may be used if the response header contains
|
||||
// 'Content-Encoding: deflate' for reading inflated request body.
|
||||
// Use Body for reading deflated request body.
|
||||
func (req *Request) BodyInflate() ([]byte, error) {
|
||||
return inflateData(req.Body())
|
||||
}
|
||||
|
||||
// BodyInflate returns inflated body data.
|
||||
//
|
||||
// This method may be used if the response header contains
|
||||
// 'Content-Encoding: deflate' for reading inflated response body.
|
||||
// Use Body for reading deflated response body.
|
||||
func (resp *Response) BodyInflate() ([]byte, error) {
|
||||
return inflateData(resp.Body())
|
||||
}
|
||||
|
||||
func inflateData(p []byte) ([]byte, error) {
|
||||
var bb ByteBuffer
|
||||
_, err := WriteInflate(&bb, p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bb.B, nil
|
||||
}
|
||||
|
||||
// BodyWriteTo writes request body to w.
|
||||
func (req *Request) BodyWriteTo(w io.Writer) error {
|
||||
if req.bodyStream != nil {
|
||||
_, err := copyZeroAlloc(w, req.bodyStream)
|
||||
req.closeBodyStream()
|
||||
return err
|
||||
}
|
||||
if req.onlyMultipartForm() {
|
||||
return WriteMultipartForm(w, req.multipartForm, req.multipartFormBoundary)
|
||||
}
|
||||
_, err := w.Write(req.bodyBytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// BodyWriteTo writes response body to w.
|
||||
func (resp *Response) BodyWriteTo(w io.Writer) error {
|
||||
if resp.bodyStream != nil {
|
||||
_, err := copyZeroAlloc(w, resp.bodyStream)
|
||||
resp.closeBodyStream()
|
||||
return err
|
||||
}
|
||||
_, err := w.Write(resp.bodyBytes())
|
||||
return err
|
||||
}
|
||||
|
||||
// AppendBody appends p to response body.
|
||||
//
|
||||
// It is safe re-using p after the function returns.
|
||||
func (resp *Response) AppendBody(p []byte) {
|
||||
resp.AppendBodyString(b2s(p))
|
||||
}
|
||||
|
||||
// AppendBodyString appends s to response body.
|
||||
func (resp *Response) AppendBodyString(s string) {
|
||||
resp.closeBodyStream()
|
||||
resp.bodyBuffer().WriteString(s)
|
||||
}
|
||||
|
||||
// SetBody sets response body.
|
||||
//
|
||||
// It is safe re-using body argument after the function returns.
|
||||
func (resp *Response) SetBody(body []byte) {
|
||||
resp.SetBodyString(b2s(body))
|
||||
}
|
||||
|
||||
// SetBodyString sets response body.
|
||||
func (resp *Response) SetBodyString(body string) {
|
||||
resp.closeBodyStream()
|
||||
bodyBuf := resp.bodyBuffer()
|
||||
bodyBuf.Reset()
|
||||
bodyBuf.WriteString(body)
|
||||
}
|
||||
|
||||
// ResetBody resets response body.
|
||||
func (resp *Response) ResetBody() {
|
||||
resp.closeBodyStream()
|
||||
if resp.body != nil {
|
||||
if resp.keepBodyBuffer {
|
||||
resp.body.Reset()
|
||||
} else {
|
||||
responseBodyPool.Put(resp.body)
|
||||
resp.body = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReleaseBody retires the response body if it is greater than "size" bytes.
|
||||
//
|
||||
// This permits GC to reclaim the large buffer. If used, must be before
|
||||
// ReleaseResponse.
|
||||
//
|
||||
// Use this method only if you really understand how it works.
|
||||
// The majority of workloads don't need this method.
|
||||
func (resp *Response) ReleaseBody(size int) {
|
||||
if cap(resp.body.B) > size {
|
||||
resp.closeBodyStream()
|
||||
resp.body = nil
|
||||
}
|
||||
}
|
||||
|
||||
// ReleaseBody retires the request body if it is greater than "size" bytes.
|
||||
//
|
||||
// This permits GC to reclaim the large buffer. If used, must be before
|
||||
// ReleaseRequest.
|
||||
//
|
||||
// Use this method only if you really understand how it works.
|
||||
// The majority of workloads don't need this method.
|
||||
func (req *Request) ReleaseBody(size int) {
|
||||
if cap(req.body.B) > size {
|
||||
req.closeBodyStream()
|
||||
req.body = nil
|
||||
}
|
||||
}
|
||||
|
||||
// SwapBody swaps response body with the given body and returns
|
||||
// the previous response body.
|
||||
//
|
||||
// It is forbidden to use the body passed to SwapBody after
|
||||
// the function returns.
|
||||
func (resp *Response) SwapBody(body []byte) []byte {
|
||||
bb := resp.bodyBuffer()
|
||||
|
||||
if resp.bodyStream != nil {
|
||||
bb.Reset()
|
||||
_, err := copyZeroAlloc(bb, resp.bodyStream)
|
||||
resp.closeBodyStream()
|
||||
if err != nil {
|
||||
bb.Reset()
|
||||
bb.SetString(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
oldBody := bb.B
|
||||
bb.B = body
|
||||
return oldBody
|
||||
}
|
||||
|
||||
// SwapBody swaps request body with the given body and returns
|
||||
// the previous request body.
|
||||
//
|
||||
// It is forbidden to use the body passed to SwapBody after
|
||||
// the function returns.
|
||||
func (req *Request) SwapBody(body []byte) []byte {
|
||||
bb := req.bodyBuffer()
|
||||
|
||||
if req.bodyStream != nil {
|
||||
bb.Reset()
|
||||
_, err := copyZeroAlloc(bb, req.bodyStream)
|
||||
req.closeBodyStream()
|
||||
if err != nil {
|
||||
bb.Reset()
|
||||
bb.SetString(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
oldBody := bb.B
|
||||
bb.B = body
|
||||
return oldBody
|
||||
}
|
||||
|
||||
// Body returns request body.
|
||||
//
|
||||
// The returned body is valid until the request modification.
|
||||
func (req *Request) Body() []byte {
|
||||
if req.bodyStream != nil {
|
||||
bodyBuf := req.bodyBuffer()
|
||||
bodyBuf.Reset()
|
||||
_, err := copyZeroAlloc(bodyBuf, req.bodyStream)
|
||||
req.closeBodyStream()
|
||||
if err != nil {
|
||||
bodyBuf.SetString(err.Error())
|
||||
}
|
||||
} else if req.onlyMultipartForm() {
|
||||
body, err := marshalMultipartForm(req.multipartForm, req.multipartFormBoundary)
|
||||
if err != nil {
|
||||
return []byte(err.Error())
|
||||
}
|
||||
return body
|
||||
}
|
||||
return req.bodyBytes()
|
||||
}
|
||||
|
||||
// AppendBody appends p to request body.
|
||||
//
|
||||
// It is safe re-using p after the function returns.
|
||||
func (req *Request) AppendBody(p []byte) {
|
||||
req.AppendBodyString(b2s(p))
|
||||
}
|
||||
|
||||
// AppendBodyString appends s to request body.
|
||||
func (req *Request) AppendBodyString(s string) {
|
||||
req.RemoveMultipartFormFiles()
|
||||
req.closeBodyStream()
|
||||
req.bodyBuffer().WriteString(s)
|
||||
}
|
||||
|
||||
// SetBody sets request body.
|
||||
//
|
||||
// It is safe re-using body argument after the function returns.
|
||||
func (req *Request) SetBody(body []byte) {
|
||||
req.SetBodyString(b2s(body))
|
||||
}
|
||||
|
||||
// SetBodyString sets request body.
|
||||
func (req *Request) SetBodyString(body string) {
|
||||
req.RemoveMultipartFormFiles()
|
||||
req.closeBodyStream()
|
||||
req.bodyBuffer().SetString(body)
|
||||
}
|
||||
|
||||
// ResetBody resets request body.
|
||||
func (req *Request) ResetBody() {
|
||||
req.RemoveMultipartFormFiles()
|
||||
req.closeBodyStream()
|
||||
if req.body != nil {
|
||||
if req.keepBodyBuffer {
|
||||
req.body.Reset()
|
||||
} else {
|
||||
requestBodyPool.Put(req.body)
|
||||
req.body = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CopyTo copies req contents to dst except of body stream.
|
||||
func (req *Request) CopyTo(dst *Request) {
|
||||
req.copyToSkipBody(dst)
|
||||
if req.body != nil {
|
||||
dst.bodyBuffer().Set(req.body.B)
|
||||
} else if dst.body != nil {
|
||||
dst.body.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (req *Request) copyToSkipBody(dst *Request) {
|
||||
dst.Reset()
|
||||
req.Header.CopyTo(&dst.Header)
|
||||
|
||||
req.uri.CopyTo(&dst.uri)
|
||||
dst.parsedURI = req.parsedURI
|
||||
|
||||
req.postArgs.CopyTo(&dst.postArgs)
|
||||
dst.parsedPostArgs = req.parsedPostArgs
|
||||
dst.isTLS = req.isTLS
|
||||
|
||||
// do not copy multipartForm - it will be automatically
|
||||
// re-created on the first call to MultipartForm.
|
||||
}
|
||||
|
||||
// CopyTo copies resp contents to dst except of body stream.
|
||||
func (resp *Response) CopyTo(dst *Response) {
|
||||
resp.copyToSkipBody(dst)
|
||||
if resp.body != nil {
|
||||
dst.bodyBuffer().Set(resp.body.B)
|
||||
} else if dst.body != nil {
|
||||
dst.body.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (resp *Response) copyToSkipBody(dst *Response) {
|
||||
dst.Reset()
|
||||
resp.Header.CopyTo(&dst.Header)
|
||||
dst.SkipBody = resp.SkipBody
|
||||
}
|
||||
|
||||
func swapRequestBody(a, b *Request) {
|
||||
a.body, b.body = b.body, a.body
|
||||
a.bodyStream, b.bodyStream = b.bodyStream, a.bodyStream
|
||||
}
|
||||
|
||||
func swapResponseBody(a, b *Response) {
|
||||
a.body, b.body = b.body, a.body
|
||||
a.bodyStream, b.bodyStream = b.bodyStream, a.bodyStream
|
||||
}
|
||||
|
||||
// URI returns request URI
|
||||
func (req *Request) URI() *URI {
|
||||
req.parseURI()
|
||||
return &req.uri
|
||||
}
|
||||
|
||||
func (req *Request) parseURI() {
|
||||
if req.parsedURI {
|
||||
return
|
||||
}
|
||||
req.parsedURI = true
|
||||
|
||||
req.uri.parseQuick(req.Header.RequestURI(), &req.Header, req.isTLS)
|
||||
}
|
||||
|
||||
// PostArgs returns POST arguments.
|
||||
func (req *Request) PostArgs() *Args {
|
||||
req.parsePostArgs()
|
||||
return &req.postArgs
|
||||
}
|
||||
|
||||
func (req *Request) parsePostArgs() {
|
||||
if req.parsedPostArgs {
|
||||
return
|
||||
}
|
||||
req.parsedPostArgs = true
|
||||
|
||||
if !bytes.HasPrefix(req.Header.ContentType(), strPostArgsContentType) {
|
||||
return
|
||||
}
|
||||
req.postArgs.ParseBytes(req.bodyBytes())
|
||||
}
|
||||
|
||||
// ErrNoMultipartForm means that the request's Content-Type
|
||||
// isn't 'multipart/form-data'.
|
||||
var ErrNoMultipartForm = errors.New("request has no multipart/form-data Content-Type")
|
||||
|
||||
// MultipartForm returns requests's multipart form.
|
||||
//
|
||||
// Returns ErrNoMultipartForm if request's Content-Type
|
||||
// isn't 'multipart/form-data'.
|
||||
//
|
||||
// RemoveMultipartFormFiles must be called after returned multipart form
|
||||
// is processed.
|
||||
func (req *Request) MultipartForm() (*multipart.Form, error) {
|
||||
if req.multipartForm != nil {
|
||||
return req.multipartForm, nil
|
||||
}
|
||||
|
||||
req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
|
||||
if len(req.multipartFormBoundary) == 0 {
|
||||
return nil, ErrNoMultipartForm
|
||||
}
|
||||
|
||||
ce := req.Header.peek(strContentEncoding)
|
||||
body := req.bodyBytes()
|
||||
if bytes.Equal(ce, strGzip) {
|
||||
// Do not care about memory usage here.
|
||||
var err error
|
||||
if body, err = AppendGunzipBytes(nil, body); err != nil {
|
||||
return nil, fmt.Errorf("cannot gunzip request body: %s", err)
|
||||
}
|
||||
} else if len(ce) > 0 {
|
||||
return nil, fmt.Errorf("unsupported Content-Encoding: %q", ce)
|
||||
}
|
||||
|
||||
f, err := readMultipartForm(bytes.NewReader(body), req.multipartFormBoundary, len(body), len(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.multipartForm = f
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func marshalMultipartForm(f *multipart.Form, boundary string) ([]byte, error) {
|
||||
var buf ByteBuffer
|
||||
if err := WriteMultipartForm(&buf, f, boundary); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.B, nil
|
||||
}
|
||||
|
||||
// WriteMultipartForm writes the given multipart form f with the given
|
||||
// boundary to w.
|
||||
func WriteMultipartForm(w io.Writer, f *multipart.Form, boundary string) error {
|
||||
// Do not care about memory allocations here, since multipart
|
||||
// form processing is slooow.
|
||||
if len(boundary) == 0 {
|
||||
panic("BUG: form boundary cannot be empty")
|
||||
}
|
||||
|
||||
mw := multipart.NewWriter(w)
|
||||
if err := mw.SetBoundary(boundary); err != nil {
|
||||
return fmt.Errorf("cannot use form boundary %q: %s", boundary, err)
|
||||
}
|
||||
|
||||
// marshal values
|
||||
for k, vv := range f.Value {
|
||||
for _, v := range vv {
|
||||
if err := mw.WriteField(k, v); err != nil {
|
||||
return fmt.Errorf("cannot write form field %q value %q: %s", k, v, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// marshal files
|
||||
for k, fvv := range f.File {
|
||||
for _, fv := range fvv {
|
||||
vw, err := mw.CreateFormFile(k, fv.Filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot create form file %q (%q): %s", k, fv.Filename, err)
|
||||
}
|
||||
fh, err := fv.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot open form file %q (%q): %s", k, fv.Filename, err)
|
||||
}
|
||||
if _, err = copyZeroAlloc(vw, fh); err != nil {
|
||||
return fmt.Errorf("error when copying form file %q (%q): %s", k, fv.Filename, err)
|
||||
}
|
||||
if err = fh.Close(); err != nil {
|
||||
return fmt.Errorf("cannot close form file %q (%q): %s", k, fv.Filename, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := mw.Close(); err != nil {
|
||||
return fmt.Errorf("error when closing multipart form writer: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readMultipartForm(r io.Reader, boundary string, size, maxInMemoryFileSize int) (*multipart.Form, error) {
|
||||
// Do not care about memory allocations here, since they are tiny
|
||||
// compared to multipart data (aka multi-MB files) usually sent
|
||||
// in multipart/form-data requests.
|
||||
|
||||
if size <= 0 {
|
||||
panic(fmt.Sprintf("BUG: form size must be greater than 0. Given %d", size))
|
||||
}
|
||||
lr := io.LimitReader(r, int64(size))
|
||||
mr := multipart.NewReader(lr, boundary)
|
||||
f, err := mr.ReadForm(int64(maxInMemoryFileSize))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read multipart/form-data body: %s", err)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Reset clears request contents.
|
||||
func (req *Request) Reset() {
|
||||
req.Header.Reset()
|
||||
req.resetSkipHeader()
|
||||
}
|
||||
|
||||
func (req *Request) resetSkipHeader() {
|
||||
req.ResetBody()
|
||||
req.uri.Reset()
|
||||
req.parsedURI = false
|
||||
req.postArgs.Reset()
|
||||
req.parsedPostArgs = false
|
||||
req.isTLS = false
|
||||
}
|
||||
|
||||
// RemoveMultipartFormFiles removes multipart/form-data temporary files
|
||||
// associated with the request.
|
||||
func (req *Request) RemoveMultipartFormFiles() {
|
||||
if req.multipartForm != nil {
|
||||
// Do not check for error, since these files may be deleted or moved
|
||||
// to new places by user code.
|
||||
req.multipartForm.RemoveAll()
|
||||
req.multipartForm = nil
|
||||
}
|
||||
req.multipartFormBoundary = ""
|
||||
}
|
||||
|
||||
// Reset clears response contents.
|
||||
func (resp *Response) Reset() {
|
||||
resp.Header.Reset()
|
||||
resp.resetSkipHeader()
|
||||
resp.SkipBody = false
|
||||
}
|
||||
|
||||
func (resp *Response) resetSkipHeader() {
|
||||
resp.ResetBody()
|
||||
}
|
||||
|
||||
// Read reads request (including body) from the given r.
|
||||
//
|
||||
// RemoveMultipartFormFiles or Reset must be called after
|
||||
// reading multipart/form-data request in order to delete temporarily
|
||||
// uploaded files.
|
||||
//
|
||||
// If MayContinue returns true, the caller must:
|
||||
//
|
||||
// - Either send StatusExpectationFailed response if request headers don't
|
||||
// satisfy the caller.
|
||||
// - Or send StatusContinue response before reading request body
|
||||
// with ContinueReadBody.
|
||||
// - Or close the connection.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (req *Request) Read(r *bufio.Reader) error {
|
||||
return req.ReadLimitBody(r, 0)
|
||||
}
|
||||
|
||||
const defaultMaxInMemoryFileSize = 16 * 1024 * 1024
|
||||
|
||||
var errGetOnly = errors.New("non-GET request received")
|
||||
|
||||
// ReadLimitBody reads request from the given r, limiting the body size.
|
||||
//
|
||||
// If maxBodySize > 0 and the body size exceeds maxBodySize,
|
||||
// then ErrBodyTooLarge is returned.
|
||||
//
|
||||
// RemoveMultipartFormFiles or Reset must be called after
|
||||
// reading multipart/form-data request in order to delete temporarily
|
||||
// uploaded files.
|
||||
//
|
||||
// If MayContinue returns true, the caller must:
|
||||
//
|
||||
// - Either send StatusExpectationFailed response if request headers don't
|
||||
// satisfy the caller.
|
||||
// - Or send StatusContinue response before reading request body
|
||||
// with ContinueReadBody.
|
||||
// - Or close the connection.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (req *Request) ReadLimitBody(r *bufio.Reader, maxBodySize int) error {
|
||||
req.resetSkipHeader()
|
||||
return req.readLimitBody(r, maxBodySize, false)
|
||||
}
|
||||
|
||||
func (req *Request) readLimitBody(r *bufio.Reader, maxBodySize int, getOnly bool) error {
|
||||
// Do not reset the request here - the caller must reset it before
|
||||
// calling this method.
|
||||
|
||||
err := req.Header.Read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if getOnly && !req.Header.IsGet() {
|
||||
return errGetOnly
|
||||
}
|
||||
|
||||
if req.Header.noBody() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if req.MayContinue() {
|
||||
// 'Expect: 100-continue' header found. Let the caller deciding
|
||||
// whether to read request body or
|
||||
// to return StatusExpectationFailed.
|
||||
return nil
|
||||
}
|
||||
|
||||
return req.ContinueReadBody(r, maxBodySize)
|
||||
}
|
||||
|
||||
// MayContinue returns true if the request contains
|
||||
// 'Expect: 100-continue' header.
|
||||
//
|
||||
// The caller must do one of the following actions if MayContinue returns true:
|
||||
//
|
||||
// - Either send StatusExpectationFailed response if request headers don't
|
||||
// satisfy the caller.
|
||||
// - Or send StatusContinue response before reading request body
|
||||
// with ContinueReadBody.
|
||||
// - Or close the connection.
|
||||
func (req *Request) MayContinue() bool {
|
||||
return bytes.Equal(req.Header.peek(strExpect), str100Continue)
|
||||
}
|
||||
|
||||
// ContinueReadBody reads request body if request header contains
|
||||
// 'Expect: 100-continue'.
|
||||
//
|
||||
// The caller must send StatusContinue response before calling this method.
|
||||
//
|
||||
// If maxBodySize > 0 and the body size exceeds maxBodySize,
|
||||
// then ErrBodyTooLarge is returned.
|
||||
func (req *Request) ContinueReadBody(r *bufio.Reader, maxBodySize int) error {
|
||||
var err error
|
||||
contentLength := req.Header.ContentLength()
|
||||
if contentLength > 0 {
|
||||
if maxBodySize > 0 && contentLength > maxBodySize {
|
||||
return ErrBodyTooLarge
|
||||
}
|
||||
|
||||
// Pre-read multipart form data of known length.
|
||||
// This way we limit memory usage for large file uploads, since their contents
|
||||
// is streamed into temporary files if file size exceeds defaultMaxInMemoryFileSize.
|
||||
req.multipartFormBoundary = string(req.Header.MultipartFormBoundary())
|
||||
if len(req.multipartFormBoundary) > 0 && len(req.Header.peek(strContentEncoding)) == 0 {
|
||||
req.multipartForm, err = readMultipartForm(r, req.multipartFormBoundary, contentLength, defaultMaxInMemoryFileSize)
|
||||
if err != nil {
|
||||
req.Reset()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if contentLength == -2 {
|
||||
// identity body has no sense for http requests, since
|
||||
// the end of body is determined by connection close.
|
||||
// So just ignore request body for requests without
|
||||
// 'Content-Length' and 'Transfer-Encoding' headers.
|
||||
req.Header.SetContentLength(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
bodyBuf := req.bodyBuffer()
|
||||
bodyBuf.Reset()
|
||||
bodyBuf.B, err = readBody(r, contentLength, maxBodySize, bodyBuf.B)
|
||||
if err != nil {
|
||||
req.Reset()
|
||||
return err
|
||||
}
|
||||
req.Header.SetContentLength(len(bodyBuf.B))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read reads response (including body) from the given r.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (resp *Response) Read(r *bufio.Reader) error {
|
||||
return resp.ReadLimitBody(r, 0)
|
||||
}
|
||||
|
||||
// ReadLimitBody reads response from the given r, limiting the body size.
|
||||
//
|
||||
// If maxBodySize > 0 and the body size exceeds maxBodySize,
|
||||
// then ErrBodyTooLarge is returned.
|
||||
//
|
||||
// io.EOF is returned if r is closed before reading the first header byte.
|
||||
func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error {
|
||||
resp.resetSkipHeader()
|
||||
err := resp.Header.Read(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if resp.Header.StatusCode() == StatusContinue {
|
||||
// Read the next response according to http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html .
|
||||
if err = resp.Header.Read(r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if !resp.mustSkipBody() {
|
||||
bodyBuf := resp.bodyBuffer()
|
||||
bodyBuf.Reset()
|
||||
bodyBuf.B, err = readBody(r, resp.Header.ContentLength(), maxBodySize, bodyBuf.B)
|
||||
if err != nil {
|
||||
resp.Reset()
|
||||
return err
|
||||
}
|
||||
resp.Header.SetContentLength(len(bodyBuf.B))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (resp *Response) mustSkipBody() bool {
|
||||
return resp.SkipBody || resp.Header.mustSkipContentLength()
|
||||
}
|
||||
|
||||
var errRequestHostRequired = errors.New("missing required Host header in request")
|
||||
|
||||
// WriteTo writes request to w. It implements io.WriterTo.
|
||||
func (req *Request) WriteTo(w io.Writer) (int64, error) {
|
||||
return writeBufio(req, w)
|
||||
}
|
||||
|
||||
// WriteTo writes response to w. It implements io.WriterTo.
|
||||
func (resp *Response) WriteTo(w io.Writer) (int64, error) {
|
||||
return writeBufio(resp, w)
|
||||
}
|
||||
|
||||
func writeBufio(hw httpWriter, w io.Writer) (int64, error) {
|
||||
sw := acquireStatsWriter(w)
|
||||
bw := acquireBufioWriter(sw)
|
||||
err1 := hw.Write(bw)
|
||||
err2 := bw.Flush()
|
||||
releaseBufioWriter(bw)
|
||||
n := sw.bytesWritten
|
||||
releaseStatsWriter(sw)
|
||||
|
||||
err := err1
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
type statsWriter struct {
|
||||
w io.Writer
|
||||
bytesWritten int64
|
||||
}
|
||||
|
||||
func (w *statsWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.w.Write(p)
|
||||
w.bytesWritten += int64(n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func acquireStatsWriter(w io.Writer) *statsWriter {
|
||||
v := statsWriterPool.Get()
|
||||
if v == nil {
|
||||
return &statsWriter{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
sw := v.(*statsWriter)
|
||||
sw.w = w
|
||||
return sw
|
||||
}
|
||||
|
||||
func releaseStatsWriter(sw *statsWriter) {
|
||||
sw.w = nil
|
||||
sw.bytesWritten = 0
|
||||
statsWriterPool.Put(sw)
|
||||
}
|
||||
|
||||
var statsWriterPool sync.Pool
|
||||
|
||||
func acquireBufioWriter(w io.Writer) *bufio.Writer {
|
||||
v := bufioWriterPool.Get()
|
||||
if v == nil {
|
||||
return bufio.NewWriter(w)
|
||||
}
|
||||
bw := v.(*bufio.Writer)
|
||||
bw.Reset(w)
|
||||
return bw
|
||||
}
|
||||
|
||||
func releaseBufioWriter(bw *bufio.Writer) {
|
||||
bufioWriterPool.Put(bw)
|
||||
}
|
||||
|
||||
var bufioWriterPool sync.Pool
|
||||
|
||||
func (req *Request) onlyMultipartForm() bool {
|
||||
return req.multipartForm != nil && (req.body == nil || len(req.body.B) == 0)
|
||||
}
|
||||
|
||||
// Write writes request to w.
|
||||
//
|
||||
// Write doesn't flush request to w for performance reasons.
|
||||
//
|
||||
// See also WriteTo.
|
||||
func (req *Request) Write(w *bufio.Writer) error {
|
||||
if len(req.Header.Host()) == 0 || req.parsedURI {
|
||||
uri := req.URI()
|
||||
host := uri.Host()
|
||||
if len(host) == 0 {
|
||||
return errRequestHostRequired
|
||||
}
|
||||
req.Header.SetHostBytes(host)
|
||||
req.Header.SetRequestURIBytes(uri.RequestURI())
|
||||
}
|
||||
|
||||
if req.bodyStream != nil {
|
||||
return req.writeBodyStream(w)
|
||||
}
|
||||
|
||||
body := req.bodyBytes()
|
||||
var err error
|
||||
if req.onlyMultipartForm() {
|
||||
body, err = marshalMultipartForm(req.multipartForm, req.multipartFormBoundary)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error when marshaling multipart form: %s", err)
|
||||
}
|
||||
req.Header.SetMultipartFormBoundary(req.multipartFormBoundary)
|
||||
}
|
||||
|
||||
hasBody := !req.Header.noBody()
|
||||
if hasBody {
|
||||
req.Header.SetContentLength(len(body))
|
||||
}
|
||||
if err = req.Header.Write(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasBody {
|
||||
_, err = w.Write(body)
|
||||
} else if len(body) > 0 {
|
||||
return fmt.Errorf("non-zero body for non-POST request. body=%q", body)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteGzip writes response with gzipped body to w.
|
||||
//
|
||||
// The method gzips response body and sets 'Content-Encoding: gzip'
|
||||
// header before writing response to w.
|
||||
//
|
||||
// WriteGzip doesn't flush response to w for performance reasons.
|
||||
func (resp *Response) WriteGzip(w *bufio.Writer) error {
|
||||
return resp.WriteGzipLevel(w, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// WriteGzipLevel writes response with gzipped body to w.
|
||||
//
|
||||
// Level is the desired compression level:
|
||||
//
|
||||
// * CompressNoCompression
|
||||
// * CompressBestSpeed
|
||||
// * CompressBestCompression
|
||||
// * CompressDefaultCompression
|
||||
// * CompressHuffmanOnly
|
||||
//
|
||||
// The method gzips response body and sets 'Content-Encoding: gzip'
|
||||
// header before writing response to w.
|
||||
//
|
||||
// WriteGzipLevel doesn't flush response to w for performance reasons.
|
||||
func (resp *Response) WriteGzipLevel(w *bufio.Writer, level int) error {
|
||||
if err := resp.gzipBody(level); err != nil {
|
||||
return err
|
||||
}
|
||||
return resp.Write(w)
|
||||
}
|
||||
|
||||
// WriteDeflate writes response with deflated body to w.
|
||||
//
|
||||
// The method deflates response body and sets 'Content-Encoding: deflate'
|
||||
// header before writing response to w.
|
||||
//
|
||||
// WriteDeflate doesn't flush response to w for performance reasons.
|
||||
func (resp *Response) WriteDeflate(w *bufio.Writer) error {
|
||||
return resp.WriteDeflateLevel(w, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// WriteDeflateLevel writes response with deflated body to w.
|
||||
//
|
||||
// Level is the desired compression level:
|
||||
//
|
||||
// * CompressNoCompression
|
||||
// * CompressBestSpeed
|
||||
// * CompressBestCompression
|
||||
// * CompressDefaultCompression
|
||||
// * CompressHuffmanOnly
|
||||
//
|
||||
// The method deflates response body and sets 'Content-Encoding: deflate'
|
||||
// header before writing response to w.
|
||||
//
|
||||
// WriteDeflateLevel doesn't flush response to w for performance reasons.
|
||||
func (resp *Response) WriteDeflateLevel(w *bufio.Writer, level int) error {
|
||||
if err := resp.deflateBody(level); err != nil {
|
||||
return err
|
||||
}
|
||||
return resp.Write(w)
|
||||
}
|
||||
|
||||
func (resp *Response) gzipBody(level int) error {
|
||||
if len(resp.Header.peek(strContentEncoding)) > 0 {
|
||||
// It looks like the body is already compressed.
|
||||
// Do not compress it again.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !resp.Header.isCompressibleContentType() {
|
||||
// The content-type cannot be compressed.
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.bodyStream != nil {
|
||||
// Reset Content-Length to -1, since it is impossible
|
||||
// to determine body size beforehand of streamed compression.
|
||||
// For https://github.com/valyala/fasthttp/issues/176 .
|
||||
resp.Header.SetContentLength(-1)
|
||||
|
||||
// Do not care about memory allocations here, since gzip is slow
|
||||
// and allocates a lot of memory by itself.
|
||||
bs := resp.bodyStream
|
||||
resp.bodyStream = NewStreamReader(func(sw *bufio.Writer) {
|
||||
zw := acquireStacklessGzipWriter(sw, level)
|
||||
fw := &flushWriter{
|
||||
wf: zw,
|
||||
bw: sw,
|
||||
}
|
||||
copyZeroAlloc(fw, bs)
|
||||
releaseStacklessGzipWriter(zw, level)
|
||||
if bsc, ok := bs.(io.Closer); ok {
|
||||
bsc.Close()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
bodyBytes := resp.bodyBytes()
|
||||
if len(bodyBytes) < minCompressLen {
|
||||
// There is no sense in spending CPU time on small body compression,
|
||||
// since there is a very high probability that the compressed
|
||||
// body size will be bigger than the original body size.
|
||||
return nil
|
||||
}
|
||||
w := responseBodyPool.Get()
|
||||
w.B = AppendGzipBytesLevel(w.B, bodyBytes, level)
|
||||
|
||||
// Hack: swap resp.body with w.
|
||||
if resp.body != nil {
|
||||
responseBodyPool.Put(resp.body)
|
||||
}
|
||||
resp.body = w
|
||||
}
|
||||
resp.Header.SetCanonical(strContentEncoding, strGzip)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (resp *Response) deflateBody(level int) error {
|
||||
if len(resp.Header.peek(strContentEncoding)) > 0 {
|
||||
// It looks like the body is already compressed.
|
||||
// Do not compress it again.
|
||||
return nil
|
||||
}
|
||||
|
||||
if !resp.Header.isCompressibleContentType() {
|
||||
// The content-type cannot be compressed.
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.bodyStream != nil {
|
||||
// Reset Content-Length to -1, since it is impossible
|
||||
// to determine body size beforehand of streamed compression.
|
||||
// For https://github.com/valyala/fasthttp/issues/176 .
|
||||
resp.Header.SetContentLength(-1)
|
||||
|
||||
// Do not care about memory allocations here, since flate is slow
|
||||
// and allocates a lot of memory by itself.
|
||||
bs := resp.bodyStream
|
||||
resp.bodyStream = NewStreamReader(func(sw *bufio.Writer) {
|
||||
zw := acquireStacklessDeflateWriter(sw, level)
|
||||
fw := &flushWriter{
|
||||
wf: zw,
|
||||
bw: sw,
|
||||
}
|
||||
copyZeroAlloc(fw, bs)
|
||||
releaseStacklessDeflateWriter(zw, level)
|
||||
if bsc, ok := bs.(io.Closer); ok {
|
||||
bsc.Close()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
bodyBytes := resp.bodyBytes()
|
||||
if len(bodyBytes) < minCompressLen {
|
||||
// There is no sense in spending CPU time on small body compression,
|
||||
// since there is a very high probability that the compressed
|
||||
// body size will be bigger than the original body size.
|
||||
return nil
|
||||
}
|
||||
w := responseBodyPool.Get()
|
||||
w.B = AppendDeflateBytesLevel(w.B, bodyBytes, level)
|
||||
|
||||
// Hack: swap resp.body with w.
|
||||
if resp.body != nil {
|
||||
responseBodyPool.Put(resp.body)
|
||||
}
|
||||
resp.body = w
|
||||
}
|
||||
resp.Header.SetCanonical(strContentEncoding, strDeflate)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Bodies with sizes smaller than minCompressLen aren't compressed at all
|
||||
const minCompressLen = 200
|
||||
|
||||
type writeFlusher interface {
|
||||
io.Writer
|
||||
Flush() error
|
||||
}
|
||||
|
||||
type flushWriter struct {
|
||||
wf writeFlusher
|
||||
bw *bufio.Writer
|
||||
}
|
||||
|
||||
func (w *flushWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.wf.Write(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = w.wf.Flush(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err = w.bw.Flush(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Write writes response to w.
|
||||
//
|
||||
// Write doesn't flush response to w for performance reasons.
|
||||
//
|
||||
// See also WriteTo.
|
||||
func (resp *Response) Write(w *bufio.Writer) error {
|
||||
sendBody := !resp.mustSkipBody()
|
||||
|
||||
if resp.bodyStream != nil {
|
||||
return resp.writeBodyStream(w, sendBody)
|
||||
}
|
||||
|
||||
body := resp.bodyBytes()
|
||||
bodyLen := len(body)
|
||||
if sendBody || bodyLen > 0 {
|
||||
resp.Header.SetContentLength(bodyLen)
|
||||
}
|
||||
if err := resp.Header.Write(w); err != nil {
|
||||
return err
|
||||
}
|
||||
if sendBody {
|
||||
if _, err := w.Write(body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (req *Request) writeBodyStream(w *bufio.Writer) error {
|
||||
var err error
|
||||
|
||||
contentLength := req.Header.ContentLength()
|
||||
if contentLength < 0 {
|
||||
lrSize := limitedReaderSize(req.bodyStream)
|
||||
if lrSize >= 0 {
|
||||
contentLength = int(lrSize)
|
||||
if int64(contentLength) != lrSize {
|
||||
contentLength = -1
|
||||
}
|
||||
if contentLength >= 0 {
|
||||
req.Header.SetContentLength(contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
if contentLength >= 0 {
|
||||
if err = req.Header.Write(w); err == nil {
|
||||
err = writeBodyFixedSize(w, req.bodyStream, int64(contentLength))
|
||||
}
|
||||
} else {
|
||||
req.Header.SetContentLength(-1)
|
||||
if err = req.Header.Write(w); err == nil {
|
||||
err = writeBodyChunked(w, req.bodyStream)
|
||||
}
|
||||
}
|
||||
err1 := req.closeBodyStream()
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (resp *Response) writeBodyStream(w *bufio.Writer, sendBody bool) error {
|
||||
var err error
|
||||
|
||||
contentLength := resp.Header.ContentLength()
|
||||
if contentLength < 0 {
|
||||
lrSize := limitedReaderSize(resp.bodyStream)
|
||||
if lrSize >= 0 {
|
||||
contentLength = int(lrSize)
|
||||
if int64(contentLength) != lrSize {
|
||||
contentLength = -1
|
||||
}
|
||||
if contentLength >= 0 {
|
||||
resp.Header.SetContentLength(contentLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
if contentLength >= 0 {
|
||||
if err = resp.Header.Write(w); err == nil && sendBody {
|
||||
err = writeBodyFixedSize(w, resp.bodyStream, int64(contentLength))
|
||||
}
|
||||
} else {
|
||||
resp.Header.SetContentLength(-1)
|
||||
if err = resp.Header.Write(w); err == nil && sendBody {
|
||||
err = writeBodyChunked(w, resp.bodyStream)
|
||||
}
|
||||
}
|
||||
err1 := resp.closeBodyStream()
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (req *Request) closeBodyStream() error {
|
||||
if req.bodyStream == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if bsc, ok := req.bodyStream.(io.Closer); ok {
|
||||
err = bsc.Close()
|
||||
}
|
||||
req.bodyStream = nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (resp *Response) closeBodyStream() error {
|
||||
if resp.bodyStream == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
if bsc, ok := resp.bodyStream.(io.Closer); ok {
|
||||
err = bsc.Close()
|
||||
}
|
||||
resp.bodyStream = nil
|
||||
return err
|
||||
}
|
||||
|
||||
// String returns request representation.
|
||||
//
|
||||
// Returns error message instead of request representation on error.
|
||||
//
|
||||
// Use Write instead of String for performance-critical code.
|
||||
func (req *Request) String() string {
|
||||
return getHTTPString(req)
|
||||
}
|
||||
|
||||
// String returns response representation.
|
||||
//
|
||||
// Returns error message instead of response representation on error.
|
||||
//
|
||||
// Use Write instead of String for performance-critical code.
|
||||
func (resp *Response) String() string {
|
||||
return getHTTPString(resp)
|
||||
}
|
||||
|
||||
func getHTTPString(hw httpWriter) string {
|
||||
w := AcquireByteBuffer()
|
||||
bw := bufio.NewWriter(w)
|
||||
if err := hw.Write(bw); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
if err := bw.Flush(); err != nil {
|
||||
return err.Error()
|
||||
}
|
||||
s := string(w.B)
|
||||
ReleaseByteBuffer(w)
|
||||
return s
|
||||
}
|
||||
|
||||
type httpWriter interface {
|
||||
Write(w *bufio.Writer) error
|
||||
}
|
||||
|
||||
func writeBodyChunked(w *bufio.Writer, r io.Reader) error {
|
||||
vbuf := copyBufPool.Get()
|
||||
buf := vbuf.([]byte)
|
||||
|
||||
var err error
|
||||
var n int
|
||||
for {
|
||||
n, err = r.Read(buf)
|
||||
if n == 0 {
|
||||
if err == nil {
|
||||
panic("BUG: io.Reader returned 0, nil")
|
||||
}
|
||||
if err == io.EOF {
|
||||
if err = writeChunk(w, buf[:0]); err != nil {
|
||||
break
|
||||
}
|
||||
err = nil
|
||||
}
|
||||
break
|
||||
}
|
||||
if err = writeChunk(w, buf[:n]); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
copyBufPool.Put(vbuf)
|
||||
return err
|
||||
}
|
||||
|
||||
func limitedReaderSize(r io.Reader) int64 {
|
||||
lr, ok := r.(*io.LimitedReader)
|
||||
if !ok {
|
||||
return -1
|
||||
}
|
||||
return lr.N
|
||||
}
|
||||
|
||||
func writeBodyFixedSize(w *bufio.Writer, r io.Reader, size int64) error {
|
||||
if size > maxSmallFileSize {
|
||||
// w buffer must be empty for triggering
|
||||
// sendfile path in bufio.Writer.ReadFrom.
|
||||
if err := w.Flush(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap a single limited reader for triggering sendfile path
|
||||
// in net.TCPConn.ReadFrom.
|
||||
lr, ok := r.(*io.LimitedReader)
|
||||
if ok {
|
||||
r = lr.R
|
||||
}
|
||||
|
||||
n, err := copyZeroAlloc(w, r)
|
||||
|
||||
if ok {
|
||||
lr.N -= n
|
||||
}
|
||||
|
||||
if n != size && err == nil {
|
||||
err = fmt.Errorf("copied %d bytes from body stream instead of %d bytes", n, size)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func copyZeroAlloc(w io.Writer, r io.Reader) (int64, error) {
|
||||
vbuf := copyBufPool.Get()
|
||||
buf := vbuf.([]byte)
|
||||
n, err := io.CopyBuffer(w, r, buf)
|
||||
copyBufPool.Put(vbuf)
|
||||
return n, err
|
||||
}
|
||||
|
||||
var copyBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make([]byte, 4096)
|
||||
},
|
||||
}
|
||||
|
||||
func writeChunk(w *bufio.Writer, b []byte) error {
|
||||
n := len(b)
|
||||
writeHexInt(w, n)
|
||||
w.Write(strCRLF)
|
||||
w.Write(b)
|
||||
_, err := w.Write(strCRLF)
|
||||
err1 := w.Flush()
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrBodyTooLarge is returned if either request or response body exceeds
|
||||
// the given limit.
|
||||
var ErrBodyTooLarge = errors.New("body size exceeds the given limit")
|
||||
|
||||
func readBody(r *bufio.Reader, contentLength int, maxBodySize int, dst []byte) ([]byte, error) {
|
||||
dst = dst[:0]
|
||||
if contentLength >= 0 {
|
||||
if maxBodySize > 0 && contentLength > maxBodySize {
|
||||
return dst, ErrBodyTooLarge
|
||||
}
|
||||
return appendBodyFixedSize(r, dst, contentLength)
|
||||
}
|
||||
if contentLength == -1 {
|
||||
return readBodyChunked(r, maxBodySize, dst)
|
||||
}
|
||||
return readBodyIdentity(r, maxBodySize, dst)
|
||||
}
|
||||
|
||||
func readBodyIdentity(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) {
|
||||
dst = dst[:cap(dst)]
|
||||
if len(dst) == 0 {
|
||||
dst = make([]byte, 1024)
|
||||
}
|
||||
offset := 0
|
||||
for {
|
||||
nn, err := r.Read(dst[offset:])
|
||||
if nn <= 0 {
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return dst[:offset], nil
|
||||
}
|
||||
return dst[:offset], err
|
||||
}
|
||||
panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn))
|
||||
}
|
||||
offset += nn
|
||||
if maxBodySize > 0 && offset > maxBodySize {
|
||||
return dst[:offset], ErrBodyTooLarge
|
||||
}
|
||||
if len(dst) == offset {
|
||||
n := round2(2 * offset)
|
||||
if maxBodySize > 0 && n > maxBodySize {
|
||||
n = maxBodySize + 1
|
||||
}
|
||||
b := make([]byte, n)
|
||||
copy(b, dst)
|
||||
dst = b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func appendBodyFixedSize(r *bufio.Reader, dst []byte, n int) ([]byte, error) {
|
||||
if n == 0 {
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
offset := len(dst)
|
||||
dstLen := offset + n
|
||||
if cap(dst) < dstLen {
|
||||
b := make([]byte, round2(dstLen))
|
||||
copy(b, dst)
|
||||
dst = b
|
||||
}
|
||||
dst = dst[:dstLen]
|
||||
|
||||
for {
|
||||
nn, err := r.Read(dst[offset:])
|
||||
if nn <= 0 {
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = io.ErrUnexpectedEOF
|
||||
}
|
||||
return dst[:offset], err
|
||||
}
|
||||
panic(fmt.Sprintf("BUG: bufio.Read() returned (%d, nil)", nn))
|
||||
}
|
||||
offset += nn
|
||||
if offset == dstLen {
|
||||
return dst, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readBodyChunked(r *bufio.Reader, maxBodySize int, dst []byte) ([]byte, error) {
|
||||
if len(dst) > 0 {
|
||||
panic("BUG: expected zero-length buffer")
|
||||
}
|
||||
|
||||
strCRLFLen := len(strCRLF)
|
||||
for {
|
||||
chunkSize, err := parseChunkSize(r)
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
if maxBodySize > 0 && len(dst)+chunkSize > maxBodySize {
|
||||
return dst, ErrBodyTooLarge
|
||||
}
|
||||
dst, err = appendBodyFixedSize(r, dst, chunkSize+strCRLFLen)
|
||||
if err != nil {
|
||||
return dst, err
|
||||
}
|
||||
if !bytes.Equal(dst[len(dst)-strCRLFLen:], strCRLF) {
|
||||
return dst, fmt.Errorf("cannot find crlf at the end of chunk")
|
||||
}
|
||||
dst = dst[:len(dst)-strCRLFLen]
|
||||
if chunkSize == 0 {
|
||||
return dst, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseChunkSize(r *bufio.Reader) (int, error) {
|
||||
n, err := readHexInt(r)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
c, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("cannot read '\r' char at the end of chunk size: %s", err)
|
||||
}
|
||||
if c != '\r' {
|
||||
return -1, fmt.Errorf("unexpected char %q at the end of chunk size. Expected %q", c, '\r')
|
||||
}
|
||||
c, err = r.ReadByte()
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("cannot read '\n' char at the end of chunk size: %s", err)
|
||||
}
|
||||
if c != '\n' {
|
||||
return -1, fmt.Errorf("unexpected char %q at the end of chunk size. Expected %q", c, '\n')
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func round2(n int) int {
|
||||
if n <= 0 {
|
||||
return 0
|
||||
}
|
||||
n--
|
||||
x := uint(0)
|
||||
for n > 0 {
|
||||
n >>= 1
|
||||
x++
|
||||
}
|
||||
return 1 << x
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
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' + 'round robin'
|
||||
// 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
|
||||
|
||||
// nextIdx is for spreading requests among equally loaded clients
|
||||
// in a round-robin fashion.
|
||||
nextIdx uint32
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
// DefaultLBClientTimeout is the default request timeout used by LBClient
|
||||
// when calling LBClient.Do.
|
||||
//
|
||||
// The timeout may be overriden 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 calls calculates deadline using LBClient.Timeout and calls DoDeadline
|
||||
// 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() {
|
||||
if len(cc.Clients) == 0 {
|
||||
panic("BUG: LBClient.Clients cannot be empty")
|
||||
}
|
||||
for _, c := range cc.Clients {
|
||||
cc.cs = append(cc.cs, &lbClient{
|
||||
c: c,
|
||||
healthCheck: cc.HealthCheck,
|
||||
})
|
||||
}
|
||||
|
||||
// Randomize nextIdx in order to prevent initial servers'
|
||||
// hammering from a cluster of identical LBClients.
|
||||
cc.nextIdx = uint32(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (cc *LBClient) get() *lbClient {
|
||||
cc.once.Do(cc.init)
|
||||
|
||||
cs := cc.cs
|
||||
idx := atomic.AddUint32(&cc.nextIdx, 1)
|
||||
idx %= uint32(len(cs))
|
||||
|
||||
minC := cs[idx]
|
||||
minN := minC.PendingRequests()
|
||||
if minN == 0 {
|
||||
return minC
|
||||
}
|
||||
for _, c := range cs[idx+1:] {
|
||||
n := c.PendingRequests()
|
||||
if n == 0 {
|
||||
return c
|
||||
}
|
||||
if n < minN {
|
||||
minC = c
|
||||
minN = n
|
||||
}
|
||||
}
|
||||
for _, c := range cs[:idx] {
|
||||
n := c.PendingRequests()
|
||||
if n == 0 {
|
||||
return c
|
||||
}
|
||||
if n < minN {
|
||||
minC = c
|
||||
minN = n
|
||||
}
|
||||
}
|
||||
return minC
|
||||
}
|
||||
|
||||
type lbClient struct {
|
||||
c BalancingClient
|
||||
healthCheck func(req *Request, resp *Response, err error) bool
|
||||
penalty uint32
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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
|
||||
)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
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.
|
||||
type noCopy struct{}
|
||||
|
||||
func (*noCopy) Lock() {}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type perIPConnCounter struct {
|
||||
pool 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()
|
||||
if cc.m == nil {
|
||||
cc.lock.Unlock()
|
||||
panic("BUG: perIPConnCounter.Register() wasn't called")
|
||||
}
|
||||
n := cc.m[ip] - 1
|
||||
if n < 0 {
|
||||
cc.lock.Unlock()
|
||||
panic(fmt.Sprintf("BUG: negative per-ip counter=%d for ip=%d", n, ip))
|
||||
}
|
||||
cc.m[ip] = n
|
||||
cc.lock.Unlock()
|
||||
}
|
||||
|
||||
type perIPConn struct {
|
||||
net.Conn
|
||||
|
||||
ip uint32
|
||||
perIPConnCounter *perIPConnCounter
|
||||
}
|
||||
|
||||
func acquirePerIPConn(conn net.Conn, ip uint32, counter *perIPConnCounter) *perIPConn {
|
||||
v := counter.pool.Get()
|
||||
if v == nil {
|
||||
v = &perIPConn{
|
||||
perIPConnCounter: counter,
|
||||
}
|
||||
}
|
||||
c := v.(*perIPConn)
|
||||
c.Conn = conn
|
||||
c.ip = ip
|
||||
return c
|
||||
}
|
||||
|
||||
func releasePerIPConn(c *perIPConn) {
|
||||
c.Conn = nil
|
||||
c.perIPConnCounter.pool.Put(c)
|
||||
}
|
||||
|
||||
func (c *perIPConn) Close() error {
|
||||
err := c.Conn.Close()
|
||||
c.perIPConnCounter.Unregister(c.ip)
|
||||
releasePerIPConn(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
|
||||
}
|
||||
+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.
|
||||
+2004
@@ -0,0 +1,2004 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ServeConn serves HTTP requests from the given connection
|
||||
// using the given handler.
|
||||
//
|
||||
// ServeConn returns nil if all requests from the c are successfully served.
|
||||
// It returns non-nil error otherwise.
|
||||
//
|
||||
// Connection c must immediately propagate all the data passed to Write()
|
||||
// to the client. Otherwise requests' processing may hang.
|
||||
//
|
||||
// ServeConn closes c before returning.
|
||||
func ServeConn(c net.Conn, handler RequestHandler) error {
|
||||
v := serverPool.Get()
|
||||
if v == nil {
|
||||
v = &Server{}
|
||||
}
|
||||
s := v.(*Server)
|
||||
s.Handler = handler
|
||||
err := s.ServeConn(c)
|
||||
s.Handler = nil
|
||||
serverPool.Put(v)
|
||||
return err
|
||||
}
|
||||
|
||||
var serverPool sync.Pool
|
||||
|
||||
// Serve serves incoming connections from the given listener
|
||||
// using the given handler.
|
||||
//
|
||||
// Serve blocks until the given listener returns permanent error.
|
||||
func Serve(ln net.Listener, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
// ServeTLS serves HTTPS requests from the given net.Listener
|
||||
// using the given handler.
|
||||
//
|
||||
// certFile and keyFile are paths to TLS certificate and key files.
|
||||
func ServeTLS(ln net.Listener, certFile, keyFile string, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ServeTLS(ln, certFile, keyFile)
|
||||
}
|
||||
|
||||
// ServeTLSEmbed serves HTTPS requests from the given net.Listener
|
||||
// using the given handler.
|
||||
//
|
||||
// certData and keyData must contain valid TLS certificate and key data.
|
||||
func ServeTLSEmbed(ln net.Listener, certData, keyData []byte, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ServeTLSEmbed(ln, certData, keyData)
|
||||
}
|
||||
|
||||
// ListenAndServe serves HTTP requests from the given TCP addr
|
||||
// using the given handler.
|
||||
func ListenAndServe(addr string, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ListenAndServe(addr)
|
||||
}
|
||||
|
||||
// ListenAndServeUNIX serves HTTP requests from the given UNIX addr
|
||||
// using the given handler.
|
||||
//
|
||||
// The function deletes existing file at addr before starting serving.
|
||||
//
|
||||
// The server sets the given file mode for the UNIX addr.
|
||||
func ListenAndServeUNIX(addr string, mode os.FileMode, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ListenAndServeUNIX(addr, mode)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS serves HTTPS requests from the given TCP addr
|
||||
// using the given handler.
|
||||
//
|
||||
// certFile and keyFile are paths to TLS certificate and key files.
|
||||
func ListenAndServeTLS(addr, certFile, keyFile string, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ListenAndServeTLS(addr, certFile, keyFile)
|
||||
}
|
||||
|
||||
// ListenAndServeTLSEmbed serves HTTPS requests from the given TCP addr
|
||||
// using the given handler.
|
||||
//
|
||||
// certData and keyData must contain valid TLS certificate and key data.
|
||||
func ListenAndServeTLSEmbed(addr string, certData, keyData []byte, handler RequestHandler) error {
|
||||
s := &Server{
|
||||
Handler: handler,
|
||||
}
|
||||
return s.ListenAndServeTLSEmbed(addr, certData, keyData)
|
||||
}
|
||||
|
||||
// RequestHandler must process incoming requests.
|
||||
//
|
||||
// RequestHandler must call ctx.TimeoutError() before returning
|
||||
// if it keeps references to ctx and/or its' members after the return.
|
||||
// Consider wrapping RequestHandler into TimeoutHandler if response time
|
||||
// must be limited.
|
||||
type RequestHandler func(ctx *RequestCtx)
|
||||
|
||||
// Server implements HTTP server.
|
||||
//
|
||||
// Default Server settings should satisfy the majority of Server users.
|
||||
// Adjust Server settings only if you really understand the consequences.
|
||||
//
|
||||
// It is forbidden copying Server instances. Create new Server instances
|
||||
// instead.
|
||||
//
|
||||
// It is safe to call Server methods from concurrently running goroutines.
|
||||
type Server struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Handler for processing incoming requests.
|
||||
Handler RequestHandler
|
||||
|
||||
// Server name for sending in response headers.
|
||||
//
|
||||
// Default server name is used if left blank.
|
||||
Name string
|
||||
|
||||
// The maximum number of concurrent connections the server may serve.
|
||||
//
|
||||
// DefaultConcurrency is used if not set.
|
||||
Concurrency int
|
||||
|
||||
// Whether to disable keep-alive connections.
|
||||
//
|
||||
// The server will close all the incoming connections after sending
|
||||
// the first response to client if this option is set to true.
|
||||
//
|
||||
// By default keep-alive connections are enabled.
|
||||
DisableKeepalive bool
|
||||
|
||||
// Per-connection buffer size for requests' reading.
|
||||
// This also limits the maximum header size.
|
||||
//
|
||||
// Increase this buffer if your clients send multi-KB RequestURIs
|
||||
// and/or multi-KB headers (for example, BIG cookies).
|
||||
//
|
||||
// Default buffer size is used if not set.
|
||||
ReadBufferSize int
|
||||
|
||||
// Per-connection buffer size for responses' writing.
|
||||
//
|
||||
// Default buffer size is used if not set.
|
||||
WriteBufferSize int
|
||||
|
||||
// Maximum duration for reading the full request (including body).
|
||||
//
|
||||
// This also limits the maximum duration for idle keep-alive
|
||||
// connections.
|
||||
//
|
||||
// By default request read timeout is unlimited.
|
||||
ReadTimeout time.Duration
|
||||
|
||||
// Maximum duration for writing the full response (including body).
|
||||
//
|
||||
// By default response write timeout is unlimited.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// Maximum number of concurrent client connections allowed per IP.
|
||||
//
|
||||
// By default unlimited number of concurrent connections
|
||||
// may be established to the server from a single IP address.
|
||||
MaxConnsPerIP int
|
||||
|
||||
// Maximum number of requests served per connection.
|
||||
//
|
||||
// The server closes connection after the last request.
|
||||
// 'Connection: close' header is added to the last response.
|
||||
//
|
||||
// By default unlimited number of requests may be served per connection.
|
||||
MaxRequestsPerConn int
|
||||
|
||||
// Maximum keep-alive connection lifetime.
|
||||
//
|
||||
// The server closes keep-alive connection after its' lifetime
|
||||
// expiration.
|
||||
//
|
||||
// See also ReadTimeout for limiting the duration of idle keep-alive
|
||||
// connections.
|
||||
//
|
||||
// By default keep-alive connection lifetime is unlimited.
|
||||
MaxKeepaliveDuration time.Duration
|
||||
|
||||
// Maximum request body size.
|
||||
//
|
||||
// The server rejects requests with bodies exceeding this limit.
|
||||
//
|
||||
// Request body size is limited by DefaultMaxRequestBodySize by default.
|
||||
MaxRequestBodySize int
|
||||
|
||||
// Aggressively reduces memory usage at the cost of higher CPU usage
|
||||
// if set to true.
|
||||
//
|
||||
// Try enabling this option only if the server consumes too much memory
|
||||
// serving mostly idle keep-alive connections. This may reduce memory
|
||||
// usage by more than 50%.
|
||||
//
|
||||
// Aggressive memory usage reduction is disabled by default.
|
||||
ReduceMemoryUsage bool
|
||||
|
||||
// Rejects all non-GET requests if set to true.
|
||||
//
|
||||
// This option is useful as anti-DoS protection for servers
|
||||
// accepting only GET requests. The request size is limited
|
||||
// by ReadBufferSize if GetOnly is set.
|
||||
//
|
||||
// Server accepts all the requests by default.
|
||||
GetOnly bool
|
||||
|
||||
// Logs all errors, including the most frequent
|
||||
// 'connection reset by peer', 'broken pipe' and 'connection timeout'
|
||||
// errors. Such errors are common in production serving real-world
|
||||
// clients.
|
||||
//
|
||||
// By default the most frequent errors such as
|
||||
// 'connection reset by peer', 'broken pipe' and 'connection timeout'
|
||||
// are suppressed in order to limit output log traffic.
|
||||
LogAllErrors bool
|
||||
|
||||
// Header names are passed as-is without normalization
|
||||
// if this option is set.
|
||||
//
|
||||
// Disabled header names' normalization may be useful only for proxying
|
||||
// incoming requests to other servers expecting case-sensitive
|
||||
// header names. See https://github.com/valyala/fasthttp/issues/57
|
||||
// for details.
|
||||
//
|
||||
// By default request and response header names are normalized, i.e.
|
||||
// The first letter and the first letters following dashes
|
||||
// are uppercased, while all the other letters are lowercased.
|
||||
// Examples:
|
||||
//
|
||||
// * HOST -> Host
|
||||
// * content-type -> Content-Type
|
||||
// * cONTENT-lenGTH -> Content-Length
|
||||
DisableHeaderNamesNormalizing bool
|
||||
|
||||
// Logger, which is used by RequestCtx.Logger().
|
||||
//
|
||||
// By default standard logger from log package is used.
|
||||
Logger Logger
|
||||
|
||||
concurrency uint32
|
||||
concurrencyCh chan struct{}
|
||||
perIPConnCounter perIPConnCounter
|
||||
serverName atomic.Value
|
||||
|
||||
ctxPool sync.Pool
|
||||
readerPool sync.Pool
|
||||
writerPool sync.Pool
|
||||
hijackConnPool sync.Pool
|
||||
bytePool sync.Pool
|
||||
}
|
||||
|
||||
// TimeoutHandler creates RequestHandler, which returns StatusRequestTimeout
|
||||
// error with the given msg to the client if h didn't return during
|
||||
// the given duration.
|
||||
//
|
||||
// The returned handler may return StatusTooManyRequests error with the given
|
||||
// msg to the client if there are more than Server.Concurrency concurrent
|
||||
// handlers h are running at the moment.
|
||||
func TimeoutHandler(h RequestHandler, timeout time.Duration, msg string) RequestHandler {
|
||||
if timeout <= 0 {
|
||||
return h
|
||||
}
|
||||
|
||||
return func(ctx *RequestCtx) {
|
||||
concurrencyCh := ctx.s.concurrencyCh
|
||||
select {
|
||||
case concurrencyCh <- struct{}{}:
|
||||
default:
|
||||
ctx.Error(msg, StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
|
||||
ch := ctx.timeoutCh
|
||||
if ch == nil {
|
||||
ch = make(chan struct{}, 1)
|
||||
ctx.timeoutCh = ch
|
||||
}
|
||||
go func() {
|
||||
h(ctx)
|
||||
ch <- struct{}{}
|
||||
<-concurrencyCh
|
||||
}()
|
||||
ctx.timeoutTimer = initTimer(ctx.timeoutTimer, timeout)
|
||||
select {
|
||||
case <-ch:
|
||||
case <-ctx.timeoutTimer.C:
|
||||
ctx.TimeoutError(msg)
|
||||
}
|
||||
stopTimer(ctx.timeoutTimer)
|
||||
}
|
||||
}
|
||||
|
||||
// CompressHandler returns RequestHandler that transparently compresses
|
||||
// response body generated by h if the request contains 'gzip' or 'deflate'
|
||||
// 'Accept-Encoding' header.
|
||||
func CompressHandler(h RequestHandler) RequestHandler {
|
||||
return CompressHandlerLevel(h, CompressDefaultCompression)
|
||||
}
|
||||
|
||||
// CompressHandlerLevel returns RequestHandler that transparently compresses
|
||||
// response body generated by h if the request contains 'gzip' or 'deflate'
|
||||
// 'Accept-Encoding' header.
|
||||
//
|
||||
// Level is the desired compression level:
|
||||
//
|
||||
// * CompressNoCompression
|
||||
// * CompressBestSpeed
|
||||
// * CompressBestCompression
|
||||
// * CompressDefaultCompression
|
||||
// * CompressHuffmanOnly
|
||||
func CompressHandlerLevel(h RequestHandler, level int) RequestHandler {
|
||||
return func(ctx *RequestCtx) {
|
||||
h(ctx)
|
||||
ce := ctx.Response.Header.PeekBytes(strContentEncoding)
|
||||
if len(ce) > 0 {
|
||||
// Do not compress responses with non-empty
|
||||
// Content-Encoding.
|
||||
return
|
||||
}
|
||||
if ctx.Request.Header.HasAcceptEncodingBytes(strGzip) {
|
||||
ctx.Response.gzipBody(level)
|
||||
} else if ctx.Request.Header.HasAcceptEncodingBytes(strDeflate) {
|
||||
ctx.Response.deflateBody(level)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RequestCtx contains incoming request and manages outgoing response.
|
||||
//
|
||||
// It is forbidden copying RequestCtx instances.
|
||||
//
|
||||
// RequestHandler should avoid holding references to incoming RequestCtx and/or
|
||||
// its' members after the return.
|
||||
// If holding RequestCtx references after the return is unavoidable
|
||||
// (for instance, ctx is passed to a separate goroutine and ctx lifetime cannot
|
||||
// be controlled), then the RequestHandler MUST call ctx.TimeoutError()
|
||||
// before return.
|
||||
//
|
||||
// It is unsafe modifying/reading RequestCtx instance from concurrently
|
||||
// running goroutines. The only exception is TimeoutError*, which may be called
|
||||
// while other goroutines accessing RequestCtx.
|
||||
type RequestCtx struct {
|
||||
noCopy noCopy
|
||||
|
||||
// Incoming request.
|
||||
//
|
||||
// Copying Request by value is forbidden. Use pointer to Request instead.
|
||||
Request Request
|
||||
|
||||
// Outgoing response.
|
||||
//
|
||||
// Copying Response by value is forbidden. Use pointer to Response instead.
|
||||
Response Response
|
||||
|
||||
userValues userData
|
||||
|
||||
lastReadDuration time.Duration
|
||||
|
||||
connID uint64
|
||||
connRequestNum uint64
|
||||
connTime time.Time
|
||||
|
||||
time time.Time
|
||||
|
||||
logger ctxLogger
|
||||
s *Server
|
||||
c net.Conn
|
||||
fbr firstByteReader
|
||||
|
||||
timeoutResponse *Response
|
||||
timeoutCh chan struct{}
|
||||
timeoutTimer *time.Timer
|
||||
|
||||
hijackHandler HijackHandler
|
||||
}
|
||||
|
||||
// HijackHandler must process the hijacked connection c.
|
||||
//
|
||||
// The connection c is automatically closed after returning from HijackHandler.
|
||||
//
|
||||
// The connection c must not be used after returning from the handler.
|
||||
type HijackHandler func(c net.Conn)
|
||||
|
||||
// Hijack registers the given handler for connection hijacking.
|
||||
//
|
||||
// The handler is called after returning from RequestHandler
|
||||
// and sending http response. The current connection is passed
|
||||
// to the handler. The connection is automatically closed after
|
||||
// returning from the handler.
|
||||
//
|
||||
// The server skips calling the handler in the following cases:
|
||||
//
|
||||
// * 'Connection: close' header exists in either request or response.
|
||||
// * Unexpected error during response writing to the connection.
|
||||
//
|
||||
// The server stops processing requests from hijacked connections.
|
||||
// Server limits such as Concurrency, ReadTimeout, WriteTimeout, etc.
|
||||
// aren't applied to hijacked connections.
|
||||
//
|
||||
// The handler must not retain references to ctx members.
|
||||
//
|
||||
// Arbitrary 'Connection: Upgrade' protocols may be implemented
|
||||
// with HijackHandler. For instance,
|
||||
//
|
||||
// * WebSocket ( https://en.wikipedia.org/wiki/WebSocket )
|
||||
// * HTTP/2.0 ( https://en.wikipedia.org/wiki/HTTP/2 )
|
||||
//
|
||||
func (ctx *RequestCtx) Hijack(handler HijackHandler) {
|
||||
ctx.hijackHandler = handler
|
||||
}
|
||||
|
||||
// Hijacked returns true after Hijack is called.
|
||||
func (ctx *RequestCtx) Hijacked() bool {
|
||||
return ctx.hijackHandler != nil
|
||||
}
|
||||
|
||||
// SetUserValue stores the given value (arbitrary object)
|
||||
// under the given key in ctx.
|
||||
//
|
||||
// The value stored in ctx may be obtained by UserValue*.
|
||||
//
|
||||
// This functionality may be useful for passing arbitrary values between
|
||||
// functions involved in request processing.
|
||||
//
|
||||
// All the values are removed from ctx after returning from the top
|
||||
// RequestHandler. Additionally, Close method is called on each value
|
||||
// implementing io.Closer before removing the value from ctx.
|
||||
func (ctx *RequestCtx) SetUserValue(key string, value interface{}) {
|
||||
ctx.userValues.Set(key, value)
|
||||
}
|
||||
|
||||
// SetUserValueBytes stores the given value (arbitrary object)
|
||||
// under the given key in ctx.
|
||||
//
|
||||
// The value stored in ctx may be obtained by UserValue*.
|
||||
//
|
||||
// This functionality may be useful for passing arbitrary values between
|
||||
// functions involved in request processing.
|
||||
//
|
||||
// All the values stored in ctx are deleted after returning from RequestHandler.
|
||||
func (ctx *RequestCtx) SetUserValueBytes(key []byte, value interface{}) {
|
||||
ctx.userValues.SetBytes(key, value)
|
||||
}
|
||||
|
||||
// UserValue returns the value stored via SetUserValue* under the given key.
|
||||
func (ctx *RequestCtx) UserValue(key string) interface{} {
|
||||
return ctx.userValues.Get(key)
|
||||
}
|
||||
|
||||
// UserValueBytes returns the value stored via SetUserValue*
|
||||
// under the given key.
|
||||
func (ctx *RequestCtx) UserValueBytes(key []byte) interface{} {
|
||||
return ctx.userValues.GetBytes(key)
|
||||
}
|
||||
|
||||
// VisitUserValues calls visitor for each existing userValue.
|
||||
//
|
||||
// visitor must not retain references to key and value after returning.
|
||||
// Make key and/or value copies if you need storing them after returning.
|
||||
func (ctx *RequestCtx) VisitUserValues(visitor func([]byte, interface{})) {
|
||||
for i, n := 0, len(ctx.userValues); i < n; i++ {
|
||||
kv := &ctx.userValues[i]
|
||||
visitor(kv.key, kv.value)
|
||||
}
|
||||
}
|
||||
|
||||
type connTLSer interface {
|
||||
ConnectionState() tls.ConnectionState
|
||||
}
|
||||
|
||||
// IsTLS returns true if the underlying connection is tls.Conn.
|
||||
//
|
||||
// tls.Conn is an encrypted connection (aka SSL, HTTPS).
|
||||
func (ctx *RequestCtx) IsTLS() bool {
|
||||
// cast to (connTLSer) instead of (*tls.Conn), since it catches
|
||||
// cases with overridden tls.Conn such as:
|
||||
//
|
||||
// type customConn struct {
|
||||
// *tls.Conn
|
||||
//
|
||||
// // other custom fields here
|
||||
// }
|
||||
_, ok := ctx.c.(connTLSer)
|
||||
return ok
|
||||
}
|
||||
|
||||
// TLSConnectionState returns TLS connection state.
|
||||
//
|
||||
// The function returns nil if the underlying connection isn't tls.Conn.
|
||||
//
|
||||
// The returned state may be used for verifying TLS version, client certificates,
|
||||
// etc.
|
||||
func (ctx *RequestCtx) TLSConnectionState() *tls.ConnectionState {
|
||||
tlsConn, ok := ctx.c.(connTLSer)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
state := tlsConn.ConnectionState()
|
||||
return &state
|
||||
}
|
||||
|
||||
type firstByteReader struct {
|
||||
c net.Conn
|
||||
ch byte
|
||||
byteRead bool
|
||||
}
|
||||
|
||||
func (r *firstByteReader) Read(b []byte) (int, error) {
|
||||
if len(b) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
nn := 0
|
||||
if !r.byteRead {
|
||||
b[0] = r.ch
|
||||
b = b[1:]
|
||||
r.byteRead = true
|
||||
nn = 1
|
||||
}
|
||||
n, err := r.c.Read(b)
|
||||
return n + nn, err
|
||||
}
|
||||
|
||||
// Logger is used for logging formatted messages.
|
||||
type Logger interface {
|
||||
// Printf must have the same semantics as log.Printf.
|
||||
Printf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
var ctxLoggerLock sync.Mutex
|
||||
|
||||
type ctxLogger struct {
|
||||
ctx *RequestCtx
|
||||
logger Logger
|
||||
}
|
||||
|
||||
func (cl *ctxLogger) Printf(format string, args ...interface{}) {
|
||||
ctxLoggerLock.Lock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
ctx := cl.ctx
|
||||
cl.logger.Printf("%.3f %s - %s", time.Since(ctx.Time()).Seconds(), ctx.String(), msg)
|
||||
ctxLoggerLock.Unlock()
|
||||
}
|
||||
|
||||
var zeroTCPAddr = &net.TCPAddr{
|
||||
IP: net.IPv4zero,
|
||||
}
|
||||
|
||||
// String returns unique string representation of the ctx.
|
||||
//
|
||||
// The returned value may be useful for logging.
|
||||
func (ctx *RequestCtx) String() string {
|
||||
return fmt.Sprintf("#%016X - %s<->%s - %s %s", ctx.ID(), ctx.LocalAddr(), ctx.RemoteAddr(), ctx.Request.Header.Method(), ctx.URI().FullURI())
|
||||
}
|
||||
|
||||
// ID returns unique ID of the request.
|
||||
func (ctx *RequestCtx) ID() uint64 {
|
||||
return (ctx.connID << 32) | ctx.connRequestNum
|
||||
}
|
||||
|
||||
// ConnID returns unique connection ID.
|
||||
//
|
||||
// This ID may be used to match distinct requests to the same incoming
|
||||
// connection.
|
||||
func (ctx *RequestCtx) ConnID() uint64 {
|
||||
return ctx.connID
|
||||
}
|
||||
|
||||
// Time returns RequestHandler call time truncated to the nearest second.
|
||||
//
|
||||
// Call time.Now() at the beginning of RequestHandler in order to obtain
|
||||
// percise RequestHandler call time.
|
||||
func (ctx *RequestCtx) Time() time.Time {
|
||||
return ctx.time
|
||||
}
|
||||
|
||||
// ConnTime returns the time server starts serving the connection
|
||||
// the current request came from.
|
||||
//
|
||||
// The returned time is truncated to the nearest second.
|
||||
func (ctx *RequestCtx) ConnTime() time.Time {
|
||||
return ctx.connTime
|
||||
}
|
||||
|
||||
// ConnRequestNum returns request sequence number
|
||||
// for the current connection.
|
||||
//
|
||||
// Sequence starts with 1.
|
||||
func (ctx *RequestCtx) ConnRequestNum() uint64 {
|
||||
return ctx.connRequestNum
|
||||
}
|
||||
|
||||
// SetConnectionClose sets 'Connection: close' response header and closes
|
||||
// connection after the RequestHandler returns.
|
||||
func (ctx *RequestCtx) SetConnectionClose() {
|
||||
ctx.Response.SetConnectionClose()
|
||||
}
|
||||
|
||||
// SetStatusCode sets response status code.
|
||||
func (ctx *RequestCtx) SetStatusCode(statusCode int) {
|
||||
ctx.Response.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
// SetContentType sets response Content-Type.
|
||||
func (ctx *RequestCtx) SetContentType(contentType string) {
|
||||
ctx.Response.Header.SetContentType(contentType)
|
||||
}
|
||||
|
||||
// SetContentTypeBytes sets response Content-Type.
|
||||
//
|
||||
// It is safe modifying contentType buffer after function return.
|
||||
func (ctx *RequestCtx) SetContentTypeBytes(contentType []byte) {
|
||||
ctx.Response.Header.SetContentTypeBytes(contentType)
|
||||
}
|
||||
|
||||
// RequestURI returns RequestURI.
|
||||
//
|
||||
// This uri is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) RequestURI() []byte {
|
||||
return ctx.Request.Header.RequestURI()
|
||||
}
|
||||
|
||||
// URI returns requested uri.
|
||||
//
|
||||
// The uri is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) URI() *URI {
|
||||
return ctx.Request.URI()
|
||||
}
|
||||
|
||||
// Referer returns request referer.
|
||||
//
|
||||
// The referer is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) Referer() []byte {
|
||||
return ctx.Request.Header.Referer()
|
||||
}
|
||||
|
||||
// UserAgent returns User-Agent header value from the request.
|
||||
func (ctx *RequestCtx) UserAgent() []byte {
|
||||
return ctx.Request.Header.UserAgent()
|
||||
}
|
||||
|
||||
// Path returns requested path.
|
||||
//
|
||||
// The path is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) Path() []byte {
|
||||
return ctx.URI().Path()
|
||||
}
|
||||
|
||||
// Host returns requested host.
|
||||
//
|
||||
// The host is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) Host() []byte {
|
||||
return ctx.URI().Host()
|
||||
}
|
||||
|
||||
// QueryArgs returns query arguments from RequestURI.
|
||||
//
|
||||
// It doesn't return POST'ed arguments - use PostArgs() for this.
|
||||
//
|
||||
// Returned arguments are valid until returning from RequestHandler.
|
||||
//
|
||||
// See also PostArgs, FormValue and FormFile.
|
||||
func (ctx *RequestCtx) QueryArgs() *Args {
|
||||
return ctx.URI().QueryArgs()
|
||||
}
|
||||
|
||||
// PostArgs returns POST arguments.
|
||||
//
|
||||
// It doesn't return query arguments from RequestURI - use QueryArgs for this.
|
||||
//
|
||||
// Returned arguments are valid until returning from RequestHandler.
|
||||
//
|
||||
// See also QueryArgs, FormValue and FormFile.
|
||||
func (ctx *RequestCtx) PostArgs() *Args {
|
||||
return ctx.Request.PostArgs()
|
||||
}
|
||||
|
||||
// MultipartForm returns requests's multipart form.
|
||||
//
|
||||
// Returns ErrNoMultipartForm if request's content-type
|
||||
// isn't 'multipart/form-data'.
|
||||
//
|
||||
// All uploaded temporary files are automatically deleted after
|
||||
// returning from RequestHandler. Either move or copy uploaded files
|
||||
// into new place if you want retaining them.
|
||||
//
|
||||
// Use SaveMultipartFile function for permanently saving uploaded file.
|
||||
//
|
||||
// The returned form is valid until returning from RequestHandler.
|
||||
//
|
||||
// See also FormFile and FormValue.
|
||||
func (ctx *RequestCtx) MultipartForm() (*multipart.Form, error) {
|
||||
return ctx.Request.MultipartForm()
|
||||
}
|
||||
|
||||
// FormFile returns uploaded file associated with the given multipart form key.
|
||||
//
|
||||
// The file is automatically deleted after returning from RequestHandler,
|
||||
// so either move or copy uploaded file into new place if you want retaining it.
|
||||
//
|
||||
// Use SaveMultipartFile function for permanently saving uploaded file.
|
||||
//
|
||||
// The returned file header is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) FormFile(key string) (*multipart.FileHeader, error) {
|
||||
mf, err := ctx.MultipartForm()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mf.File == nil {
|
||||
return nil, err
|
||||
}
|
||||
fhh := mf.File[key]
|
||||
if fhh == nil {
|
||||
return nil, ErrMissingFile
|
||||
}
|
||||
return fhh[0], nil
|
||||
}
|
||||
|
||||
// ErrMissingFile may be returned from FormFile when the is no uploaded file
|
||||
// associated with the given multipart form key.
|
||||
var ErrMissingFile = errors.New("there is no uploaded file associated with the given key")
|
||||
|
||||
// SaveMultipartFile saves multipart file fh under the given filename path.
|
||||
func SaveMultipartFile(fh *multipart.FileHeader, path string) error {
|
||||
f, err := fh.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if ff, ok := f.(*os.File); ok {
|
||||
return os.Rename(ff.Name(), path)
|
||||
}
|
||||
|
||||
ff, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ff.Close()
|
||||
_, err = copyZeroAlloc(ff, f)
|
||||
return err
|
||||
}
|
||||
|
||||
// FormValue returns form value associated with the given key.
|
||||
//
|
||||
// The value is searched in the following places:
|
||||
//
|
||||
// * Query string.
|
||||
// * POST or PUT body.
|
||||
//
|
||||
// There are more fine-grained methods for obtaining form values:
|
||||
//
|
||||
// * QueryArgs for obtaining values from query string.
|
||||
// * PostArgs for obtaining values from POST or PUT body.
|
||||
// * MultipartForm for obtaining values from multipart form.
|
||||
// * FormFile for obtaining uploaded files.
|
||||
//
|
||||
// The returned value is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) FormValue(key string) []byte {
|
||||
v := ctx.QueryArgs().Peek(key)
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
v = ctx.PostArgs().Peek(key)
|
||||
if len(v) > 0 {
|
||||
return v
|
||||
}
|
||||
mf, err := ctx.MultipartForm()
|
||||
if err == nil && mf.Value != nil {
|
||||
vv := mf.Value[key]
|
||||
if len(vv) > 0 {
|
||||
return []byte(vv[0])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsGet returns true if request method is GET.
|
||||
func (ctx *RequestCtx) IsGet() bool {
|
||||
return ctx.Request.Header.IsGet()
|
||||
}
|
||||
|
||||
// IsPost returns true if request method is POST.
|
||||
func (ctx *RequestCtx) IsPost() bool {
|
||||
return ctx.Request.Header.IsPost()
|
||||
}
|
||||
|
||||
// IsPut returns true if request method is PUT.
|
||||
func (ctx *RequestCtx) IsPut() bool {
|
||||
return ctx.Request.Header.IsPut()
|
||||
}
|
||||
|
||||
// IsDelete returns true if request method is DELETE.
|
||||
func (ctx *RequestCtx) IsDelete() bool {
|
||||
return ctx.Request.Header.IsDelete()
|
||||
}
|
||||
|
||||
// Method return request method.
|
||||
//
|
||||
// Returned value is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) Method() []byte {
|
||||
return ctx.Request.Header.Method()
|
||||
}
|
||||
|
||||
// IsHead returns true if request method is HEAD.
|
||||
func (ctx *RequestCtx) IsHead() bool {
|
||||
return ctx.Request.Header.IsHead()
|
||||
}
|
||||
|
||||
// RemoteAddr returns client address for the given request.
|
||||
//
|
||||
// Always returns non-nil result.
|
||||
func (ctx *RequestCtx) RemoteAddr() net.Addr {
|
||||
if ctx.c == nil {
|
||||
return zeroTCPAddr
|
||||
}
|
||||
addr := ctx.c.RemoteAddr()
|
||||
if addr == nil {
|
||||
return zeroTCPAddr
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// LocalAddr returns server address for the given request.
|
||||
//
|
||||
// Always returns non-nil result.
|
||||
func (ctx *RequestCtx) LocalAddr() net.Addr {
|
||||
if ctx.c == nil {
|
||||
return zeroTCPAddr
|
||||
}
|
||||
addr := ctx.c.LocalAddr()
|
||||
if addr == nil {
|
||||
return zeroTCPAddr
|
||||
}
|
||||
return addr
|
||||
}
|
||||
|
||||
// RemoteIP returns the client ip the request came from.
|
||||
//
|
||||
// Always returns non-nil result.
|
||||
func (ctx *RequestCtx) RemoteIP() net.IP {
|
||||
return addrToIP(ctx.RemoteAddr())
|
||||
}
|
||||
|
||||
// LocalIP returns the server ip the request came to.
|
||||
//
|
||||
// Always returns non-nil result.
|
||||
func (ctx *RequestCtx) LocalIP() net.IP {
|
||||
return addrToIP(ctx.LocalAddr())
|
||||
}
|
||||
|
||||
func addrToIP(addr net.Addr) net.IP {
|
||||
x, ok := addr.(*net.TCPAddr)
|
||||
if !ok {
|
||||
return net.IPv4zero
|
||||
}
|
||||
return x.IP
|
||||
}
|
||||
|
||||
// Error sets response status code to the given value and sets response body
|
||||
// to the given message.
|
||||
func (ctx *RequestCtx) Error(msg string, statusCode int) {
|
||||
ctx.Response.Reset()
|
||||
ctx.SetStatusCode(statusCode)
|
||||
ctx.SetContentTypeBytes(defaultContentType)
|
||||
ctx.SetBodyString(msg)
|
||||
}
|
||||
|
||||
// Success sets response Content-Type and body to the given values.
|
||||
func (ctx *RequestCtx) Success(contentType string, body []byte) {
|
||||
ctx.SetContentType(contentType)
|
||||
ctx.SetBody(body)
|
||||
}
|
||||
|
||||
// SuccessString sets response Content-Type and body to the given values.
|
||||
func (ctx *RequestCtx) SuccessString(contentType, body string) {
|
||||
ctx.SetContentType(contentType)
|
||||
ctx.SetBodyString(body)
|
||||
}
|
||||
|
||||
// Redirect sets 'Location: uri' response header and sets the given statusCode.
|
||||
//
|
||||
// statusCode must have one of the following values:
|
||||
//
|
||||
// * StatusMovedPermanently (301)
|
||||
// * StatusFound (302)
|
||||
// * StatusSeeOther (303)
|
||||
// * StatusTemporaryRedirect (307)
|
||||
//
|
||||
// All other statusCode values are replaced by StatusFound (302).
|
||||
//
|
||||
// The redirect uri may be either absolute or relative to the current
|
||||
// request uri.
|
||||
func (ctx *RequestCtx) Redirect(uri string, statusCode int) {
|
||||
u := AcquireURI()
|
||||
ctx.URI().CopyTo(u)
|
||||
u.Update(uri)
|
||||
ctx.redirect(u.FullURI(), statusCode)
|
||||
ReleaseURI(u)
|
||||
}
|
||||
|
||||
// RedirectBytes sets 'Location: uri' response header and sets
|
||||
// the given statusCode.
|
||||
//
|
||||
// statusCode must have one of the following values:
|
||||
//
|
||||
// * StatusMovedPermanently (301)
|
||||
// * StatusFound (302)
|
||||
// * StatusSeeOther (303)
|
||||
// * StatusTemporaryRedirect (307)
|
||||
//
|
||||
// All other statusCode values are replaced by StatusFound (302).
|
||||
//
|
||||
// The redirect uri may be either absolute or relative to the current
|
||||
// request uri.
|
||||
func (ctx *RequestCtx) RedirectBytes(uri []byte, statusCode int) {
|
||||
s := b2s(uri)
|
||||
ctx.Redirect(s, statusCode)
|
||||
}
|
||||
|
||||
func (ctx *RequestCtx) redirect(uri []byte, statusCode int) {
|
||||
ctx.Response.Header.SetCanonical(strLocation, uri)
|
||||
statusCode = getRedirectStatusCode(statusCode)
|
||||
ctx.Response.SetStatusCode(statusCode)
|
||||
}
|
||||
|
||||
func getRedirectStatusCode(statusCode int) int {
|
||||
if statusCode == StatusMovedPermanently || statusCode == StatusFound ||
|
||||
statusCode == StatusSeeOther || statusCode == StatusTemporaryRedirect {
|
||||
return statusCode
|
||||
}
|
||||
return StatusFound
|
||||
}
|
||||
|
||||
// SetBody sets response body to the given value.
|
||||
//
|
||||
// It is safe re-using body argument after the function returns.
|
||||
func (ctx *RequestCtx) SetBody(body []byte) {
|
||||
ctx.Response.SetBody(body)
|
||||
}
|
||||
|
||||
// SetBodyString sets response body to the given value.
|
||||
func (ctx *RequestCtx) SetBodyString(body string) {
|
||||
ctx.Response.SetBodyString(body)
|
||||
}
|
||||
|
||||
// ResetBody resets response body contents.
|
||||
func (ctx *RequestCtx) ResetBody() {
|
||||
ctx.Response.ResetBody()
|
||||
}
|
||||
|
||||
// SendFile sends local file contents from the given path as response body.
|
||||
//
|
||||
// This is a shortcut to ServeFile(ctx, path).
|
||||
//
|
||||
// SendFile logs all the errors via ctx.Logger.
|
||||
//
|
||||
// See also ServeFile, FSHandler and FS.
|
||||
func (ctx *RequestCtx) SendFile(path string) {
|
||||
ServeFile(ctx, path)
|
||||
}
|
||||
|
||||
// SendFileBytes sends local file contents from the given path as response body.
|
||||
//
|
||||
// This is a shortcut to ServeFileBytes(ctx, path).
|
||||
//
|
||||
// SendFileBytes logs all the errors via ctx.Logger.
|
||||
//
|
||||
// See also ServeFileBytes, FSHandler and FS.
|
||||
func (ctx *RequestCtx) SendFileBytes(path []byte) {
|
||||
ServeFileBytes(ctx, path)
|
||||
}
|
||||
|
||||
// IfModifiedSince returns true if lastModified exceeds 'If-Modified-Since'
|
||||
// value from the request header.
|
||||
//
|
||||
// The function returns true also 'If-Modified-Since' request header is missing.
|
||||
func (ctx *RequestCtx) IfModifiedSince(lastModified time.Time) bool {
|
||||
ifModStr := ctx.Request.Header.peek(strIfModifiedSince)
|
||||
if len(ifModStr) == 0 {
|
||||
return true
|
||||
}
|
||||
ifMod, err := ParseHTTPDate(ifModStr)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
lastModified = lastModified.Truncate(time.Second)
|
||||
return ifMod.Before(lastModified)
|
||||
}
|
||||
|
||||
// NotModified resets response and sets '304 Not Modified' response status code.
|
||||
func (ctx *RequestCtx) NotModified() {
|
||||
ctx.Response.Reset()
|
||||
ctx.SetStatusCode(StatusNotModified)
|
||||
}
|
||||
|
||||
// NotFound resets response and sets '404 Not Found' response status code.
|
||||
func (ctx *RequestCtx) NotFound() {
|
||||
ctx.Response.Reset()
|
||||
ctx.SetStatusCode(StatusNotFound)
|
||||
ctx.SetBodyString("404 Page not found")
|
||||
}
|
||||
|
||||
// Write writes p into response body.
|
||||
func (ctx *RequestCtx) Write(p []byte) (int, error) {
|
||||
ctx.Response.AppendBody(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// WriteString appends s to response body.
|
||||
func (ctx *RequestCtx) WriteString(s string) (int, error) {
|
||||
ctx.Response.AppendBodyString(s)
|
||||
return len(s), nil
|
||||
}
|
||||
|
||||
// PostBody returns POST request body.
|
||||
//
|
||||
// The returned value is valid until RequestHandler return.
|
||||
func (ctx *RequestCtx) PostBody() []byte {
|
||||
return ctx.Request.Body()
|
||||
}
|
||||
|
||||
// SetBodyStream sets response body stream and, optionally body size.
|
||||
//
|
||||
// bodyStream.Close() is called after finishing reading all body data
|
||||
// if it implements io.Closer.
|
||||
//
|
||||
// If bodySize is >= 0, then bodySize bytes must be provided by bodyStream
|
||||
// before returning io.EOF.
|
||||
//
|
||||
// If bodySize < 0, then bodyStream is read until io.EOF.
|
||||
//
|
||||
// See also SetBodyStreamWriter.
|
||||
func (ctx *RequestCtx) SetBodyStream(bodyStream io.Reader, bodySize int) {
|
||||
ctx.Response.SetBodyStream(bodyStream, bodySize)
|
||||
}
|
||||
|
||||
// SetBodyStreamWriter registers the given stream writer for populating
|
||||
// response body.
|
||||
//
|
||||
// Access to RequestCtx and/or its' members is forbidden from sw.
|
||||
//
|
||||
// This function may be used in the following cases:
|
||||
//
|
||||
// * if response body is too big (more than 10MB).
|
||||
// * if response body is streamed from slow external sources.
|
||||
// * if response body must be streamed to the client in chunks.
|
||||
// (aka `http server push`).
|
||||
func (ctx *RequestCtx) SetBodyStreamWriter(sw StreamWriter) {
|
||||
ctx.Response.SetBodyStreamWriter(sw)
|
||||
}
|
||||
|
||||
// IsBodyStream returns true if response body is set via SetBodyStream*.
|
||||
func (ctx *RequestCtx) IsBodyStream() bool {
|
||||
return ctx.Response.IsBodyStream()
|
||||
}
|
||||
|
||||
// Logger returns logger, which may be used for logging arbitrary
|
||||
// request-specific messages inside RequestHandler.
|
||||
//
|
||||
// Each message logged via returned logger contains request-specific information
|
||||
// such as request id, request duration, local address, remote address,
|
||||
// request method and request url.
|
||||
//
|
||||
// It is safe re-using returned logger for logging multiple messages
|
||||
// for the current request.
|
||||
//
|
||||
// The returned logger is valid until returning from RequestHandler.
|
||||
func (ctx *RequestCtx) Logger() Logger {
|
||||
if ctx.logger.ctx == nil {
|
||||
ctx.logger.ctx = ctx
|
||||
}
|
||||
if ctx.logger.logger == nil {
|
||||
ctx.logger.logger = ctx.s.logger()
|
||||
}
|
||||
return &ctx.logger
|
||||
}
|
||||
|
||||
// TimeoutError sets response status code to StatusRequestTimeout and sets
|
||||
// body to the given msg.
|
||||
//
|
||||
// All response modifications after TimeoutError call are ignored.
|
||||
//
|
||||
// TimeoutError MUST be called before returning from RequestHandler if there are
|
||||
// references to ctx and/or its members in other goroutines remain.
|
||||
//
|
||||
// Usage of this function is discouraged. Prefer eliminating ctx references
|
||||
// from pending goroutines instead of using this function.
|
||||
func (ctx *RequestCtx) TimeoutError(msg string) {
|
||||
ctx.TimeoutErrorWithCode(msg, StatusRequestTimeout)
|
||||
}
|
||||
|
||||
// TimeoutErrorWithCode sets response body to msg and response status
|
||||
// code to statusCode.
|
||||
//
|
||||
// All response modifications after TimeoutErrorWithCode call are ignored.
|
||||
//
|
||||
// TimeoutErrorWithCode MUST be called before returning from RequestHandler
|
||||
// if there are references to ctx and/or its members in other goroutines remain.
|
||||
//
|
||||
// Usage of this function is discouraged. Prefer eliminating ctx references
|
||||
// from pending goroutines instead of using this function.
|
||||
func (ctx *RequestCtx) TimeoutErrorWithCode(msg string, statusCode int) {
|
||||
var resp Response
|
||||
resp.SetStatusCode(statusCode)
|
||||
resp.SetBodyString(msg)
|
||||
ctx.TimeoutErrorWithResponse(&resp)
|
||||
}
|
||||
|
||||
// TimeoutErrorWithResponse marks the ctx as timed out and sends the given
|
||||
// response to the client.
|
||||
//
|
||||
// All ctx modifications after TimeoutErrorWithResponse call are ignored.
|
||||
//
|
||||
// TimeoutErrorWithResponse MUST be called before returning from RequestHandler
|
||||
// if there are references to ctx and/or its members in other goroutines remain.
|
||||
//
|
||||
// Usage of this function is discouraged. Prefer eliminating ctx references
|
||||
// from pending goroutines instead of using this function.
|
||||
func (ctx *RequestCtx) TimeoutErrorWithResponse(resp *Response) {
|
||||
respCopy := &Response{}
|
||||
resp.CopyTo(respCopy)
|
||||
ctx.timeoutResponse = respCopy
|
||||
}
|
||||
|
||||
// ListenAndServe serves HTTP requests from the given TCP4 addr.
|
||||
//
|
||||
// Pass custom listener to Serve if you need listening on non-TCP4 media
|
||||
// such as IPv6.
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
ln, err := net.Listen("tcp4", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
// ListenAndServeUNIX serves HTTP requests from the given UNIX addr.
|
||||
//
|
||||
// The function deletes existing file at addr before starting serving.
|
||||
//
|
||||
// The server sets the given file mode for the UNIX addr.
|
||||
func (s *Server) ListenAndServeUNIX(addr string, mode os.FileMode) error {
|
||||
if err := os.Remove(addr); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("unexpected error when trying to remove unix socket file %q: %s", addr, err)
|
||||
}
|
||||
ln, err := net.Listen("unix", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Chmod(addr, mode); err != nil {
|
||||
return fmt.Errorf("cannot chmod %#o for %q: %s", mode, addr, err)
|
||||
}
|
||||
return s.Serve(ln)
|
||||
}
|
||||
|
||||
// ListenAndServeTLS serves HTTPS requests from the given TCP4 addr.
|
||||
//
|
||||
// certFile and keyFile are paths to TLS certificate and key files.
|
||||
//
|
||||
// Pass custom listener to Serve if you need listening on non-TCP4 media
|
||||
// such as IPv6.
|
||||
func (s *Server) ListenAndServeTLS(addr, certFile, keyFile string) error {
|
||||
ln, err := net.Listen("tcp4", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ServeTLS(ln, certFile, keyFile)
|
||||
}
|
||||
|
||||
// ListenAndServeTLSEmbed serves HTTPS requests from the given TCP4 addr.
|
||||
//
|
||||
// certData and keyData must contain valid TLS certificate and key data.
|
||||
//
|
||||
// Pass custom listener to Serve if you need listening on arbitrary media
|
||||
// such as IPv6.
|
||||
func (s *Server) ListenAndServeTLSEmbed(addr string, certData, keyData []byte) error {
|
||||
ln, err := net.Listen("tcp4", addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.ServeTLSEmbed(ln, certData, keyData)
|
||||
}
|
||||
|
||||
// ServeTLS serves HTTPS requests from the given listener.
|
||||
//
|
||||
// certFile and keyFile are paths to TLS certificate and key files.
|
||||
func (s *Server) ServeTLS(ln net.Listener, certFile, keyFile string) error {
|
||||
lnTLS, err := newTLSListener(ln, certFile, keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(lnTLS)
|
||||
}
|
||||
|
||||
// ServeTLSEmbed serves HTTPS requests from the given listener.
|
||||
//
|
||||
// certData and keyData must contain valid TLS certificate and key data.
|
||||
func (s *Server) ServeTLSEmbed(ln net.Listener, certData, keyData []byte) error {
|
||||
lnTLS, err := newTLSListenerEmbed(ln, certData, keyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Serve(lnTLS)
|
||||
}
|
||||
|
||||
func newTLSListener(ln net.Listener, certFile, keyFile string) (net.Listener, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load TLS key pair from certFile=%q and keyFile=%q: %s", certFile, keyFile, err)
|
||||
}
|
||||
return newCertListener(ln, &cert), nil
|
||||
}
|
||||
|
||||
func newTLSListenerEmbed(ln net.Listener, certData, keyData []byte) (net.Listener, error) {
|
||||
cert, err := tls.X509KeyPair(certData, keyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot load TLS key pair from the provided certData(%d) and keyData(%d): %s",
|
||||
len(certData), len(keyData), err)
|
||||
}
|
||||
return newCertListener(ln, &cert), nil
|
||||
}
|
||||
|
||||
func newCertListener(ln net.Listener, cert *tls.Certificate) net.Listener {
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{*cert},
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
return tls.NewListener(ln, tlsConfig)
|
||||
}
|
||||
|
||||
// DefaultConcurrency is the maximum number of concurrent connections
|
||||
// the Server may serve by default (i.e. if Server.Concurrency isn't set).
|
||||
const DefaultConcurrency = 256 * 1024
|
||||
|
||||
// Serve serves incoming connections from the given listener.
|
||||
//
|
||||
// Serve blocks until the given listener returns permanent error.
|
||||
func (s *Server) Serve(ln net.Listener) error {
|
||||
var lastOverflowErrorTime time.Time
|
||||
var lastPerIPErrorTime time.Time
|
||||
var c net.Conn
|
||||
var err error
|
||||
|
||||
maxWorkersCount := s.getConcurrency()
|
||||
s.concurrencyCh = make(chan struct{}, maxWorkersCount)
|
||||
wp := &workerPool{
|
||||
WorkerFunc: s.serveConn,
|
||||
MaxWorkersCount: maxWorkersCount,
|
||||
LogAllErrors: s.LogAllErrors,
|
||||
Logger: s.logger(),
|
||||
}
|
||||
wp.Start()
|
||||
|
||||
for {
|
||||
if c, err = acceptConn(s, ln, &lastPerIPErrorTime); err != nil {
|
||||
wp.Stop()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !wp.Serve(c) {
|
||||
s.writeFastError(c, StatusServiceUnavailable,
|
||||
"The connection cannot be served because Server.Concurrency limit exceeded")
|
||||
c.Close()
|
||||
if time.Since(lastOverflowErrorTime) > time.Minute {
|
||||
s.logger().Printf("The incoming connection cannot be served, because %d concurrent connections are served. "+
|
||||
"Try increasing Server.Concurrency", maxWorkersCount)
|
||||
lastOverflowErrorTime = CoarseTimeNow()
|
||||
}
|
||||
|
||||
// The current server reached concurrency limit,
|
||||
// so give other concurrently running servers a chance
|
||||
// accepting incoming connections on the same address.
|
||||
//
|
||||
// There is a hope other servers didn't reach their
|
||||
// concurrency limits yet :)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
c = nil
|
||||
}
|
||||
}
|
||||
|
||||
func acceptConn(s *Server, ln net.Listener, lastPerIPErrorTime *time.Time) (net.Conn, error) {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
if c != nil {
|
||||
panic("BUG: net.Listener returned non-nil conn and non-nil error")
|
||||
}
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Temporary() {
|
||||
s.logger().Printf("Temporary error when accepting new connections: %s", netErr)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if err != io.EOF && !strings.Contains(err.Error(), "use of closed network connection") {
|
||||
s.logger().Printf("Permanent error when accepting new connections: %s", err)
|
||||
return nil, err
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
if c == nil {
|
||||
panic("BUG: net.Listener returned (nil, nil)")
|
||||
}
|
||||
if s.MaxConnsPerIP > 0 {
|
||||
pic := wrapPerIPConn(s, c)
|
||||
if pic == nil {
|
||||
if time.Since(*lastPerIPErrorTime) > time.Minute {
|
||||
s.logger().Printf("The number of connections from %s exceeds MaxConnsPerIP=%d",
|
||||
getConnIP4(c), s.MaxConnsPerIP)
|
||||
*lastPerIPErrorTime = CoarseTimeNow()
|
||||
}
|
||||
continue
|
||||
}
|
||||
c = pic
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
|
||||
func wrapPerIPConn(s *Server, c net.Conn) net.Conn {
|
||||
ip := getUint32IP(c)
|
||||
if ip == 0 {
|
||||
return c
|
||||
}
|
||||
n := s.perIPConnCounter.Register(ip)
|
||||
if n > s.MaxConnsPerIP {
|
||||
s.perIPConnCounter.Unregister(ip)
|
||||
s.writeFastError(c, StatusTooManyRequests, "The number of connections from your ip exceeds MaxConnsPerIP")
|
||||
c.Close()
|
||||
return nil
|
||||
}
|
||||
return acquirePerIPConn(c, ip, &s.perIPConnCounter)
|
||||
}
|
||||
|
||||
var defaultLogger = Logger(log.New(os.Stderr, "", log.LstdFlags))
|
||||
|
||||
func (s *Server) logger() Logger {
|
||||
if s.Logger != nil {
|
||||
return s.Logger
|
||||
}
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrPerIPConnLimit may be returned from ServeConn if the number of connections
|
||||
// per ip exceeds Server.MaxConnsPerIP.
|
||||
ErrPerIPConnLimit = errors.New("too many connections per ip")
|
||||
|
||||
// ErrConcurrencyLimit may be returned from ServeConn if the number
|
||||
// of concurrenty served connections exceeds Server.Concurrency.
|
||||
ErrConcurrencyLimit = errors.New("canot serve the connection because Server.Concurrency concurrent connections are served")
|
||||
|
||||
// ErrKeepaliveTimeout is returned from ServeConn
|
||||
// if the connection lifetime exceeds MaxKeepaliveDuration.
|
||||
ErrKeepaliveTimeout = errors.New("exceeded MaxKeepaliveDuration")
|
||||
)
|
||||
|
||||
// ServeConn serves HTTP requests from the given connection.
|
||||
//
|
||||
// ServeConn returns nil if all requests from the c are successfully served.
|
||||
// It returns non-nil error otherwise.
|
||||
//
|
||||
// Connection c must immediately propagate all the data passed to Write()
|
||||
// to the client. Otherwise requests' processing may hang.
|
||||
//
|
||||
// ServeConn closes c before returning.
|
||||
func (s *Server) ServeConn(c net.Conn) error {
|
||||
if s.MaxConnsPerIP > 0 {
|
||||
pic := wrapPerIPConn(s, c)
|
||||
if pic == nil {
|
||||
return ErrPerIPConnLimit
|
||||
}
|
||||
c = pic
|
||||
}
|
||||
|
||||
n := atomic.AddUint32(&s.concurrency, 1)
|
||||
if n > uint32(s.getConcurrency()) {
|
||||
atomic.AddUint32(&s.concurrency, ^uint32(0))
|
||||
s.writeFastError(c, StatusServiceUnavailable, "The connection cannot be served because Server.Concurrency limit exceeded")
|
||||
c.Close()
|
||||
return ErrConcurrencyLimit
|
||||
}
|
||||
|
||||
err := s.serveConn(c)
|
||||
|
||||
atomic.AddUint32(&s.concurrency, ^uint32(0))
|
||||
|
||||
if err != errHijacked {
|
||||
err1 := c.Close()
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
} else {
|
||||
err = nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var errHijacked = errors.New("connection has been hijacked")
|
||||
|
||||
func (s *Server) getConcurrency() int {
|
||||
n := s.Concurrency
|
||||
if n <= 0 {
|
||||
n = DefaultConcurrency
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
var globalConnID uint64
|
||||
|
||||
func nextConnID() uint64 {
|
||||
return atomic.AddUint64(&globalConnID, 1)
|
||||
}
|
||||
|
||||
// DefaultMaxRequestBodySize is the maximum request body size the server
|
||||
// reads by default.
|
||||
//
|
||||
// See Server.MaxRequestBodySize for details.
|
||||
const DefaultMaxRequestBodySize = 4 * 1024 * 1024
|
||||
|
||||
func (s *Server) serveConn(c net.Conn) error {
|
||||
serverName := s.getServerName()
|
||||
connRequestNum := uint64(0)
|
||||
connID := nextConnID()
|
||||
currentTime := CoarseTimeNow()
|
||||
connTime := currentTime
|
||||
maxRequestBodySize := s.MaxRequestBodySize
|
||||
if maxRequestBodySize <= 0 {
|
||||
maxRequestBodySize = DefaultMaxRequestBodySize
|
||||
}
|
||||
|
||||
ctx := s.acquireCtx(c)
|
||||
ctx.connTime = connTime
|
||||
isTLS := ctx.IsTLS()
|
||||
var (
|
||||
br *bufio.Reader
|
||||
bw *bufio.Writer
|
||||
|
||||
err error
|
||||
timeoutResponse *Response
|
||||
hijackHandler HijackHandler
|
||||
|
||||
lastReadDeadlineTime time.Time
|
||||
lastWriteDeadlineTime time.Time
|
||||
|
||||
connectionClose bool
|
||||
isHTTP11 bool
|
||||
)
|
||||
for {
|
||||
connRequestNum++
|
||||
ctx.time = currentTime
|
||||
|
||||
if s.ReadTimeout > 0 || s.MaxKeepaliveDuration > 0 {
|
||||
lastReadDeadlineTime = s.updateReadDeadline(c, ctx, lastReadDeadlineTime)
|
||||
if lastReadDeadlineTime.IsZero() {
|
||||
err = ErrKeepaliveTimeout
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !(s.ReduceMemoryUsage || ctx.lastReadDuration > time.Second) || br != nil {
|
||||
if br == nil {
|
||||
br = acquireReader(ctx)
|
||||
}
|
||||
} else {
|
||||
br, err = acquireByteReader(&ctx)
|
||||
}
|
||||
ctx.Request.isTLS = isTLS
|
||||
|
||||
if err == nil {
|
||||
if s.DisableHeaderNamesNormalizing {
|
||||
ctx.Request.Header.DisableNormalizing()
|
||||
ctx.Response.Header.DisableNormalizing()
|
||||
}
|
||||
err = ctx.Request.readLimitBody(br, maxRequestBodySize, s.GetOnly)
|
||||
if br.Buffered() == 0 || err != nil {
|
||||
releaseReader(s, br)
|
||||
br = nil
|
||||
}
|
||||
}
|
||||
|
||||
currentTime = CoarseTimeNow()
|
||||
ctx.lastReadDuration = currentTime.Sub(ctx.time)
|
||||
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
} else {
|
||||
bw = writeErrorResponse(bw, ctx, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// 'Expect: 100-continue' request handling.
|
||||
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html for details.
|
||||
if !ctx.Request.Header.noBody() && ctx.Request.MayContinue() {
|
||||
// Send 'HTTP/1.1 100 Continue' response.
|
||||
if bw == nil {
|
||||
bw = acquireWriter(ctx)
|
||||
}
|
||||
bw.Write(strResponseContinue)
|
||||
err = bw.Flush()
|
||||
releaseWriter(s, bw)
|
||||
bw = nil
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Read request body.
|
||||
if br == nil {
|
||||
br = acquireReader(ctx)
|
||||
}
|
||||
err = ctx.Request.ContinueReadBody(br, maxRequestBodySize)
|
||||
if br.Buffered() == 0 || err != nil {
|
||||
releaseReader(s, br)
|
||||
br = nil
|
||||
}
|
||||
if err != nil {
|
||||
bw = writeErrorResponse(bw, ctx, err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connectionClose = s.DisableKeepalive || ctx.Request.Header.connectionCloseFast()
|
||||
isHTTP11 = ctx.Request.Header.IsHTTP11()
|
||||
|
||||
ctx.Response.Header.SetServerBytes(serverName)
|
||||
ctx.connID = connID
|
||||
ctx.connRequestNum = connRequestNum
|
||||
ctx.connTime = connTime
|
||||
ctx.time = currentTime
|
||||
s.Handler(ctx)
|
||||
|
||||
timeoutResponse = ctx.timeoutResponse
|
||||
if timeoutResponse != nil {
|
||||
ctx = s.acquireCtx(c)
|
||||
timeoutResponse.CopyTo(&ctx.Response)
|
||||
if br != nil {
|
||||
// Close connection, since br may be attached to the old ctx via ctx.fbr.
|
||||
ctx.SetConnectionClose()
|
||||
}
|
||||
}
|
||||
|
||||
if !ctx.IsGet() && ctx.IsHead() {
|
||||
ctx.Response.SkipBody = true
|
||||
}
|
||||
ctx.Request.Reset()
|
||||
|
||||
hijackHandler = ctx.hijackHandler
|
||||
ctx.hijackHandler = nil
|
||||
|
||||
ctx.userValues.Reset()
|
||||
|
||||
if s.MaxRequestsPerConn > 0 && connRequestNum >= uint64(s.MaxRequestsPerConn) {
|
||||
ctx.SetConnectionClose()
|
||||
}
|
||||
|
||||
if s.WriteTimeout > 0 || s.MaxKeepaliveDuration > 0 {
|
||||
lastWriteDeadlineTime = s.updateWriteDeadline(c, ctx, lastWriteDeadlineTime)
|
||||
}
|
||||
|
||||
// Verify Request.Header.connectionCloseFast() again,
|
||||
// since request handler might trigger full headers' parsing.
|
||||
connectionClose = connectionClose || ctx.Request.Header.connectionCloseFast() || ctx.Response.ConnectionClose()
|
||||
if connectionClose {
|
||||
ctx.Response.Header.SetCanonical(strConnection, strClose)
|
||||
} else if !isHTTP11 {
|
||||
// Set 'Connection: keep-alive' response header for non-HTTP/1.1 request.
|
||||
// There is no need in setting this header for http/1.1, since in http/1.1
|
||||
// connections are keep-alive by default.
|
||||
ctx.Response.Header.SetCanonical(strConnection, strKeepAlive)
|
||||
}
|
||||
|
||||
if len(ctx.Response.Header.Server()) == 0 {
|
||||
ctx.Response.Header.SetServerBytes(serverName)
|
||||
}
|
||||
|
||||
if bw == nil {
|
||||
bw = acquireWriter(ctx)
|
||||
}
|
||||
if err = writeResponse(ctx, bw); err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
if br == nil || connectionClose {
|
||||
err = bw.Flush()
|
||||
releaseWriter(s, bw)
|
||||
bw = nil
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if connectionClose {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hijackHandler != nil {
|
||||
var hjr io.Reader
|
||||
hjr = c
|
||||
if br != nil {
|
||||
hjr = br
|
||||
br = nil
|
||||
|
||||
// br may point to ctx.fbr, so do not return ctx into pool.
|
||||
ctx = s.acquireCtx(c)
|
||||
}
|
||||
if bw != nil {
|
||||
err = bw.Flush()
|
||||
releaseWriter(s, bw)
|
||||
bw = nil
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
c.SetReadDeadline(zeroTime)
|
||||
c.SetWriteDeadline(zeroTime)
|
||||
go hijackConnHandler(hjr, c, s, hijackHandler)
|
||||
hijackHandler = nil
|
||||
err = errHijacked
|
||||
break
|
||||
}
|
||||
|
||||
currentTime = CoarseTimeNow()
|
||||
}
|
||||
|
||||
if br != nil {
|
||||
releaseReader(s, br)
|
||||
}
|
||||
if bw != nil {
|
||||
releaseWriter(s, bw)
|
||||
}
|
||||
s.releaseCtx(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Server) updateReadDeadline(c net.Conn, ctx *RequestCtx, lastDeadlineTime time.Time) time.Time {
|
||||
readTimeout := s.ReadTimeout
|
||||
currentTime := ctx.time
|
||||
if s.MaxKeepaliveDuration > 0 {
|
||||
connTimeout := s.MaxKeepaliveDuration - currentTime.Sub(ctx.connTime)
|
||||
if connTimeout <= 0 {
|
||||
return zeroTime
|
||||
}
|
||||
if connTimeout < readTimeout {
|
||||
readTimeout = connTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Optimization: update read deadline only if more than 25%
|
||||
// of the last read deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
if currentTime.Sub(lastDeadlineTime) > (readTimeout >> 2) {
|
||||
if err := c.SetReadDeadline(currentTime.Add(readTimeout)); err != nil {
|
||||
panic(fmt.Sprintf("BUG: error in SetReadDeadline(%s): %s", readTimeout, err))
|
||||
}
|
||||
lastDeadlineTime = currentTime
|
||||
}
|
||||
return lastDeadlineTime
|
||||
}
|
||||
|
||||
func (s *Server) updateWriteDeadline(c net.Conn, ctx *RequestCtx, lastDeadlineTime time.Time) time.Time {
|
||||
writeTimeout := s.WriteTimeout
|
||||
if s.MaxKeepaliveDuration > 0 {
|
||||
connTimeout := s.MaxKeepaliveDuration - time.Since(ctx.connTime)
|
||||
if connTimeout <= 0 {
|
||||
// MaxKeepAliveDuration exceeded, but let's try sending response anyway
|
||||
// in 100ms with 'Connection: close' header.
|
||||
ctx.SetConnectionClose()
|
||||
connTimeout = 100 * time.Millisecond
|
||||
}
|
||||
if connTimeout < writeTimeout {
|
||||
writeTimeout = connTimeout
|
||||
}
|
||||
}
|
||||
|
||||
// Optimization: update write deadline only if more than 25%
|
||||
// of the last write deadline exceeded.
|
||||
// See https://github.com/golang/go/issues/15133 for details.
|
||||
currentTime := CoarseTimeNow()
|
||||
if currentTime.Sub(lastDeadlineTime) > (writeTimeout >> 2) {
|
||||
if err := c.SetWriteDeadline(currentTime.Add(writeTimeout)); err != nil {
|
||||
panic(fmt.Sprintf("BUG: error in SetWriteDeadline(%s): %s", writeTimeout, err))
|
||||
}
|
||||
lastDeadlineTime = currentTime
|
||||
}
|
||||
return lastDeadlineTime
|
||||
}
|
||||
|
||||
func hijackConnHandler(r io.Reader, c net.Conn, s *Server, h HijackHandler) {
|
||||
hjc := s.acquireHijackConn(r, c)
|
||||
h(hjc)
|
||||
|
||||
if br, ok := r.(*bufio.Reader); ok {
|
||||
releaseReader(s, br)
|
||||
}
|
||||
c.Close()
|
||||
s.releaseHijackConn(hjc)
|
||||
}
|
||||
|
||||
func (s *Server) acquireHijackConn(r io.Reader, c net.Conn) *hijackConn {
|
||||
v := s.hijackConnPool.Get()
|
||||
if v == nil {
|
||||
hjc := &hijackConn{
|
||||
Conn: c,
|
||||
r: r,
|
||||
}
|
||||
return hjc
|
||||
}
|
||||
hjc := v.(*hijackConn)
|
||||
hjc.Conn = c
|
||||
hjc.r = r
|
||||
return hjc
|
||||
}
|
||||
|
||||
func (s *Server) releaseHijackConn(hjc *hijackConn) {
|
||||
hjc.Conn = nil
|
||||
hjc.r = nil
|
||||
s.hijackConnPool.Put(hjc)
|
||||
}
|
||||
|
||||
type hijackConn struct {
|
||||
net.Conn
|
||||
r io.Reader
|
||||
}
|
||||
|
||||
func (c hijackConn) Read(p []byte) (int, error) {
|
||||
return c.r.Read(p)
|
||||
}
|
||||
|
||||
func (c hijackConn) Close() error {
|
||||
// hijacked conn is closed in hijackConnHandler.
|
||||
return nil
|
||||
}
|
||||
|
||||
// LastTimeoutErrorResponse returns the last timeout response set
|
||||
// via TimeoutError* call.
|
||||
//
|
||||
// This function is intended for custom server implementations.
|
||||
func (ctx *RequestCtx) LastTimeoutErrorResponse() *Response {
|
||||
return ctx.timeoutResponse
|
||||
}
|
||||
|
||||
func writeResponse(ctx *RequestCtx, w *bufio.Writer) error {
|
||||
if ctx.timeoutResponse != nil {
|
||||
panic("BUG: cannot write timed out response")
|
||||
}
|
||||
err := ctx.Response.Write(w)
|
||||
ctx.Response.Reset()
|
||||
return err
|
||||
}
|
||||
|
||||
const (
|
||||
defaultReadBufferSize = 4096
|
||||
defaultWriteBufferSize = 4096
|
||||
)
|
||||
|
||||
func acquireByteReader(ctxP **RequestCtx) (*bufio.Reader, error) {
|
||||
ctx := *ctxP
|
||||
s := ctx.s
|
||||
c := ctx.c
|
||||
t := ctx.time
|
||||
s.releaseCtx(ctx)
|
||||
|
||||
// Make GC happy, so it could garbage collect ctx
|
||||
// while we waiting for the next request.
|
||||
ctx = nil
|
||||
*ctxP = nil
|
||||
|
||||
v := s.bytePool.Get()
|
||||
if v == nil {
|
||||
v = make([]byte, 1)
|
||||
}
|
||||
b := v.([]byte)
|
||||
n, err := c.Read(b)
|
||||
ch := b[0]
|
||||
s.bytePool.Put(v)
|
||||
ctx = s.acquireCtx(c)
|
||||
ctx.time = t
|
||||
*ctxP = ctx
|
||||
if err != nil {
|
||||
// Treat all errors as EOF on unsuccessful read
|
||||
// of the first request byte.
|
||||
return nil, io.EOF
|
||||
}
|
||||
if n != 1 {
|
||||
panic("BUG: Reader must return at least one byte")
|
||||
}
|
||||
|
||||
ctx.fbr.c = c
|
||||
ctx.fbr.ch = ch
|
||||
ctx.fbr.byteRead = false
|
||||
r := acquireReader(ctx)
|
||||
r.Reset(&ctx.fbr)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func acquireReader(ctx *RequestCtx) *bufio.Reader {
|
||||
v := ctx.s.readerPool.Get()
|
||||
if v == nil {
|
||||
n := ctx.s.ReadBufferSize
|
||||
if n <= 0 {
|
||||
n = defaultReadBufferSize
|
||||
}
|
||||
return bufio.NewReaderSize(ctx.c, n)
|
||||
}
|
||||
r := v.(*bufio.Reader)
|
||||
r.Reset(ctx.c)
|
||||
return r
|
||||
}
|
||||
|
||||
func releaseReader(s *Server, r *bufio.Reader) {
|
||||
s.readerPool.Put(r)
|
||||
}
|
||||
|
||||
func acquireWriter(ctx *RequestCtx) *bufio.Writer {
|
||||
v := ctx.s.writerPool.Get()
|
||||
if v == nil {
|
||||
n := ctx.s.WriteBufferSize
|
||||
if n <= 0 {
|
||||
n = defaultWriteBufferSize
|
||||
}
|
||||
return bufio.NewWriterSize(ctx.c, n)
|
||||
}
|
||||
w := v.(*bufio.Writer)
|
||||
w.Reset(ctx.c)
|
||||
return w
|
||||
}
|
||||
|
||||
func releaseWriter(s *Server, w *bufio.Writer) {
|
||||
s.writerPool.Put(w)
|
||||
}
|
||||
|
||||
func (s *Server) acquireCtx(c net.Conn) *RequestCtx {
|
||||
v := s.ctxPool.Get()
|
||||
var ctx *RequestCtx
|
||||
if v == nil {
|
||||
ctx = &RequestCtx{
|
||||
s: s,
|
||||
}
|
||||
keepBodyBuffer := !s.ReduceMemoryUsage
|
||||
ctx.Request.keepBodyBuffer = keepBodyBuffer
|
||||
ctx.Response.keepBodyBuffer = keepBodyBuffer
|
||||
} else {
|
||||
ctx = v.(*RequestCtx)
|
||||
}
|
||||
ctx.c = c
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Init2 prepares ctx for passing to RequestHandler.
|
||||
//
|
||||
// conn is used only for determining local and remote addresses.
|
||||
//
|
||||
// This function is intended for custom Server implementations.
|
||||
// See https://github.com/valyala/httpteleport for details.
|
||||
func (ctx *RequestCtx) Init2(conn net.Conn, logger Logger, reduceMemoryUsage bool) {
|
||||
ctx.c = conn
|
||||
ctx.logger.logger = logger
|
||||
ctx.connID = nextConnID()
|
||||
ctx.s = fakeServer
|
||||
ctx.connRequestNum = 0
|
||||
ctx.connTime = CoarseTimeNow()
|
||||
ctx.time = ctx.connTime
|
||||
|
||||
keepBodyBuffer := !reduceMemoryUsage
|
||||
ctx.Request.keepBodyBuffer = keepBodyBuffer
|
||||
ctx.Response.keepBodyBuffer = keepBodyBuffer
|
||||
}
|
||||
|
||||
// Init prepares ctx for passing to RequestHandler.
|
||||
//
|
||||
// remoteAddr and logger are optional. They are used by RequestCtx.Logger().
|
||||
//
|
||||
// This function is intended for custom Server implementations.
|
||||
func (ctx *RequestCtx) Init(req *Request, remoteAddr net.Addr, logger Logger) {
|
||||
if remoteAddr == nil {
|
||||
remoteAddr = zeroTCPAddr
|
||||
}
|
||||
c := &fakeAddrer{
|
||||
laddr: zeroTCPAddr,
|
||||
raddr: remoteAddr,
|
||||
}
|
||||
if logger == nil {
|
||||
logger = defaultLogger
|
||||
}
|
||||
ctx.Init2(c, logger, true)
|
||||
req.CopyTo(&ctx.Request)
|
||||
}
|
||||
|
||||
var fakeServer = &Server{
|
||||
// Initialize concurrencyCh for TimeoutHandler
|
||||
concurrencyCh: make(chan struct{}, DefaultConcurrency),
|
||||
}
|
||||
|
||||
type fakeAddrer struct {
|
||||
net.Conn
|
||||
laddr net.Addr
|
||||
raddr net.Addr
|
||||
}
|
||||
|
||||
func (fa *fakeAddrer) RemoteAddr() net.Addr {
|
||||
return fa.raddr
|
||||
}
|
||||
|
||||
func (fa *fakeAddrer) LocalAddr() net.Addr {
|
||||
return fa.laddr
|
||||
}
|
||||
|
||||
func (fa *fakeAddrer) Read(p []byte) (int, error) {
|
||||
panic("BUG: unexpected Read call")
|
||||
}
|
||||
|
||||
func (fa *fakeAddrer) Write(p []byte) (int, error) {
|
||||
panic("BUG: unexpected Write call")
|
||||
}
|
||||
|
||||
func (fa *fakeAddrer) Close() error {
|
||||
panic("BUG: unexpected Close call")
|
||||
}
|
||||
|
||||
func (s *Server) releaseCtx(ctx *RequestCtx) {
|
||||
if ctx.timeoutResponse != nil {
|
||||
panic("BUG: cannot release timed out RequestCtx")
|
||||
}
|
||||
ctx.c = nil
|
||||
ctx.fbr.c = nil
|
||||
s.ctxPool.Put(ctx)
|
||||
}
|
||||
|
||||
func (s *Server) getServerName() []byte {
|
||||
v := s.serverName.Load()
|
||||
var serverName []byte
|
||||
if v == nil {
|
||||
serverName = []byte(s.Name)
|
||||
if len(serverName) == 0 {
|
||||
serverName = defaultServerName
|
||||
}
|
||||
s.serverName.Store(serverName)
|
||||
} else {
|
||||
serverName = v.([]byte)
|
||||
}
|
||||
return serverName
|
||||
}
|
||||
|
||||
func (s *Server) writeFastError(w io.Writer, statusCode int, msg string) {
|
||||
w.Write(statusLine(statusCode))
|
||||
fmt.Fprintf(w, "Connection: close\r\n"+
|
||||
"Server: %s\r\n"+
|
||||
"Date: %s\r\n"+
|
||||
"Content-Type: text/plain\r\n"+
|
||||
"Content-Length: %d\r\n"+
|
||||
"\r\n"+
|
||||
"%s",
|
||||
s.getServerName(), serverDate.Load(), len(msg), msg)
|
||||
}
|
||||
|
||||
func writeErrorResponse(bw *bufio.Writer, ctx *RequestCtx, err error) *bufio.Writer {
|
||||
if _, ok := err.(*ErrSmallBuffer); ok {
|
||||
ctx.Error("Too big request header", StatusRequestHeaderFieldsTooLarge)
|
||||
} else {
|
||||
ctx.Error("Error when parsing request", StatusBadRequest)
|
||||
}
|
||||
ctx.SetConnectionClose()
|
||||
if bw == nil {
|
||||
bw = acquireWriter(ctx)
|
||||
}
|
||||
writeResponse(ctx, bw)
|
||||
bw.Flush()
|
||||
return bw
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4IQusAs8PJdnG
|
||||
3mURt/AXtgC+ceqLOatJ49JJE1VPTkMAy+oE1f1XvkMrYsHqmDf6GWVzgVXryL4U
|
||||
wq2/nJSm56ddhN55nI8oSN3dtywUB8/ShelEN73nlN77PeD9tl6NksPwWaKrqxq0
|
||||
FlabRPZSQCfmgZbhDV8Sa8mfCkFU0G0lit6kLGceCKMvmW+9Bz7ebsYmVdmVMxmf
|
||||
IJStFD44lWFTdUc65WISKEdW2ELcUefb0zOLw+0PCbXFGJH5x5ktksW8+BBk2Hkg
|
||||
GeQRL/qPCccthbScO0VgNj3zJ3ZZL0ObSDAbvNDG85joeNjDNq5DT/BAZ0bOSbEF
|
||||
sh+f9BAzAgMBAAECggEBAJWv2cq7Jw6MVwSRxYca38xuD6TUNBopgBvjREixURW2
|
||||
sNUaLuMb9Omp7fuOaE2N5rcJ+xnjPGIxh/oeN5MQctz9gwn3zf6vY+15h97pUb4D
|
||||
uGvYPRDaT8YVGS+X9NMZ4ZCmqW2lpWzKnCFoGHcy8yZLbcaxBsRdvKzwOYGoPiFb
|
||||
K2QuhXZ/1UPmqK9i2DFKtj40X6vBszTNboFxOVpXrPu0FJwLVSDf2hSZ4fMM0DH3
|
||||
YqwKcYf5te+hxGKgrqRA3tn0NCWii0in6QIwXMC+kMw1ebg/tZKqyDLMNptAK8J+
|
||||
DVw9m5X1seUHS5ehU/g2jrQrtK5WYn7MrFK4lBzlRwECgYEA/d1TeANYECDWRRDk
|
||||
B0aaRZs87Rwl/J9PsvbsKvtU/bX+OfSOUjOa9iQBqn0LmU8GqusEET/QVUfocVwV
|
||||
Bggf/5qDLxz100Rj0ags/yE/kNr0Bb31kkkKHFMnCT06YasR7qKllwrAlPJvQv9x
|
||||
IzBKq+T/Dx08Wep9bCRSFhzRCnsCgYEA+jdeZXTDr/Vz+D2B3nAw1frqYFfGnEVY
|
||||
wqmoK3VXMDkGuxsloO2rN+SyiUo3JNiQNPDub/t7175GH5pmKtZOlftePANsUjBj
|
||||
wZ1D0rI5Bxu/71ibIUYIRVmXsTEQkh/ozoh3jXCZ9+bLgYiYx7789IUZZSokFQ3D
|
||||
FICUT9KJ36kCgYAGoq9Y1rWJjmIrYfqj2guUQC+CfxbbGIrrwZqAsRsSmpwvhZ3m
|
||||
tiSZxG0quKQB+NfSxdvQW5ulbwC7Xc3K35F+i9pb8+TVBdeaFkw+yu6vaZmxQLrX
|
||||
fQM/pEjD7A7HmMIaO7QaU5SfEAsqdCTP56Y8AftMuNXn/8IRfo2KuGwaWwKBgFpU
|
||||
ILzJoVdlad9E/Rw7LjYhZfkv1uBVXIyxyKcfrkEXZSmozDXDdxsvcZCEfVHM6Ipk
|
||||
K/+7LuMcqp4AFEAEq8wTOdq6daFaHLkpt/FZK6M4TlruhtpFOPkoNc3e45eM83OT
|
||||
6mziKINJC1CQ6m65sQHpBtjxlKMRG8rL/D6wx9s5AoGBAMRlqNPMwglT3hvDmsAt
|
||||
9Lf9pdmhERUlHhD8bj8mDaBj2Aqv7f6VRJaYZqP403pKKQexuqcn80mtjkSAPFkN
|
||||
Cj7BVt/RXm5uoxDTnfi26RF9F6yNDEJ7UU9+peBr99aazF/fTgW/1GcMkQnum8uV
|
||||
c257YgaWmjK9uB0Y2r2VxS0G
|
||||
-----END PRIVATE KEY-----
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICujCCAaKgAwIBAgIJAMbXnKZ/cikUMA0GCSqGSIb3DQEBCwUAMBUxEzARBgNV
|
||||
BAMTCnVidW50dS5uYW4wHhcNMTUwMjA0MDgwMTM5WhcNMjUwMjAxMDgwMTM5WjAV
|
||||
MRMwEQYDVQQDEwp1YnVudHUubmFuMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||
CgKCAQEA+CELrALPDyXZxt5lEbfwF7YAvnHqizmrSePSSRNVT05DAMvqBNX9V75D
|
||||
K2LB6pg3+hllc4FV68i+FMKtv5yUpuenXYTeeZyPKEjd3bcsFAfP0oXpRDe955Te
|
||||
+z3g/bZejZLD8Fmiq6satBZWm0T2UkAn5oGW4Q1fEmvJnwpBVNBtJYrepCxnHgij
|
||||
L5lvvQc+3m7GJlXZlTMZnyCUrRQ+OJVhU3VHOuViEihHVthC3FHn29Mzi8PtDwm1
|
||||
xRiR+ceZLZLFvPgQZNh5IBnkES/6jwnHLYW0nDtFYDY98yd2WS9Dm0gwG7zQxvOY
|
||||
6HjYwzauQ0/wQGdGzkmxBbIfn/QQMwIDAQABow0wCzAJBgNVHRMEAjAAMA0GCSqG
|
||||
SIb3DQEBCwUAA4IBAQBQjKm/4KN/iTgXbLTL3i7zaxYXFLXsnT1tF+ay4VA8aj98
|
||||
L3JwRTciZ3A5iy/W4VSCt3eASwOaPWHKqDBB5RTtL73LoAqsWmO3APOGQAbixcQ2
|
||||
45GXi05OKeyiYRi1Nvq7Unv9jUkRDHUYVPZVSAjCpsXzPhFkmZoTRxmx5l0ZF7Li
|
||||
K91lI5h+eFq0dwZwrmlPambyh1vQUi70VHv8DNToVU29kel7YLbxGbuqETfhrcy6
|
||||
X+Mha6RYITkAn5FqsZcKMsc9eYGEF4l3XV+oS7q6xfTxktYJMFTI18J0lQ2Lv/CI
|
||||
whdMnYGntDQBE/iFCrJEGNsKGc38796GBOb5j+zd
|
||||
-----END CERTIFICATE-----
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Package stackless provides functionality that may save stack space
|
||||
// for high number of concurrently running goroutines.
|
||||
package stackless
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
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 interface{})) func(ctx interface{}) bool {
|
||||
if f == nil {
|
||||
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 interface{}) 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 interface{})) {
|
||||
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 interface{}
|
||||
done chan struct{}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package stackless
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"io"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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 stacklessWriterFunc = NewFunc(writerFunc)
|
||||
|
||||
func writerFunc(ctx interface{}) {
|
||||
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()
|
||||
}
|
||||
w.bb.Write(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *xWriter) Reset() {
|
||||
if w.bb != nil {
|
||||
bufferPool.Put(w.bb)
|
||||
w.bb = nil
|
||||
}
|
||||
}
|
||||
|
||||
var bufferPool bytebufferpool.Pool
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// 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
|
||||
|
||||
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
|
||||
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 (
|
||||
statusLines atomic.Value
|
||||
|
||||
statusMessages = map[int]string{
|
||||
StatusContinue: "Continue",
|
||||
StatusSwitchingProtocols: "Switching Protocols",
|
||||
StatusProcessing: "Processing",
|
||||
|
||||
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",
|
||||
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 {
|
||||
s := statusMessages[statusCode]
|
||||
if s == "" {
|
||||
s = "Unknown Status Code"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func init() {
|
||||
statusLines.Store(make(map[int][]byte))
|
||||
}
|
||||
|
||||
func statusLine(statusCode int) []byte {
|
||||
m := statusLines.Load().(map[int][]byte)
|
||||
h := m[statusCode]
|
||||
if h != nil {
|
||||
return h
|
||||
}
|
||||
|
||||
statusText := StatusMessage(statusCode)
|
||||
|
||||
h = []byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", statusCode, statusText))
|
||||
newM := make(map[int][]byte, len(m)+1)
|
||||
for k, v := range m {
|
||||
newM[k] = v
|
||||
}
|
||||
newM[statusCode] = h
|
||||
statusLines.Store(newM)
|
||||
return h
|
||||
}
|
||||
+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
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package fasthttp
|
||||
|
||||
var (
|
||||
defaultServerName = []byte("fasthttp")
|
||||
defaultUserAgent = []byte("fasthttp")
|
||||
defaultContentType = []byte("text/plain; charset=utf-8")
|
||||
)
|
||||
|
||||
var (
|
||||
strSlash = []byte("/")
|
||||
strSlashSlash = []byte("//")
|
||||
strSlashDotDot = []byte("/..")
|
||||
strSlashDotSlash = []byte("/./")
|
||||
strSlashDotDotSlash = []byte("/../")
|
||||
strCRLF = []byte("\r\n")
|
||||
strHTTP = []byte("http")
|
||||
strHTTPS = []byte("https")
|
||||
strHTTP11 = []byte("HTTP/1.1")
|
||||
strColonSlashSlash = []byte("://")
|
||||
strColonSpace = []byte(": ")
|
||||
strGMT = []byte("GMT")
|
||||
|
||||
strResponseContinue = []byte("HTTP/1.1 100 Continue\r\n\r\n")
|
||||
|
||||
strGet = []byte("GET")
|
||||
strHead = []byte("HEAD")
|
||||
strPost = []byte("POST")
|
||||
strPut = []byte("PUT")
|
||||
strDelete = []byte("DELETE")
|
||||
|
||||
strExpect = []byte("Expect")
|
||||
strConnection = []byte("Connection")
|
||||
strContentLength = []byte("Content-Length")
|
||||
strContentType = []byte("Content-Type")
|
||||
strDate = []byte("Date")
|
||||
strHost = []byte("Host")
|
||||
strReferer = []byte("Referer")
|
||||
strServer = []byte("Server")
|
||||
strTransferEncoding = []byte("Transfer-Encoding")
|
||||
strContentEncoding = []byte("Content-Encoding")
|
||||
strAcceptEncoding = []byte("Accept-Encoding")
|
||||
strUserAgent = []byte("User-Agent")
|
||||
strCookie = []byte("Cookie")
|
||||
strSetCookie = []byte("Set-Cookie")
|
||||
strLocation = []byte("Location")
|
||||
strIfModifiedSince = []byte("If-Modified-Since")
|
||||
strLastModified = []byte("Last-Modified")
|
||||
strAcceptRanges = []byte("Accept-Ranges")
|
||||
strRange = []byte("Range")
|
||||
strContentRange = []byte("Content-Range")
|
||||
|
||||
strCookieExpires = []byte("expires")
|
||||
strCookieDomain = []byte("domain")
|
||||
strCookiePath = []byte("path")
|
||||
strCookieHTTPOnly = []byte("HttpOnly")
|
||||
strCookieSecure = []byte("secure")
|
||||
|
||||
strClose = []byte("close")
|
||||
strGzip = []byte("gzip")
|
||||
strDeflate = []byte("deflate")
|
||||
strKeepAlive = []byte("keep-alive")
|
||||
strKeepAliveCamelCase = []byte("Keep-Alive")
|
||||
strUpgrade = []byte("Upgrade")
|
||||
strChunked = []byte("chunked")
|
||||
strIdentity = []byte("identity")
|
||||
str100Continue = []byte("100-continue")
|
||||
strPostArgsContentType = []byte("application/x-www-form-urlencoded")
|
||||
strMultipartFormData = []byte("multipart/form-data")
|
||||
strBoundary = []byte("boundary")
|
||||
strBytes = []byte("bytes")
|
||||
strTextSlash = []byte("text/")
|
||||
strApplicationSlash = []byte("application/")
|
||||
)
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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 DefaultDNSCacheDuration.
|
||||
// * 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 getDialer(DefaultDialTimeout, false)(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 DefaultDNSCacheDuration.
|
||||
// * 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.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 DialTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return getDialer(timeout, false)(addr)
|
||||
}
|
||||
|
||||
// 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 DefaultDNSCacheDuration.
|
||||
// * 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 getDialer(DefaultDialTimeout, true)(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 DefaultDNSCacheDuration.
|
||||
// * 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.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 DialDualStackTimeout(addr string, timeout time.Duration) (net.Conn, error) {
|
||||
return getDialer(timeout, true)(addr)
|
||||
}
|
||||
|
||||
func getDialer(timeout time.Duration, dualStack bool) DialFunc {
|
||||
if timeout <= 0 {
|
||||
timeout = DefaultDialTimeout
|
||||
}
|
||||
timeoutRounded := int(timeout.Seconds()*10 + 9)
|
||||
|
||||
m := dialMap
|
||||
if dualStack {
|
||||
m = dialDualStackMap
|
||||
}
|
||||
|
||||
dialMapLock.Lock()
|
||||
d := m[timeoutRounded]
|
||||
if d == nil {
|
||||
dialer := dialerStd
|
||||
if dualStack {
|
||||
dialer = dialerDualStack
|
||||
}
|
||||
d = dialer.NewDial(timeout)
|
||||
m[timeoutRounded] = d
|
||||
}
|
||||
dialMapLock.Unlock()
|
||||
return d
|
||||
}
|
||||
|
||||
var (
|
||||
dialerStd = &tcpDialer{}
|
||||
dialerDualStack = &tcpDialer{DualStack: true}
|
||||
|
||||
dialMap = make(map[int]DialFunc)
|
||||
dialDualStackMap = make(map[int]DialFunc)
|
||||
dialMapLock sync.Mutex
|
||||
)
|
||||
|
||||
type tcpDialer struct {
|
||||
DualStack bool
|
||||
|
||||
tcpAddrsLock sync.Mutex
|
||||
tcpAddrsMap map[string]*tcpAddrEntry
|
||||
|
||||
concurrencyCh chan struct{}
|
||||
|
||||
once sync.Once
|
||||
}
|
||||
|
||||
const maxDialConcurrency = 1000
|
||||
|
||||
func (d *tcpDialer) NewDial(timeout time.Duration) DialFunc {
|
||||
d.once.Do(func() {
|
||||
d.concurrencyCh = make(chan struct{}, maxDialConcurrency)
|
||||
d.tcpAddrsMap = make(map[string]*tcpAddrEntry)
|
||||
go d.tcpAddrsClean()
|
||||
})
|
||||
|
||||
return func(addr string) (net.Conn, error) {
|
||||
addrs, idx, err := d.getTCPAddrs(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
network := "tcp4"
|
||||
if d.DualStack {
|
||||
network = "tcp"
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
n := uint32(len(addrs))
|
||||
deadline := time.Now().Add(timeout)
|
||||
for n > 0 {
|
||||
conn, err = tryDial(network, &addrs[idx%n], deadline, d.concurrencyCh)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
if err == ErrDialTimeout {
|
||||
return nil, err
|
||||
}
|
||||
idx++
|
||||
n--
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func tryDial(network string, addr *net.TCPAddr, deadline time.Time, concurrencyCh chan struct{}) (net.Conn, error) {
|
||||
timeout := -time.Since(deadline)
|
||||
if timeout <= 0 {
|
||||
return nil, ErrDialTimeout
|
||||
}
|
||||
|
||||
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, ErrDialTimeout
|
||||
}
|
||||
}
|
||||
|
||||
timeout = -time.Since(deadline)
|
||||
if timeout <= 0 {
|
||||
<-concurrencyCh
|
||||
return nil, ErrDialTimeout
|
||||
}
|
||||
|
||||
chv := dialResultChanPool.Get()
|
||||
if chv == nil {
|
||||
chv = make(chan dialResult, 1)
|
||||
}
|
||||
ch := chv.(chan dialResult)
|
||||
go func() {
|
||||
var dr dialResult
|
||||
dr.conn, dr.err = net.DialTCP(network, nil, addr)
|
||||
ch <- dr
|
||||
<-concurrencyCh
|
||||
}()
|
||||
|
||||
var (
|
||||
conn net.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
tc := acquireTimer(timeout)
|
||||
select {
|
||||
case dr := <-ch:
|
||||
conn = dr.conn
|
||||
err = dr.err
|
||||
dialResultChanPool.Put(ch)
|
||||
case <-tc.C:
|
||||
err = ErrDialTimeout
|
||||
}
|
||||
releaseTimer(tc)
|
||||
|
||||
return conn, err
|
||||
}
|
||||
|
||||
var dialResultChanPool sync.Pool
|
||||
|
||||
type dialResult struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
|
||||
// ErrDialTimeout is returned when TCP dialing is timed out.
|
||||
var ErrDialTimeout = errors.New("dialing to the given TCP address timed out")
|
||||
|
||||
// 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
|
||||
|
||||
resolveTime time.Time
|
||||
pending bool
|
||||
}
|
||||
|
||||
// DefaultDNSCacheDuration is the duration for caching resolved TCP addresses
|
||||
// by Dial* functions.
|
||||
const DefaultDNSCacheDuration = time.Minute
|
||||
|
||||
func (d *tcpDialer) tcpAddrsClean() {
|
||||
expireDuration := 2 * DefaultDNSCacheDuration
|
||||
for {
|
||||
time.Sleep(time.Second)
|
||||
t := time.Now()
|
||||
|
||||
d.tcpAddrsLock.Lock()
|
||||
for k, e := range d.tcpAddrsMap {
|
||||
if t.Sub(e.resolveTime) > expireDuration {
|
||||
delete(d.tcpAddrsMap, k)
|
||||
}
|
||||
}
|
||||
d.tcpAddrsLock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *tcpDialer) getTCPAddrs(addr string) ([]net.TCPAddr, uint32, error) {
|
||||
d.tcpAddrsLock.Lock()
|
||||
e := d.tcpAddrsMap[addr]
|
||||
if e != nil && !e.pending && time.Since(e.resolveTime) > DefaultDNSCacheDuration {
|
||||
e.pending = true
|
||||
e = nil
|
||||
}
|
||||
d.tcpAddrsLock.Unlock()
|
||||
|
||||
if e == nil {
|
||||
addrs, err := resolveTCPAddrs(addr, d.DualStack)
|
||||
if err != nil {
|
||||
d.tcpAddrsLock.Lock()
|
||||
e = d.tcpAddrsMap[addr]
|
||||
if e != nil && e.pending {
|
||||
e.pending = false
|
||||
}
|
||||
d.tcpAddrsLock.Unlock()
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
e = &tcpAddrEntry{
|
||||
addrs: addrs,
|
||||
resolveTime: time.Now(),
|
||||
}
|
||||
|
||||
d.tcpAddrsLock.Lock()
|
||||
d.tcpAddrsMap[addr] = e
|
||||
d.tcpAddrsLock.Unlock()
|
||||
}
|
||||
|
||||
idx := atomic.AddUint32(&e.addrsIdx, 1)
|
||||
return e.addrs, idx, nil
|
||||
}
|
||||
|
||||
func resolveTCPAddrs(addr string, dualStack bool) ([]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
|
||||
}
|
||||
|
||||
ips, err := net.LookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
n := len(ips)
|
||||
addrs := make([]net.TCPAddr, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ip := ips[i]
|
||||
if !dualStack && ip.To4() == nil {
|
||||
continue
|
||||
}
|
||||
addrs = append(addrs, net.TCPAddr{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
})
|
||||
}
|
||||
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")
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
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) {
|
||||
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:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func releaseTimer(t *time.Timer) {
|
||||
stopTimer(t)
|
||||
timerPool.Put(t)
|
||||
}
|
||||
|
||||
var timerPool sync.Pool
|
||||
+525
@@ -0,0 +1,525 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"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() interface{} {
|
||||
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
|
||||
|
||||
fullURI []byte
|
||||
requestURI []byte
|
||||
|
||||
h *RequestHeader
|
||||
}
|
||||
|
||||
// CopyTo copies uri contents to dst.
|
||||
func (u *URI) CopyTo(dst *URI) {
|
||||
dst.Reset()
|
||||
dst.pathOriginal = append(dst.pathOriginal[:0], u.pathOriginal...)
|
||||
dst.scheme = append(dst.scheme[:0], u.scheme...)
|
||||
dst.path = append(dst.path[:0], u.path...)
|
||||
dst.queryString = append(dst.queryString[:0], u.queryString...)
|
||||
dst.hash = append(dst.hash[:0], u.hash...)
|
||||
dst.host = append(dst.host[:0], u.host...)
|
||||
|
||||
u.queryArgs.CopyTo(&dst.queryArgs)
|
||||
dst.parsedQueryArgs = u.parsedQueryArgs
|
||||
|
||||
// fullURI and requestURI shouldn't be copied, since they are created
|
||||
// from scratch on each FullURI() and RequestURI() call.
|
||||
dst.h = u.h
|
||||
}
|
||||
|
||||
// Hash returns URI hash, i.e. qwe of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// The returned value is 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...)
|
||||
}
|
||||
|
||||
// QueryString returns URI query string,
|
||||
// i.e. baz=123 of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// The returned value is 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 value is 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 value is 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 value is 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)
|
||||
}
|
||||
|
||||
// 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.host = u.host[:0]
|
||||
u.queryArgs.Reset()
|
||||
u.parsedQueryArgs = false
|
||||
|
||||
// There is no need in u.fullURI = u.fullURI[:0], since full uri
|
||||
// is calucalted on each call to FullURI().
|
||||
|
||||
// There is no need in u.requestURI = u.requestURI[:0], since requestURI
|
||||
// is calculated on each call to RequestURI().
|
||||
|
||||
u.h = nil
|
||||
}
|
||||
|
||||
// Host returns host part, i.e. aaa.com of http://aaa.com/foo/bar?baz=123#qwe .
|
||||
//
|
||||
// Host is always lowercased.
|
||||
func (u *URI) Host() []byte {
|
||||
if len(u.host) == 0 && u.h != nil {
|
||||
u.host = append(u.host[:0], u.h.Host()...)
|
||||
lowercaseBytes(u.host)
|
||||
u.h = nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
u.parse(host, uri, nil)
|
||||
}
|
||||
|
||||
func (u *URI) parseQuick(uri []byte, h *RequestHeader, isTLS bool) {
|
||||
u.parse(nil, uri, h)
|
||||
if isTLS {
|
||||
u.scheme = append(u.scheme[:0], strHTTPS...)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *URI) parse(host, uri []byte, h *RequestHeader) {
|
||||
u.Reset()
|
||||
u.h = h
|
||||
|
||||
scheme, host, uri := splitHostURI(host, uri)
|
||||
u.scheme = append(u.scheme, scheme...)
|
||||
lowercaseBytes(u.scheme)
|
||||
u.host = append(u.host, host...)
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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:]...)
|
||||
}
|
||||
|
||||
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 strSlash
|
||||
}
|
||||
b = b[:nn+1]
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// RequestURI returns RequestURI - i.e. URI without Scheme and Host.
|
||||
func (u *URI) RequestURI() []byte {
|
||||
dst := appendQuotedPath(u.requestURI[:0], u.Path())
|
||||
if 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...)
|
||||
}
|
||||
if len(u.hash) > 0 {
|
||||
dst = append(dst, '#')
|
||||
dst = append(dst, u.hash...)
|
||||
}
|
||||
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.
|
||||
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...)
|
||||
}
|
||||
u.Parse(nil, newURI)
|
||||
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...)
|
||||
u.Parse(nil, buf)
|
||||
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("BUG: path must contain at least one slash")
|
||||
}
|
||||
buf = u.appendSchemeHost(buf[:0])
|
||||
buf = appendQuotedPath(buf, path[:n+1])
|
||||
buf = append(buf, newURI...)
|
||||
u.Parse(nil, buf)
|
||||
return buf
|
||||
}
|
||||
}
|
||||
|
||||
// FullURI returns full uri in the form {Scheme}://{Host}{RequestURI}#{Hash}.
|
||||
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)
|
||||
return append(dst, u.RequestURI()...)
|
||||
}
|
||||
|
||||
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, '/')
|
||||
if n < 0 {
|
||||
// A hack for bogus urls like foobar.com?a=b without
|
||||
// slash after host.
|
||||
if n = bytes.IndexByte(uri, '?'); n >= 0 {
|
||||
return scheme, uri[:n], uri[n:]
|
||||
}
|
||||
return scheme, uri, strSlash
|
||||
}
|
||||
return scheme, uri[:n], uri[n:]
|
||||
}
|
||||
|
||||
// QueryArgs returns query args.
|
||||
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
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// +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
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// +build windows
|
||||
|
||||
package fasthttp
|
||||
|
||||
func addLeadingSlash(dst, src []byte) []byte {
|
||||
// zero length and "C:/" case
|
||||
if len(src) == 0 || (len(src) > 2 && src[1] != ':') {
|
||||
dst = append(dst, '/')
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
type userDataKV struct {
|
||||
key []byte
|
||||
value interface{}
|
||||
}
|
||||
|
||||
type userData []userDataKV
|
||||
|
||||
func (d *userData) Set(key string, value interface{}) {
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if string(kv.key) == key {
|
||||
kv.value = value
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c := cap(args)
|
||||
if c > n {
|
||||
args = args[:n+1]
|
||||
kv := &args[n]
|
||||
kv.key = append(kv.key[:0], key...)
|
||||
kv.value = value
|
||||
*d = args
|
||||
return
|
||||
}
|
||||
|
||||
kv := userDataKV{}
|
||||
kv.key = append(kv.key[:0], key...)
|
||||
kv.value = value
|
||||
*d = append(args, kv)
|
||||
}
|
||||
|
||||
func (d *userData) SetBytes(key []byte, value interface{}) {
|
||||
d.Set(b2s(key), value)
|
||||
}
|
||||
|
||||
func (d *userData) Get(key string) interface{} {
|
||||
args := *d
|
||||
n := len(args)
|
||||
for i := 0; i < n; i++ {
|
||||
kv := &args[i]
|
||||
if string(kv.key) == key {
|
||||
return kv.value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *userData) GetBytes(key []byte) interface{} {
|
||||
return d.Get(b2s(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]
|
||||
}
|
||||
+231
@@ -0,0 +1,231 @@
|
||||
package fasthttp
|
||||
|
||||
import (
|
||||
"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 func(c net.Conn) error
|
||||
|
||||
MaxWorkersCount int
|
||||
|
||||
LogAllErrors bool
|
||||
|
||||
MaxIdleWorkerDuration time.Duration
|
||||
|
||||
Logger Logger
|
||||
|
||||
lock sync.Mutex
|
||||
workersCount int
|
||||
mustStop bool
|
||||
|
||||
ready []*workerChan
|
||||
|
||||
stopCh chan struct{}
|
||||
|
||||
workerChanPool sync.Pool
|
||||
}
|
||||
|
||||
type workerChan struct {
|
||||
lastUseTime time.Time
|
||||
ch chan net.Conn
|
||||
}
|
||||
|
||||
func (wp *workerPool) Start() {
|
||||
if wp.stopCh != nil {
|
||||
panic("BUG: workerPool already started")
|
||||
}
|
||||
wp.stopCh = make(chan struct{})
|
||||
stopCh := wp.stopCh
|
||||
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 {
|
||||
panic("BUG: workerPool wasn't started")
|
||||
}
|
||||
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, ch := range ready {
|
||||
ch.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.
|
||||
currentTime := time.Now()
|
||||
|
||||
wp.lock.Lock()
|
||||
ready := wp.ready
|
||||
n := len(ready)
|
||||
i := 0
|
||||
for i < n && currentTime.Sub(ready[i].lastUseTime) > maxIdleWorkerDuration {
|
||||
i++
|
||||
}
|
||||
*scratch = append((*scratch)[:0], ready[:i]...)
|
||||
if i > 0 {
|
||||
m := copy(ready, ready[i:])
|
||||
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, ch := range tmp {
|
||||
ch.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()
|
||||
if vch == nil {
|
||||
vch = &workerChan{
|
||||
ch: make(chan net.Conn, workerChanCap),
|
||||
}
|
||||
}
|
||||
ch = vch.(*workerChan)
|
||||
go func() {
|
||||
wp.workerFunc(ch)
|
||||
wp.workerChanPool.Put(vch)
|
||||
}()
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (wp *workerPool) release(ch *workerChan) bool {
|
||||
ch.lastUseTime = CoarseTimeNow()
|
||||
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, "i/o timeout")) {
|
||||
wp.Logger.Printf("error when serving connection %q<->%q: %s", c.LocalAddr(), c.RemoteAddr(), err)
|
||||
}
|
||||
}
|
||||
if err != errHijacked {
|
||||
c.Close()
|
||||
}
|
||||
c = nil
|
||||
|
||||
if !wp.release(ch) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
wp.lock.Lock()
|
||||
wp.workersCount--
|
||||
wp.lock.Unlock()
|
||||
}
|
||||
Reference in New Issue
Block a user