mirror of
https://github.com/caddyserver/caddy.git
synced 2026-05-13 09:06:41 +00:00
caddyhttp: move WebTransport echo handler to integration tests
Francis pointed out in review of #7669 that the echo handler — which exists solely as a test upstream for the WebTransport reverse-proxy tests — should not be a full-fledged module registered in every Caddy binary. Mirroring the mockdns_test.go pattern, move it into a _test.go file under caddytest/integration/. The module ID http.handlers.webtransport is now registered only when the integration test binary is built, which is when caddytest/integration/webtransport_test.go references it by ID string in its JSON configs. Production Caddy builds no longer include it. Changes: * New file: caddytest/integration/webtransport_echo_test.go — contains the WebTransportEcho handler, its types and interface guards, the isWebTransportEchoUpgrade helper, and the unit tests that used to live in the deleted package's handler_test.go. * Deleted: modules/caddyhttp/webtransport/ (handler.go + handler_test.go). * Removed the blank import from modules/caddyhttp/standard/imports.go. The Protocol const and Writer interface that this package used to export were inlined into reverseproxy's own files in a preceding commit, so nothing else depends on the deleted package.
This commit is contained in:
parent
914b7a5e2b
commit
96c087d48f
4 changed files with 197 additions and 215 deletions
197
caddytest/integration/webtransport_echo_test.go
Normal file
197
caddytest/integration/webtransport_echo_test.go
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
// 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.
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/webtransport-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
// This file provides a terminating WebTransport handler used exclusively
|
||||
// as a test upstream for the WebTransport reverse-proxy integration
|
||||
// tests in webtransport_test.go. Keeping it in a _test.go file (mirroring
|
||||
// mockdns_test.go) means the http.handlers.webtransport module is only
|
||||
// registered in the integration test binary — it does not ship in
|
||||
// production Caddy builds.
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(WebTransportEcho{})
|
||||
}
|
||||
|
||||
// webtransportEchoProtocol is the :protocol pseudo-header value for an
|
||||
// HTTP/3 Extended CONNECT that establishes a WebTransport session.
|
||||
const webtransportEchoProtocol = "webtransport"
|
||||
|
||||
// webtransportEchoWriter is the naked HTTP/3 response-writer shape that
|
||||
// webtransport.Server.Upgrade type-asserts on.
|
||||
type webtransportEchoWriter interface {
|
||||
http.ResponseWriter
|
||||
http3.Settingser
|
||||
http3.HTTPStreamer
|
||||
}
|
||||
|
||||
// WebTransportEcho terminates an incoming WebTransport session and echoes
|
||||
// bytes on each accepted bidirectional stream. Registered as
|
||||
// `http.handlers.webtransport` in the integration test binary.
|
||||
type WebTransportEcho struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (WebTransportEcho) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.webtransport",
|
||||
New: func() caddy.Module { return new(WebTransportEcho) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the handler.
|
||||
func (h *WebTransportEcho) Provision(ctx caddy.Context) error {
|
||||
h.logger = ctx.Logger()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP upgrades the request to a WebTransport session and echoes
|
||||
// bytes on each accepted bidirectional stream. Non-WebTransport requests
|
||||
// are passed through to the next handler.
|
||||
func (h *WebTransportEcho) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
if !isWebTransportEchoUpgrade(r) {
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
srv, ok := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
|
||||
if !ok || srv == nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: caddyhttp.Server not in request context"))
|
||||
}
|
||||
wtServer, ok := srv.WebTransportServer().(*webtransport.Server)
|
||||
if !ok || wtServer == nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: HTTP/3 is not enabled on this server"))
|
||||
}
|
||||
|
||||
naked, ok := caddyhttp.UnwrapResponseWriterAs[webtransportEchoWriter](w)
|
||||
if !ok {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: underlying writer does not support WebTransport upgrade"))
|
||||
}
|
||||
|
||||
session, err := wtServer.Upgrade(naked, r)
|
||||
if err != nil {
|
||||
h.logger.Debug("webtransport upgrade failed", zap.Error(err))
|
||||
return caddyhttp.Error(http.StatusBadRequest,
|
||||
fmt.Errorf("webtransport upgrade: %w", err))
|
||||
}
|
||||
|
||||
h.echoStreams(session)
|
||||
return nil
|
||||
}
|
||||
|
||||
// echoStreams accepts bidirectional streams on session until the session
|
||||
// ends, and echoes bytes on each one.
|
||||
func (h *WebTransportEcho) echoStreams(session *webtransport.Session) {
|
||||
ctx := session.Context()
|
||||
for {
|
||||
str, err := session.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(s *webtransport.Stream) {
|
||||
// io.Copy from the stream back to itself echoes everything
|
||||
// received on this bidirectional stream. When the peer closes
|
||||
// its send side we observe EOF and close our send side too.
|
||||
if _, err := io.Copy(s, s); err != nil && h.logger != nil {
|
||||
h.logger.Debug("webtransport echo stream error", zap.Error(err))
|
||||
}
|
||||
_ = s.Close()
|
||||
}(str)
|
||||
}
|
||||
}
|
||||
|
||||
// isWebTransportEchoUpgrade reports whether r is an HTTP/3 Extended
|
||||
// CONNECT that requests a WebTransport session. The quic-go/http3 server
|
||||
// places the :protocol pseudo-header value in r.Proto for CONNECT requests.
|
||||
func isWebTransportEchoUpgrade(r *http.Request) bool {
|
||||
return r.ProtoMajor == 3 &&
|
||||
r.Method == http.MethodConnect &&
|
||||
r.Proto == webtransportEchoProtocol
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ caddy.Provisioner = (*WebTransportEcho)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*WebTransportEcho)(nil)
|
||||
)
|
||||
|
||||
// --- unit tests ------------------------------------------------------------
|
||||
|
||||
func TestIsWebTransportEchoUpgrade(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
proto string
|
||||
major int
|
||||
meth string
|
||||
want bool
|
||||
}{
|
||||
{"h3 connect webtransport", "webtransport", 3, http.MethodConnect, true},
|
||||
{"h3 connect websocket", "websocket", 3, http.MethodConnect, false},
|
||||
{"h2 connect webtransport", "webtransport", 2, http.MethodConnect, false},
|
||||
{"h3 GET", "HTTP/3.0", 3, http.MethodGet, false},
|
||||
{"h3 connect missing :protocol", "HTTP/3.0", 3, http.MethodConnect, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest(tc.meth, "/", nil)
|
||||
r.ProtoMajor = tc.major
|
||||
r.Proto = tc.proto
|
||||
if got := isWebTransportEchoUpgrade(r); got != tc.want {
|
||||
t.Errorf("isWebTransportEchoUpgrade = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// echoNextNoop is a stand-in for the next handler. It records whether it
|
||||
// was invoked, used to assert that non-WebTransport requests pass through.
|
||||
type echoNextNoop struct{ called bool }
|
||||
|
||||
func (n *echoNextNoop) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
n.called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestWebTransportEcho_PassesThroughNonWebTransportRequests(t *testing.T) {
|
||||
h := &WebTransportEcho{}
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
nx := &echoNextNoop{}
|
||||
if err := h.ServeHTTP(w, r, nx); err != nil {
|
||||
t.Fatalf("ServeHTTP returned error: %v", err)
|
||||
}
|
||||
if !nx.called {
|
||||
t.Error("expected next handler to be invoked for non-WebTransport request")
|
||||
}
|
||||
}
|
||||
|
|
@ -22,5 +22,4 @@ import (
|
|||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/rewrite"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/templates"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/tracing"
|
||||
_ "github.com/caddyserver/caddy/v2/modules/caddyhttp/webtransport"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
// 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.
|
||||
|
||||
// Package webtransport is an EXPERIMENTAL HTTP handler that terminates a
|
||||
// WebTransport session (draft-ietf-webtrans-http3) on top of Caddy's HTTP/3
|
||||
// server and echoes bytes on each bidirectional stream. It exists mainly as
|
||||
// a test upstream for the WebTransport reverse-proxy transport. Behavior
|
||||
// and configuration are subject to change without notice.
|
||||
package webtransport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/quic-go/quic-go/http3"
|
||||
"github.com/quic-go/webtransport-go"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/caddyserver/caddy/v2"
|
||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||
)
|
||||
|
||||
func init() {
|
||||
caddy.RegisterModule(Handler{})
|
||||
}
|
||||
|
||||
// Protocol is the :protocol pseudo-header value sent by a client that wants
|
||||
// to establish a WebTransport session over HTTP/3 Extended CONNECT.
|
||||
const Protocol = "webtransport"
|
||||
|
||||
// Writer is the interface satisfied by the naked HTTP/3 response writer.
|
||||
// webtransport.Server.Upgrade performs these assertions itself; callers
|
||||
// can use caddyhttp.UnwrapResponseWriterAs[Writer] to reach it past
|
||||
// Caddy's ResponseWriter wrapping chain before calling Upgrade.
|
||||
type Writer interface {
|
||||
http.ResponseWriter
|
||||
http3.Settingser
|
||||
http3.HTTPStreamer
|
||||
}
|
||||
|
||||
// Handler terminates an incoming WebTransport session and echoes bytes on
|
||||
// each bidirectional stream. EXPERIMENTAL: intended primarily as a test
|
||||
// upstream for the WebTransport reverse-proxy transport.
|
||||
type Handler struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// CaddyModule returns the Caddy module information.
|
||||
func (Handler) CaddyModule() caddy.ModuleInfo {
|
||||
return caddy.ModuleInfo{
|
||||
ID: "http.handlers.webtransport",
|
||||
New: func() caddy.Module { return new(Handler) },
|
||||
}
|
||||
}
|
||||
|
||||
// Provision sets up the handler.
|
||||
func (h *Handler) Provision(ctx caddy.Context) error {
|
||||
h.logger = ctx.Logger()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeHTTP upgrades the request to a WebTransport session and echoes bytes
|
||||
// on each accepted bidirectional stream. Non-WebTransport requests are
|
||||
// passed through to the next handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||
if !isWebTransportUpgrade(r) {
|
||||
return next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
srv, ok := r.Context().Value(caddyhttp.ServerCtxKey).(*caddyhttp.Server)
|
||||
if !ok || srv == nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: caddyhttp.Server not in request context"))
|
||||
}
|
||||
wtServer, ok := srv.WebTransportServer().(*webtransport.Server)
|
||||
if !ok || wtServer == nil {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: HTTP/3 is not enabled on this server"))
|
||||
}
|
||||
|
||||
naked, ok := caddyhttp.UnwrapResponseWriterAs[Writer](w)
|
||||
if !ok {
|
||||
return caddyhttp.Error(http.StatusInternalServerError,
|
||||
errors.New("webtransport: underlying writer does not support WebTransport upgrade"))
|
||||
}
|
||||
|
||||
session, err := wtServer.Upgrade(naked, r)
|
||||
if err != nil {
|
||||
h.logger.Debug("webtransport upgrade failed", zap.Error(err))
|
||||
return caddyhttp.Error(http.StatusBadRequest,
|
||||
fmt.Errorf("webtransport upgrade: %w", err))
|
||||
}
|
||||
|
||||
h.echoStreams(session)
|
||||
return nil
|
||||
}
|
||||
|
||||
// echoStreams accepts bidirectional streams on session until the session
|
||||
// ends, and echoes bytes on each one.
|
||||
func (h *Handler) echoStreams(session *webtransport.Session) {
|
||||
ctx := session.Context()
|
||||
for {
|
||||
str, err := session.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go func(s *webtransport.Stream) {
|
||||
// io.Copy from the stream back to itself echoes everything
|
||||
// received on this bidirectional stream. When the peer closes
|
||||
// its send side we observe EOF and close our send side too.
|
||||
if _, err := io.Copy(s, s); err != nil && h.logger != nil {
|
||||
h.logger.Debug("webtransport echo stream error", zap.Error(err))
|
||||
}
|
||||
_ = s.Close()
|
||||
}(str)
|
||||
}
|
||||
}
|
||||
|
||||
// isWebTransportUpgrade reports whether r is an HTTP/3 Extended CONNECT that
|
||||
// requests a WebTransport session. The quic-go/http3 server places the
|
||||
// :protocol pseudo-header value in r.Proto for CONNECT requests.
|
||||
func isWebTransportUpgrade(r *http.Request) bool {
|
||||
return r.ProtoMajor == 3 &&
|
||||
r.Method == http.MethodConnect &&
|
||||
r.Proto == Protocol
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ caddy.Provisioner = (*Handler)(nil)
|
||||
_ caddyhttp.MiddlewareHandler = (*Handler)(nil)
|
||||
)
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
// 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.
|
||||
|
||||
package webtransport
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsWebTransportUpgrade(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
proto string
|
||||
major int
|
||||
meth string
|
||||
want bool
|
||||
}{
|
||||
{"h3 connect webtransport", "webtransport", 3, http.MethodConnect, true},
|
||||
{"h3 connect websocket", "websocket", 3, http.MethodConnect, false},
|
||||
{"h2 connect webtransport", "webtransport", 2, http.MethodConnect, false},
|
||||
{"h3 GET", "HTTP/3.0", 3, http.MethodGet, false},
|
||||
{"h3 connect missing :protocol", "HTTP/3.0", 3, http.MethodConnect, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
r := httptest.NewRequest(tc.meth, "/", nil)
|
||||
r.ProtoMajor = tc.major
|
||||
r.Proto = tc.proto
|
||||
if got := isWebTransportUpgrade(r); got != tc.want {
|
||||
t.Errorf("isWebTransportUpgrade = %v, want %v", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// nextNoop is a stand-in for the next handler. It records whether it was
|
||||
// invoked, used to assert that non-WebTransport requests pass through.
|
||||
type nextNoop struct{ called bool }
|
||||
|
||||
func (n *nextNoop) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
|
||||
n.called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHandler_PassesThroughNonWebTransportRequests(t *testing.T) {
|
||||
h := &Handler{}
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
nx := &nextNoop{}
|
||||
if err := h.ServeHTTP(w, r, nx); err != nil {
|
||||
t.Fatalf("ServeHTTP returned error: %v", err)
|
||||
}
|
||||
if !nx.called {
|
||||
t.Error("expected next handler to be invoked for non-WebTransport request")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue