Compare commits

..

10 commits

Author SHA1 Message Date
b9ecf19980
Enable TLS 1.0/1.1 2025-12-04 23:25:35 -08:00
Artur H.
551f793700
caddyfile: Fix importing nested tokens for {block} (#7189) 2025-08-22 21:29:34 +00:00
Francis Lavoie
4564261d83
httpcaddyfile: Fix acme_dns regression (#7199) 2025-08-22 15:09:25 -06:00
Francis Lavoie
16fe83c7af
http: Make logger first, before TLS provisioning (#7198) 2025-08-22 14:24:08 -06:00
Matthew Holt
3723e89585
go.mod: Upgrade CertMagic to v0.24.0 2025-08-22 09:41:47 -06:00
WeidiDeng
14a63a26b9
caddyhttp: use the new http.Protocols to handle h1, h2 and h2c requests (#6961)
* use the new http.Protocols to handle h1, h2 and h2c requests

* fix lint

* keep ConnCtxKey for now

* fix handling for h2c

* check http version while reading the connection

* check if connection implements connectionStater when it should

* add comments about either h1 or h2 must be used in the listener

* fix if check

* return a net.Conn that implements connectionStater if applicable

* remove http/1.1 from alpn if h1 is disabled

* fix matching if only h1 is enabled

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-22 14:30:42 +00:00
WeidiDeng
67debd0e11
fileserver: set Range header for precompressed static files to force Content Length header to appear (#7042)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-22 08:23:13 -06:00
Luka T. Korošec
b9710c6af4
fileserver: Add a few doc lines about Etag file content (#7173)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-21 16:02:10 -06:00
Kévin Dunglas
493898d9bd
ci: set proper build tags in golangci and minor cleanup (#7183)
* ci: set proper build tags in golangci and minor cleanup

* clean

* review

---------

Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-21 20:43:38 +00:00
WeidiDeng
1c596e3c5a
reverse_proxy: use the new KeepAliveConfig to set probe interval (#7157)
Co-authored-by: Matt Holt <mholt@users.noreply.github.com>
2025-08-21 14:36:54 -06:00
21 changed files with 496 additions and 211 deletions

View file

@ -13,6 +13,7 @@ on:
- 2.*
env:
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
@ -110,7 +111,7 @@ jobs:
env:
CGO_ENABLED: 0
run: |
go build -tags nobadger,nomysql,nopgx -trimpath -ldflags="-w -s" -v
go build -trimpath -ldflags="-w -s" -v
- name: Smoke test Caddy
working-directory: ./cmd/caddy
@ -133,7 +134,7 @@ jobs:
# continue-on-error: true
run: |
# (go test -v -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out
go test -tags nobadger,nomysql,nopgx -v -coverprofile="cover-profile.out" -short -race ./...
go test -v -coverprofile="cover-profile.out" -short -race ./...
# echo "status=$?" >> $GITHUB_OUTPUT
# Relevant step if we reinvestigate publishing test/coverage reports
@ -190,7 +191,7 @@ jobs:
retries=3
exit_code=0
while ((retries > 0)); do
CGO_ENABLED=0 go test -p 1 -tags nobadger,nomysql,nopgx -v ./...
CGO_ENABLED=0 go test -p 1 -v ./...
exit_code=$?
if ((exit_code == 0)); then
break

View file

@ -11,6 +11,8 @@ on:
- 2.*
env:
GOFLAGS: '-tags=nobadger,nomysql,nopgx'
CGO_ENABLED: '0'
# https://github.com/actions/setup-go/issues/491
GOTOOLCHAIN: local
@ -74,11 +76,9 @@ jobs:
- name: Run Build
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goos == 'aix' && 'ppc64' || 'amd64' }}
shell: bash
continue-on-error: true
working-directory: ./cmd/caddy
run: |
GOOS=$GOOS GOARCH=$GOARCH go build -tags=nobadger,nomysql,nopgx -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null
run: go build -trimpath -o caddy-"$GOOS"-$GOARCH 2> /dev/null

View file

@ -2,6 +2,10 @@ version: "2"
run:
issues-exit-code: 1
tests: false
build-tags:
- nobadger
- nomysql
- nopgx
output:
formats:
text:

View file

@ -308,9 +308,9 @@ func (d *Dispenser) CountRemainingArgs() int {
}
// RemainingArgs loads any more arguments (tokens on the same line)
// into a slice and returns them. Open curly brace tokens also indicate
// the end of arguments, and the curly brace is not included in
// the return value nor is it loaded.
// into a slice of strings and returns them. Open curly brace tokens
// also indicate the end of arguments, and the curly brace is not
// included in the return value nor is it loaded.
func (d *Dispenser) RemainingArgs() []string {
var args []string
for d.NextArg() {
@ -320,9 +320,9 @@ func (d *Dispenser) RemainingArgs() []string {
}
// RemainingArgsRaw loads any more arguments (tokens on the same line,
// retaining quotes) into a slice and returns them. Open curly brace
// tokens also indicate the end of arguments, and the curly brace is
// not included in the return value nor is it loaded.
// retaining quotes) into a slice of strings and returns them.
// Open curly brace tokens also indicate the end of arguments,
// and the curly brace is not included in the return value nor is it loaded.
func (d *Dispenser) RemainingArgsRaw() []string {
var args []string
for d.NextArg() {
@ -331,6 +331,18 @@ func (d *Dispenser) RemainingArgsRaw() []string {
return args
}
// RemainingArgsAsTokens loads any more arguments (tokens on the same line)
// into a slice of Token-structs and returns them. Open curly brace tokens
// also indicate the end of arguments, and the curly brace is not included
// in the return value nor is it loaded.
func (d *Dispenser) RemainingArgsAsTokens() []Token {
var args []Token
for d.NextArg() {
args = append(args, d.Token())
}
return args
}
// NewFromNextSegment returns a new dispenser with a copy of
// the tokens from the current token until the end of the
// "directive" whether that be to the end of the line or

View file

@ -274,6 +274,66 @@ func TestDispenser_RemainingArgs(t *testing.T) {
}
}
func TestDispenser_RemainingArgsAsTokens(t *testing.T) {
input := `dir1 arg1 arg2 arg3
dir2 arg4 arg5
dir3 arg6 { arg7
dir4`
d := NewTestDispenser(input)
d.Next() // dir1
args := d.RemainingArgsAsTokens()
tokenTexts := make([]string, 0, len(args))
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg1", "arg2", "arg3"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // dir2
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg4", "arg5"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // dir3
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if expected := []string{"arg6"}; !reflect.DeepEqual(tokenTexts, expected) {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", expected, tokenTexts)
}
d.Next() // {
d.Next() // arg7
d.Next() // dir4
args = d.RemainingArgsAsTokens()
tokenTexts = tokenTexts[:0]
for _, arg := range args {
tokenTexts = append(tokenTexts, arg.Text)
}
if len(args) != 0 {
t.Errorf("RemainingArgsAsTokens(): Expected %v, got %v", []string{}, tokenTexts)
}
}
func TestDispenser_ArgErr_Err(t *testing.T) {
input := `dir1 {
}

View file

@ -379,28 +379,23 @@ func (p *parser) doImport(nesting int) error {
if len(blockTokens) > 0 {
// use such tokens to create a new dispenser, and then use it to parse each block
bd := NewDispenser(blockTokens)
// one iteration processes one sub-block inside the import
for bd.Next() {
// see if we can grab a key
var currentMappingKey string
if bd.Val() == "{" {
currentMappingKey := bd.Val()
if currentMappingKey == "{" {
return p.Err("anonymous blocks are not supported")
}
currentMappingKey = bd.Val()
currentMappingTokens := []Token{}
// read all args until end of line / {
if bd.NextArg() {
currentMappingTokens = append(currentMappingTokens, bd.Token())
for bd.NextArg() {
currentMappingTokens = append(currentMappingTokens, bd.Token())
}
// TODO(elee1766): we don't enter another mapping here because it's annoying to extract the { and } properly.
// maybe someone can do that in the future
} else {
// attempt to enter a block and add tokens to the currentMappingTokens
// load up all arguments (if there even are any)
currentMappingTokens := bd.RemainingArgsAsTokens()
// load up the entire block
for mappingNesting := bd.Nesting(); bd.NextBlock(mappingNesting); {
currentMappingTokens = append(currentMappingTokens, bd.Token())
}
}
blockMapping[currentMappingKey] = currentMappingTokens
}
}

View file

@ -18,6 +18,7 @@ import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
@ -884,6 +885,51 @@ func TestRejectsGlobalMatcher(t *testing.T) {
}
}
func TestRejectAnonymousImportBlock(t *testing.T) {
p := testParser(`
(site) {
http://{args[0]} https://{args[0]} {
{block}
}
}
import site test.domain {
{
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
`)
_, err := p.parseAll()
if err == nil {
t.Fatal("Expected an error, but got nil")
}
expected := "anonymous blocks are not supported"
if !strings.HasPrefix(err.Error(), "anonymous blocks are not supported") {
t.Errorf("Expected error to start with '%s' but got '%v'", expected, err)
}
}
func TestAcceptSiteImportWithBraces(t *testing.T) {
p := testParser(`
(site) {
http://{args[0]} https://{args[0]} {
{block}
}
}
import site test.domain {
reverse_proxy http://192.168.1.1:8080 {
header_up Host {host}
}
}
`)
_, err := p.parseAll()
if err != nil {
t.Errorf("Expected error to be nil but got '%v'", err)
}
}
func testParser(input string) parser {
return parser{Dispenser: NewTestDispenser(input)}
}

View file

@ -564,22 +564,23 @@ func fillInGlobalACMEDefaults(issuer certmagic.Issuer, options map[string]any) e
if globalACMECARoot != nil && !slices.Contains(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string)) {
acmeIssuer.TrustedRootsPEMFiles = append(acmeIssuer.TrustedRootsPEMFiles, globalACMECARoot.(string))
}
if globalACMEDNSok && (acmeIssuer.Challenges == nil || acmeIssuer.Challenges.DNS == nil) {
if globalACMEDNS == nil {
globalACMEDNS = options["dns"]
if globalACMEDNS == nil {
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
}
}
if globalACMEDNSok {
globalDNS := options["dns"]
if globalDNS != nil {
// If global `dns` is set, do NOT set provider in issuer, just set empty dns config
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: new(caddytls.DNSChallengeConfig),
DNS: &caddytls.DNSChallengeConfig{},
}
} else if globalACMEDNS != nil {
// Set a global DNS provider if `acme_dns` is set and `dns` is NOT set
acmeIssuer.Challenges = &caddytls.ChallengesConfig{
DNS: &caddytls.DNSChallengeConfig{
ProviderRaw: caddyconfig.JSONModuleObject(globalACMEDNS, "name", globalACMEDNS.(caddy.Module).CaddyModule().ID.Name(), nil),
},
}
} else {
return fmt.Errorf("acme_dns specified without DNS provider config, but no provider specified with 'dns' global option")
}
}
if globalACMEEAB != nil && acmeIssuer.ExternalAccount == nil {
acmeIssuer.ExternalAccount = globalACMEEAB.(*acme.EAB)

View file

@ -0,0 +1,68 @@
{
acme_dns mock foo
}
example.com {
respond "Hello World"
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"example.com"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"body": "Hello World",
"handler": "static_response"
}
]
}
]
}
],
"terminal": true
}
]
}
}
},
"tls": {
"automation": {
"policies": [
{
"issuers": [
{
"challenges": {
"dns": {
"provider": {
"name": "mock"
}
}
},
"module": "acme"
}
]
}
]
}
}
}
}

View file

@ -0,0 +1,9 @@
{
acme_dns
}
example.com {
respond "Hello World"
}
----------
acme_dns specified without DNS provider config, but no provider specified with 'dns' global option

View file

@ -0,0 +1,13 @@
(site) {
http://{args[0]} https://{args[0]} {
{block}
}
}
import site test.domain {
{
header_up Host {host}
header_up X-Real-IP {remote_host}
}
}
----------
anonymous blocks are not supported

View file

@ -0,0 +1,65 @@
(site) {
https://{args[0]} {
{block}
}
}
import site test.domain {
reverse_proxy http://192.168.1.1:8080 {
header_up Host {host}
}
}
----------
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [
":443"
],
"routes": [
{
"match": [
{
"host": [
"test.domain"
]
}
],
"handle": [
{
"handler": "subroute",
"routes": [
{
"handle": [
{
"handler": "reverse_proxy",
"headers": {
"request": {
"set": {
"Host": [
"{http.request.host}"
]
}
}
},
"upstreams": [
{
"dial": "192.168.1.1:8080"
}
]
}
]
}
]
}
],
"terminal": true
}
]
}
}
}
}
}

2
go.mod
View file

@ -8,7 +8,7 @@ require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/alecthomas/chroma/v2 v2.20.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.23.0
github.com/caddyserver/certmagic v0.24.0
github.com/caddyserver/zerossl v0.1.3
github.com/cloudflare/circl v1.6.1
github.com/dustin/go-humanize v1.0.1

4
go.sum
View file

@ -91,8 +91,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
github.com/caddyserver/certmagic v0.24.0 h1:EfXTWpxHAUKgDfOj6MHImJN8Jm4AMFfMT6ITuKhrDF0=
github.com/caddyserver/certmagic v0.24.0/go.mod h1:xPT7dC1DuHHnS2yuEQCEyks+b89sUkMENh8dJF+InLE=
github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=

View file

@ -28,7 +28,6 @@ import (
"go.uber.org/zap"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/modules/caddyevents"
@ -171,13 +170,15 @@ func (App) CaddyModule() caddy.ModuleInfo {
// Provision sets up the app.
func (app *App) Provision(ctx caddy.Context) error {
// store some references
app.logger = ctx.Logger()
app.ctx = ctx
// provision TLS and events apps
tlsAppIface, err := ctx.App("tls")
if err != nil {
return fmt.Errorf("getting tls app: %v", err)
}
app.tlsApp = tlsAppIface.(*caddytls.TLS)
app.ctx = ctx
app.logger = ctx.Logger()
eventsAppIface, err := ctx.App("events")
if err != nil {
@ -236,15 +237,6 @@ func (app *App) Provision(ctx caddy.Context) error {
for _, srvProtocol := range srv.Protocols {
srvProtocolsUnique[srvProtocol] = struct{}{}
}
_, h1ok := srvProtocolsUnique["h1"]
_, h2ok := srvProtocolsUnique["h2"]
_, h2cok := srvProtocolsUnique["h2c"]
// the Go standard library does not let us serve only HTTP/2 using
// http.Server; we would probably need to write our own server
if !h1ok && (h2ok || h2cok) {
return fmt.Errorf("server %s: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName)
}
if srv.ListenProtocols != nil {
if len(srv.ListenProtocols) != len(srv.Listen) {
@ -278,19 +270,6 @@ func (app *App) Provision(ctx caddy.Context) error {
}
}
lnProtocolsIncludeUnique := map[string]struct{}{}
for _, lnProtocol := range lnProtocolsInclude {
lnProtocolsIncludeUnique[lnProtocol] = struct{}{}
}
_, h1ok := lnProtocolsIncludeUnique["h1"]
_, h2ok := lnProtocolsIncludeUnique["h2"]
_, h2cok := lnProtocolsIncludeUnique["h2c"]
// check if any listener protocols contain h2 or h2c without h1
if !h1ok && (h2ok || h2cok) {
return fmt.Errorf("server %s, listener %d: cannot enable HTTP/2 or H2C without enabling HTTP/1.1; add h1 to protocols or remove h2/h2c", srvName, i)
}
srv.ListenProtocols[i] = lnProtocolsInclude
}
}
@ -448,6 +427,25 @@ func (app *App) Validate() error {
return nil
}
func removeTLSALPN(srv *Server, target string) {
for _, cp := range srv.TLSConnPolicies {
// the TLSConfig was already provisioned, so... manually remove it
for i, np := range cp.TLSConfig.NextProtos {
if np == target {
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
break
}
}
// remove it from the parent connection policy too, just to keep things tidy
for i, alpn := range cp.ALPN {
if alpn == target {
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
break
}
}
}
}
// Start runs the app. It finishes automatic HTTPS if enabled,
// including management of certificates.
func (app *App) Start() error {
@ -466,32 +464,37 @@ func (app *App) Start() error {
MaxHeaderBytes: srv.MaxHeaderBytes,
Handler: srv,
ErrorLog: serverLogger,
Protocols: new(http.Protocols),
ConnContext: func(ctx context.Context, c net.Conn) context.Context {
return context.WithValue(ctx, ConnCtxKey, c)
},
}
h2server := new(http2.Server)
// disable HTTP/2, which we enabled by default during provisioning
if !srv.protocol("h2") {
srv.server.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
for _, cp := range srv.TLSConnPolicies {
// the TLSConfig was already provisioned, so... manually remove it
for i, np := range cp.TLSConfig.NextProtos {
if np == "h2" {
cp.TLSConfig.NextProtos = append(cp.TLSConfig.NextProtos[:i], cp.TLSConfig.NextProtos[i+1:]...)
break
removeTLSALPN(srv, "h2")
}
if !srv.protocol("h1") {
removeTLSALPN(srv, "http/1.1")
}
// remove it from the parent connection policy too, just to keep things tidy
for i, alpn := range cp.ALPN {
if alpn == "h2" {
cp.ALPN = append(cp.ALPN[:i], cp.ALPN[i+1:]...)
break
// configure the http versions the server will serve
if srv.protocol("h1") {
srv.server.Protocols.SetHTTP1(true)
}
}
}
} else {
if srv.protocol("h2") || srv.protocol("h2c") {
// skip setting h2 because if NextProtos is present, it's list of alpn versions will take precedence.
// it will always be present because http2.ConfigureServer will populate that field
// enabling h2c because some listener wrapper will wrap the connection that is no longer *tls.Conn
// However, we need to handle the case that if the connection is h2c but h2c is not enabled. We identify
// this type of connection by checking if it's behind a TLS listener wrapper or if it implements tls.ConnectionState.
srv.server.Protocols.SetUnencryptedHTTP2(true)
// when h2c is enabled but h2 disabled, we already removed h2 from NextProtos
// the handshake will never succeed with h2
// http2.ConfigureServer will enable the server to handle both h2 and h2c
h2server := new(http2.Server)
//nolint:errcheck
http2.ConfigureServer(srv.server, h2server)
}
@ -501,11 +504,6 @@ func (app *App) Start() error {
tlsCfg := srv.TLSConnPolicies.TLSConfig(app.ctx)
srv.configureServer(srv.server)
// enable H2C if configured
if srv.protocol("h2c") {
srv.server.Handler = h2c.NewHandler(srv, h2server)
}
for lnIndex, lnAddr := range srv.Listen {
listenAddr, err := caddy.ParseNetworkAddress(lnAddr)
if err != nil {
@ -570,15 +568,13 @@ func (app *App) Start() error {
ln = srv.listenerWrappers[i].WrapListener(ln)
}
// handle http2 if use tls listener wrapper
if h2ok {
http2lnWrapper := &http2Listener{
// check if the connection is h2c
ln = &http2Listener{
useTLS: useTLS,
useH1: h1ok,
useH2: h2ok || h2cok,
Listener: ln,
server: srv.server,
h2server: h2server,
}
srv.h2listeners = append(srv.h2listeners, http2lnWrapper)
ln = http2lnWrapper
logger: app.logger,
}
// if binding to port 0, the OS chooses a port for us;
@ -596,12 +592,9 @@ func (app *App) Start() error {
srv.listeners = append(srv.listeners, ln)
// enable HTTP/1 if configured
if h1ok {
//nolint:errcheck
go srv.server.Serve(ln)
}
}
if h2ok && !useTLS {
// Can only serve h2 with TLS enabled
@ -756,25 +749,12 @@ func (app *App) Stop() error {
}
}
}
stopH2Listener := func(server *Server) {
defer finishedShutdown.Done()
startedShutdown.Done()
for i, s := range server.h2listeners {
if err := s.Shutdown(ctx); err != nil {
app.logger.Error("http2 listener shutdown",
zap.Error(err),
zap.Int("index", i))
}
}
}
for _, server := range app.Servers {
startedShutdown.Add(3)
finishedShutdown.Add(3)
startedShutdown.Add(2)
finishedShutdown.Add(2)
go stopServer(server)
go stopH3Server(server)
go stopH2Listener(server)
}
// block until all the goroutines have been run by the scheduler;

View file

@ -167,6 +167,8 @@ type FileServer struct {
// If set, file Etags will be read from sidecar files
// with any of these suffixes, instead of generating
// our own Etag.
// Keep in mind that the Etag values in the files have to be quoted as per RFC7232.
// See https://datatracker.ietf.org/doc/html/rfc7232#section-2.3 for a few examples.
EtagFileExtensions []string `json:"etag_file_extensions,omitempty"`
fsmap caddy.FileSystems
@ -455,7 +457,14 @@ func (fsrv *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request, next c
}
defer file.Close()
respHeader.Set("Content-Encoding", ae)
respHeader.Del("Accept-Ranges")
// stdlib won't set Content-Length if Content-Encoding is set.
// set Range header if it's not present will force Content-Length to be set
if r.Header.Get("Range") == "" {
r.Header.Set("Range", "bytes=0-")
// remove this header, because it is not part of the request
defer r.Header.Del("Range")
}
// try to get the etag from pre computed files if an etag suffix list was provided
if etag == "" && fsrv.EtagFileExtensions != nil {

View file

@ -1,102 +1,110 @@
package caddyhttp
import (
"context"
"crypto/tls"
weakrand "math/rand"
"io"
"net"
"net/http"
"sync/atomic"
"time"
"go.uber.org/zap"
"golang.org/x/net/http2"
)
// http2Listener wraps the listener to solve the following problems:
// 1. server h2 natively without using h2c hack when listener handles tls connection but
// don't return *tls.Conn
// 2. graceful shutdown. the shutdown logic is copied from stdlib http.Server, it's an extra maintenance burden but
// whatever, the shutdown logic maybe extracted to be used with h2c graceful shutdown. http2.Server supports graceful shutdown
// sending GO_AWAY frame to connected clients, but doesn't track connection status. It requires explicit call of http2.ConfigureServer
type http2Listener struct {
cnt uint64
net.Listener
server *http.Server
h2server *http2.Server
}
type connectionStateConn interface {
net.Conn
type connectionStater interface {
ConnectionState() tls.ConnectionState
}
// http2Listener wraps the listener to solve the following problems:
// 1. prevent genuine h2c connections from succeeding if h2c is not enabled
// and the connection doesn't implment connectionStater or the resulting NegotiatedProtocol
// isn't http2.
// This does allow a connection to pass as tls enabled even if it's not, listener wrappers
// can do this.
// 2. After wrapping the connection doesn't implement connectionStater, emit a warning so that listener
// wrapper authors will hopefully implement it.
// 3. check if the connection matches a specific http version. h2/h2c has a distinct preface.
type http2Listener struct {
useTLS bool
useH1 bool
useH2 bool
net.Listener
logger *zap.Logger
}
func (h *http2Listener) Accept() (net.Conn, error) {
for {
conn, err := h.Listener.Accept()
if err != nil {
return nil, err
}
if csc, ok := conn.(connectionStateConn); ok {
// *tls.Conn will return empty string because it's only populated after handshake is complete
if csc.ConnectionState().NegotiatedProtocol == http2.NextProtoTLS {
go h.serveHttp2(csc)
continue
}
_, isConnectionStater := conn.(connectionStater)
// emit a warning
if h.useTLS && !isConnectionStater {
h.logger.Warn("tls is enabled, but listener wrapper returns a connection that doesn't implement connectionStater")
} else if !h.useTLS && isConnectionStater {
h.logger.Warn("tls is disabled, but listener wrapper returns a connection that implements connectionStater")
}
// if both h1 and h2 are enabled, we don't need to check the preface
if h.useH1 && h.useH2 {
return conn, nil
}
// impossible both are false, either useH1 or useH2 must be true,
// or else the listener wouldn't be created
h2Conn := &http2Conn{
h2Expected: h.useH2,
Conn: conn,
}
if isConnectionStater {
return http2StateConn{h2Conn}, nil
}
return h2Conn, nil
}
func (h *http2Listener) serveHttp2(csc connectionStateConn) {
atomic.AddUint64(&h.cnt, 1)
h.runHook(csc, http.StateNew)
defer func() {
csc.Close()
atomic.AddUint64(&h.cnt, ^uint64(0))
h.runHook(csc, http.StateClosed)
}()
h.h2server.ServeConn(csc, &http2.ServeConnOpts{
Context: h.server.ConnContext(context.Background(), csc),
BaseConfig: h.server,
Handler: h.server.Handler,
})
type http2StateConn struct {
*http2Conn
}
const shutdownPollIntervalMax = 500 * time.Millisecond
func (h *http2Listener) Shutdown(ctx context.Context) error {
pollIntervalBase := time.Millisecond
nextPollInterval := func() time.Duration {
// Add 10% jitter.
//nolint:gosec
interval := pollIntervalBase + time.Duration(weakrand.Intn(int(pollIntervalBase/10)))
// Double and clamp for next time.
pollIntervalBase *= 2
if pollIntervalBase > shutdownPollIntervalMax {
pollIntervalBase = shutdownPollIntervalMax
}
return interval
func (conn http2StateConn) ConnectionState() tls.ConnectionState {
return conn.Conn.(connectionStater).ConnectionState()
}
timer := time.NewTimer(nextPollInterval())
defer timer.Stop()
for {
if atomic.LoadUint64(&h.cnt) == 0 {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
timer.Reset(nextPollInterval())
}
}
type http2Conn struct {
// current index where the preface should match,
// no matching is done if idx is >= len(http2.ClientPreface)
idx int
// whether the connection is expected to be h2/h2c
h2Expected bool
// log if one such connection is detected
logger *zap.Logger
net.Conn
}
func (h *http2Listener) runHook(conn net.Conn, state http.ConnState) {
if h.server.ConnState != nil {
h.server.ConnState(conn, state)
func (c *http2Conn) Read(p []byte) (int, error) {
if c.idx >= len(http2.ClientPreface) {
return c.Conn.Read(p)
}
n, err := c.Conn.Read(p)
for i := range n {
// first mismatch
if p[i] != http2.ClientPreface[c.idx] {
// close the connection if h2 is expected
if c.h2Expected {
c.logger.Debug("h1 connection detected, but h1 is not enabled")
_ = c.Conn.Close()
return 0, io.EOF
}
// no need to continue matching anymore
c.idx = len(http2.ClientPreface)
return n, err
}
c.idx++
// matching complete
if c.idx == len(http2.ClientPreface) && !c.h2Expected {
c.logger.Debug("h2/h2c connection detected, but h2/h2c is not enabled")
_ = c.Conn.Close()
return 0, io.EOF
}
}
return n, err
}

View file

@ -428,7 +428,19 @@ func (h *HTTPTransport) NewTransport(caddyCtx caddy.Context) (*http.Transport, e
}
if h.KeepAlive != nil {
// according to https://pkg.go.dev/net#Dialer.KeepAliveConfig,
// KeepAlive is ignored if KeepAliveConfig.Enable is true.
// If configured to 0, a system-dependent default is used.
// To disable tcp keepalive, choose a negative value,
// so KeepAliveConfig.Enable is false and KeepAlive is negative.
// This is different from http keepalive where a tcp connection
// can transfer multiple http requests/responses.
dialer.KeepAlive = time.Duration(h.KeepAlive.ProbeInterval)
dialer.KeepAliveConfig = net.KeepAliveConfig{
Enable: h.KeepAlive.ProbeInterval > 0,
Interval: time.Duration(h.KeepAlive.ProbeInterval),
}
if h.KeepAlive.Enabled != nil {
rt.DisableKeepAlives = !*h.KeepAlive.Enabled
}

View file

@ -248,7 +248,6 @@ type Server struct {
server *http.Server
h3server *http3.Server
h2listeners []*http2Listener
addresses []caddy.NetworkAddress
trustedProxies IPRangeSource
@ -266,11 +265,11 @@ type Server struct {
// ServeHTTP is the entry point for all HTTP requests.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// If there are listener wrappers that process tls connections but don't return a *tls.Conn, this field will be nil.
// TODO: Can be removed if https://github.com/golang/go/pull/56110 is ever merged.
// TODO: Scheduled to be removed later because https://github.com/golang/go/pull/56110 has been merged.
if r.TLS == nil {
// not all requests have a conn (like virtual requests) - see #5698
if conn, ok := r.Context().Value(ConnCtxKey).(net.Conn); ok {
if csc, ok := conn.(connectionStateConn); ok {
if csc, ok := conn.(connectionStater); ok {
r.TLS = new(tls.ConnectionState)
*r.TLS = csc.ConnectionState()
}
@ -1083,6 +1082,8 @@ const (
OriginalRequestCtxKey caddy.CtxKey = "original_request"
// For referencing underlying net.Conn
// This will eventually be deprecated and not used. To refer to the underlying connection, implement a middleware plugin
// that RegisterConnContext during provisioning.
ConnCtxKey caddy.CtxKey = "conn"
// For tracking whether the client is a trusted proxy

View file

@ -139,7 +139,7 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
}
tlsCfg := &tls.Config{
MinVersion: tls.VersionTLS12,
MinVersion: tls.VersionTLS10,
GetConfigForClient: getConfigForClient,
}

View file

@ -117,7 +117,10 @@ var defaultCurves = []tls.CurveID{
}
// SupportedProtocols is a map of supported protocols.
// Note that HTTP/2 only supports TLS 1.2 and higher.
var SupportedProtocols = map[string]uint16{
"tls1.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11,
"tls1.2": tls.VersionTLS12,
"tls1.3": tls.VersionTLS13,
}
@ -127,8 +130,6 @@ var SupportedProtocols = map[string]uint16{
var unsupportedProtocols = map[string]uint16{
//nolint:staticcheck
"ssl3.0": tls.VersionSSL30,
"tls1.0": tls.VersionTLS10,
"tls1.1": tls.VersionTLS11,
}
// publicKeyAlgorithms is the map of supported public key algorithms.