mirror of
https://github.com/caddyserver/caddy.git
synced 2026-10-05 12:21:36 -04:00
A request whose body has no length of its own — Transfer-Encoding: chunked, or HTTP/2 and HTTP/3 requests sent without the header — cannot be forwarded over FastCGI until it has been buffered in full, because CGI/1.1 requires CONTENT_LENGTH and php-fpm hangs when it is absent or wrong and the body is not empty. When request_buffers is too small to hold the whole body, the length stays unknown and the request is refused with 411. The refusal said none of that. RoundTrip passes r.ContentLength to Post, which is -1 for such a request, so the operator got a bare "411 Length Required" with either no error at all or strconv's "invalid syntax" on a value they never wrote. On #7386 that sent two people looking for the fault in their backend. The 411 now carries the reason and the remedy, and the unknown-length case is told apart from a genuinely malformed value. ParseUint becomes ParseInt plus an explicit negative check, which is strictly tighter: values above MaxInt64 used to be accepted and are unreachable anyway, since CONTENT_LENGTH is always FormatInt of an int64 by the time Do sees it. No status code changes. Every request that was refused before is still refused; raising request_buffers is still what makes a large chunked body work. Tests: the reachable path through Post, the unusable CONTENT_LENGTH values Do itself guards against, an integration test driving a chunked body through a fastcgi reverse_proxy over a unix socket at each side of the buffer boundary (including request_buffers -1, which succeeds at any size and is the remedy), and a boundary test recording that a body exactly the size of the buffer counts as partial. That last one is deliberately not changed here. Telling "exactly the limit" apart from "more to come" costs a read that a paused stream may never answer, and bufferedBody also backs response_buffers: measured against a body that delivers exactly the limit and stops, the current code hands the buffered prefix on immediately, while peeking one byte first delivers nothing at all. Co-authored-by: Aditya <205600203+Rohilalala@users.noreply.github.com> Co-authored-by: Zen Dodd <mail@steadytao.com>
399 lines
11 KiB
Go
399 lines
11 KiB
Go
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Forked Jan. 2015 from http://bitbucket.org/PinIdea/fcgi_client
|
|
// (which is forked from https://code.google.com/p/go-fastcgi-client/).
|
|
// This fork contains several fixes and improvements by Matt Holt and
|
|
// other contributors to the Caddy project.
|
|
|
|
// Copyright 2012 Junqing Tan <ivan@mysqlab.net> and The Go Authors
|
|
// Use of this source code is governed by a BSD-style
|
|
// Part of source code is from Go fcgi package
|
|
|
|
package fastcgi
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/textproto"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
|
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
|
)
|
|
|
|
// FCGIListenSockFileno describes listen socket file number.
|
|
const FCGIListenSockFileno uint8 = 0
|
|
|
|
// FCGIHeaderLen describes header length.
|
|
const FCGIHeaderLen uint8 = 8
|
|
|
|
// Version1 describes the version.
|
|
const Version1 uint8 = 1
|
|
|
|
// FCGINullRequestID describes the null request ID.
|
|
const FCGINullRequestID uint8 = 0
|
|
|
|
// FCGIKeepConn describes keep connection mode.
|
|
const FCGIKeepConn uint8 = 1
|
|
|
|
const (
|
|
// BeginRequest is the begin request flag.
|
|
BeginRequest uint8 = iota + 1
|
|
// AbortRequest is the abort request flag.
|
|
AbortRequest
|
|
// EndRequest is the end request flag.
|
|
EndRequest
|
|
// Params is the parameters flag.
|
|
Params
|
|
// Stdin is the standard input flag.
|
|
Stdin
|
|
// Stdout is the standard output flag.
|
|
Stdout
|
|
// Stderr is the standard error flag.
|
|
Stderr
|
|
// Data is the data flag.
|
|
Data
|
|
// GetValues is the get values flag.
|
|
GetValues
|
|
// GetValuesResult is the get values result flag.
|
|
GetValuesResult
|
|
// UnknownType is the unknown type flag.
|
|
UnknownType
|
|
// MaxType is the maximum type flag.
|
|
MaxType = UnknownType
|
|
)
|
|
|
|
const (
|
|
// Responder is the responder flag.
|
|
Responder uint8 = iota + 1
|
|
// Authorizer is the authorizer flag.
|
|
Authorizer
|
|
// Filter is the filter flag.
|
|
Filter
|
|
)
|
|
|
|
const (
|
|
// RequestComplete is the completed request flag.
|
|
RequestComplete uint8 = iota
|
|
// CantMultiplexConns is the multiplexed connections flag.
|
|
CantMultiplexConns
|
|
// Overloaded is the overloaded flag.
|
|
Overloaded
|
|
// UnknownRole is the unknown role flag.
|
|
UnknownRole
|
|
)
|
|
|
|
const (
|
|
// MaxConns is the maximum connections flag.
|
|
MaxConns string = "MAX_CONNS"
|
|
// MaxRequests is the maximum requests flag.
|
|
MaxRequests string = "MAX_REQS"
|
|
// MultiplexConns is the multiplex connections flag.
|
|
MultiplexConns string = "MPXS_CONNS"
|
|
)
|
|
|
|
const (
|
|
maxWrite = 65500 // 65530 may work, but for compatibility
|
|
maxPad = 255
|
|
)
|
|
|
|
// for padding so we don't have to allocate all the time
|
|
// not synchronized because we don't care what the contents are
|
|
var pad [maxPad]byte
|
|
|
|
// FastCGI has no way to express a body of unknown length: CONTENT_LENGTH is
|
|
// required by CGI/1.1, and php-fpm hangs when it is absent or wrong and the
|
|
// body is not empty. A request that arrives without a length therefore has to
|
|
// be buffered in full before it can be forwarded.
|
|
var (
|
|
errNoContentLength = errors.New("request body has no known length; increase request_buffers so the body can be buffered in full to determine it")
|
|
errBadContentLength = errors.New("request body length is not a valid CONTENT_LENGTH")
|
|
)
|
|
|
|
// client implements a FastCGI client, which is a standard for
|
|
// interfacing external applications with Web servers.
|
|
type client struct {
|
|
rwc net.Conn
|
|
// keepAlive bool // TODO: implement
|
|
reqID uint16
|
|
stderr bool
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// Do makes the request and returns an io.Reader that translates the data read
|
|
// from the FastCGI responder out of FastCGI packets before returning it.
|
|
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
|
|
// check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang.
|
|
// A request with no length of its own (Transfer-Encoding: chunked, or an HTTP/2 or HTTP/3
|
|
// request sent without the header) only acquires one by being buffered in full, so say so:
|
|
// the bare status alone has sent people looking for the fault in their backend.
|
|
if clStr, ok := p["CONTENT_LENGTH"]; !ok || clStr == "" {
|
|
return nil, caddyhttp.Error(http.StatusLengthRequired, errNoContentLength)
|
|
} else if l, err := strconv.ParseInt(clStr, 10, 64); err != nil {
|
|
return nil, caddyhttp.Error(http.StatusLengthRequired, fmt.Errorf("%w: %w", errBadContentLength, err))
|
|
} else if l < 0 {
|
|
// net/http reports -1 for a body whose length it does not know, which is
|
|
// what a chunked request looks like by the time it reaches here. This is
|
|
// the case operators actually hit, so it gets the actionable message.
|
|
return nil, caddyhttp.Error(http.StatusLengthRequired, errNoContentLength)
|
|
}
|
|
|
|
writer := &streamWriter{c: c}
|
|
writer.buf = bufPool.Get().(*bytes.Buffer)
|
|
writer.buf.Reset()
|
|
defer bufPool.Put(writer.buf)
|
|
|
|
err = writer.writeBeginRequest(uint16(Responder), 0)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
|
|
writer.recType = Params
|
|
err = writer.writePairs(p)
|
|
if err != nil {
|
|
return r, err
|
|
}
|
|
|
|
writer.recType = Stdin
|
|
if req != nil {
|
|
_, err = io.Copy(writer, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
err = writer.FlushStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
r = &streamReader{c: c}
|
|
return r, err
|
|
}
|
|
|
|
// clientCloser is an io.ReadCloser. It wraps an io.Reader with a Closer
|
|
// that closes the client connection.
|
|
type clientCloser struct {
|
|
rwc net.Conn
|
|
r *streamReader
|
|
io.Reader
|
|
|
|
status int
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func (f clientCloser) Close() error {
|
|
stderr := f.r.stderr.Bytes()
|
|
if len(stderr) == 0 {
|
|
return f.rwc.Close()
|
|
}
|
|
|
|
logLevel := zapcore.WarnLevel
|
|
if f.status >= 400 {
|
|
logLevel = zapcore.ErrorLevel
|
|
}
|
|
|
|
if c := f.logger.Check(logLevel, "stderr"); c != nil {
|
|
c.Write(zap.ByteString("body", stderr))
|
|
}
|
|
|
|
return f.rwc.Close()
|
|
}
|
|
|
|
// Request returns an HTTP response with header and body
|
|
// from the FastCGI responder.
|
|
func (c *client) Request(p map[string]string, req io.Reader) (resp *http.Response, err error) {
|
|
r, err := c.Do(p, req)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
rb := bufio.NewReader(r)
|
|
tp := textproto.NewReader(rb)
|
|
resp = new(http.Response)
|
|
|
|
// Parse the response headers.
|
|
mimeHeader, err := tp.ReadMIMEHeader()
|
|
if err != nil && err != io.EOF {
|
|
return resp, err
|
|
}
|
|
resp.Header = http.Header(mimeHeader)
|
|
|
|
if resp.Header.Get("Status") != "" {
|
|
statusNumber, statusInfo, statusIsCut := strings.Cut(resp.Header.Get("Status"), " ")
|
|
resp.StatusCode, err = strconv.Atoi(statusNumber)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
if statusIsCut {
|
|
resp.Status = statusInfo
|
|
}
|
|
} else {
|
|
resp.StatusCode = http.StatusOK
|
|
}
|
|
|
|
// TODO: fixTransferEncoding ?
|
|
resp.TransferEncoding = resp.Header["Transfer-Encoding"]
|
|
resp.ContentLength, _ = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
|
|
|
// wrap the response body in our closer
|
|
closer := clientCloser{
|
|
rwc: c.rwc,
|
|
r: r.(*streamReader),
|
|
Reader: rb,
|
|
status: resp.StatusCode,
|
|
logger: noopLogger,
|
|
}
|
|
if chunked(resp.TransferEncoding) {
|
|
closer.Reader = httputil.NewChunkedReader(rb)
|
|
}
|
|
if c.stderr {
|
|
closer.logger = c.logger
|
|
}
|
|
resp.Body = closer
|
|
|
|
return resp, err
|
|
}
|
|
|
|
// Get issues a GET request to the fcgi responder.
|
|
func (c *client) Get(p map[string]string, body io.Reader, l int64) (resp *http.Response, err error) {
|
|
p["REQUEST_METHOD"] = "GET"
|
|
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
|
|
|
return c.Request(p, body)
|
|
}
|
|
|
|
// Head issues a HEAD request to the fcgi responder.
|
|
func (c *client) Head(p map[string]string) (resp *http.Response, err error) {
|
|
p["REQUEST_METHOD"] = "HEAD"
|
|
p["CONTENT_LENGTH"] = "0"
|
|
|
|
return c.Request(p, nil)
|
|
}
|
|
|
|
// Options issues an OPTIONS request to the fcgi responder.
|
|
func (c *client) Options(p map[string]string) (resp *http.Response, err error) {
|
|
p["REQUEST_METHOD"] = "OPTIONS"
|
|
p["CONTENT_LENGTH"] = "0"
|
|
|
|
return c.Request(p, nil)
|
|
}
|
|
|
|
// Post issues a POST request to the fcgi responder. with request body
|
|
// in the format that bodyType specified
|
|
func (c *client) Post(p map[string]string, method string, bodyType string, body io.Reader, l int64) (resp *http.Response, err error) {
|
|
if p == nil {
|
|
p = make(map[string]string)
|
|
}
|
|
|
|
p["REQUEST_METHOD"] = strings.ToUpper(method)
|
|
|
|
if len(p["REQUEST_METHOD"]) == 0 || p["REQUEST_METHOD"] == "GET" {
|
|
p["REQUEST_METHOD"] = "POST"
|
|
}
|
|
|
|
p["CONTENT_LENGTH"] = strconv.FormatInt(l, 10)
|
|
if len(bodyType) > 0 {
|
|
p["CONTENT_TYPE"] = bodyType
|
|
} else {
|
|
p["CONTENT_TYPE"] = "application/x-www-form-urlencoded"
|
|
}
|
|
|
|
return c.Request(p, body)
|
|
}
|
|
|
|
// PostForm issues a POST to the fcgi responder, with form
|
|
// as a string key to a list values (url.Values)
|
|
func (c *client) PostForm(p map[string]string, data url.Values) (resp *http.Response, err error) {
|
|
body := bytes.NewReader([]byte(data.Encode()))
|
|
return c.Post(p, "POST", "application/x-www-form-urlencoded", body, int64(body.Len()))
|
|
}
|
|
|
|
// PostFile issues a POST to the fcgi responder in multipart(RFC 2046) standard,
|
|
// with form as a string key to a list values (url.Values),
|
|
// and/or with file as a string key to a list file path.
|
|
func (c *client) PostFile(p map[string]string, data url.Values, file map[string]string) (resp *http.Response, err error) {
|
|
buf := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(buf)
|
|
bodyType := writer.FormDataContentType()
|
|
|
|
for key, val := range data {
|
|
for _, v0 := range val {
|
|
err = writer.WriteField(key, v0)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
}
|
|
}
|
|
|
|
for key, val := range file {
|
|
fd, e := os.Open(val)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
defer fd.Close()
|
|
|
|
part, e := writer.CreateFormFile(key, filepath.Base(val))
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
_, err = io.Copy(part, fd)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
}
|
|
|
|
err = writer.Close()
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
return c.Post(p, "POST", bodyType, buf, int64(buf.Len()))
|
|
}
|
|
|
|
// SetReadTimeout sets the read timeout for future calls that read from the
|
|
// fcgi responder. A zero value for t means no timeout will be set.
|
|
func (c *client) SetReadTimeout(t time.Duration) error {
|
|
if t != 0 {
|
|
return c.rwc.SetReadDeadline(time.Now().Add(t))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetWriteTimeout sets the write timeout for future calls that send data to
|
|
// the fcgi responder. A zero value for t means no timeout will be set.
|
|
func (c *client) SetWriteTimeout(t time.Duration) error {
|
|
if t != 0 {
|
|
return c.rwc.SetWriteDeadline(time.Now().Add(t))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Checks whether chunked is part of the encodings stack
|
|
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }
|