From ffb222f1ad9d9fb69e8c4c8929886d35044bccd1 Mon Sep 17 00:00:00 2001 From: Pat Whittingslow Date: Fri, 3 Jul 2026 17:04:17 -0300 Subject: [PATCH] more complete PR for adding more net support (#64) --- http/response.go | 6 ++ http/responsecontroller.go | 152 +++++++++++++++++++++++++++++++++++++ http/server.go | 17 +++++ http/transport.go | 108 +++++++++++++++++++++++++- interface.go | 5 ++ lookup.go | 23 ++++++ tlssock.go | 8 ++ unixsock.go | 32 ++++++++ 8 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 http/responsecontroller.go diff --git a/http/response.go b/http/response.go index 9bf59e3..aa034dc 100644 --- a/http/response.go +++ b/http/response.go @@ -14,6 +14,7 @@ package http import ( "bufio" "bytes" + "crypto/tls" "errors" "fmt" "io" @@ -117,6 +118,11 @@ type Response struct { // Request's Body is nil (having already been consumed). // This is only populated for Client requests. Request *Request + + // TLS contains information about the TLS connection on which the response + // was received. It is nil for unencrypted responses. TINYGO: populated only + // if a caller sets it; the wasm fetch path leaves it nil. + TLS *tls.ConnectionState } // Cookies parses and returns the cookies set in the Set-Cookie headers. diff --git a/http/responsecontroller.go b/http/responsecontroller.go new file mode 100644 index 0000000..bcf1ee1 --- /dev/null +++ b/http/responsecontroller.go @@ -0,0 +1,152 @@ +// TINYGO: Copied verbatim from the Go 1.26.2 official implementation to provide +// the ResponseController API used by net/http/httputil (reverse proxy). The +// underlying methods degrade to ErrNotSupported when the ResponseWriter does +// not implement them, which is the standard behavior. + +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package http + +import ( + "bufio" + "fmt" + "net" + "time" +) + +// A ResponseController is used by an HTTP handler to control the response. +// +// A ResponseController may not be used after the [Handler.ServeHTTP] method has returned. +type ResponseController struct { + rw ResponseWriter +} + +// NewResponseController creates a [ResponseController] for a request. +// +// The ResponseWriter should be the original value passed to the [Handler.ServeHTTP] method, +// or have an Unwrap method returning the original ResponseWriter. +// +// If the ResponseWriter implements any of the following methods, the ResponseController +// will call them as appropriate: +// +// Flush() +// FlushError() error // alternative Flush returning an error +// Hijack() (net.Conn, *bufio.ReadWriter, error) +// SetReadDeadline(deadline time.Time) error +// SetWriteDeadline(deadline time.Time) error +// EnableFullDuplex() error +// +// If the ResponseWriter does not support a method, ResponseController returns +// an error matching [ErrNotSupported]. +func NewResponseController(rw ResponseWriter) *ResponseController { + return &ResponseController{rw} +} + +type rwUnwrapper interface { + Unwrap() ResponseWriter +} + +// Flush flushes buffered data to the client. +func (c *ResponseController) Flush() error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ FlushError() error }: + return t.FlushError() + case Flusher: + t.Flush() + return nil + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// Hijack lets the caller take over the connection. +// See the [Hijacker] interface for details. +func (c *ResponseController) Hijack() (net.Conn, *bufio.ReadWriter, error) { + rw := c.rw + for { + switch t := rw.(type) { + case Hijacker: + return t.Hijack() + case rwUnwrapper: + rw = t.Unwrap() + default: + return nil, nil, errNotSupported() + } + } +} + +// SetReadDeadline sets the deadline for reading the entire request, including the body. +// Reads from the request body after the deadline has been exceeded will return an error. +// A zero value means no deadline. +// +// Setting the read deadline after it has been exceeded will not extend it. +func (c *ResponseController) SetReadDeadline(deadline time.Time) error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ SetReadDeadline(time.Time) error }: + return t.SetReadDeadline(deadline) + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// SetWriteDeadline sets the deadline for writing the response. +// Writes to the response body after the deadline has been exceeded will not block, +// but may succeed if the data has been buffered. +// A zero value means no deadline. +// +// Setting the write deadline after it has been exceeded will not extend it. +func (c *ResponseController) SetWriteDeadline(deadline time.Time) error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ SetWriteDeadline(time.Time) error }: + return t.SetWriteDeadline(deadline) + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// EnableFullDuplex indicates that the request handler will interleave reads from [Request.Body] +// with writes to the [ResponseWriter]. +// +// For HTTP/1 requests, the Go HTTP server by default consumes any unread portion of +// the request body before beginning to write the response, preventing handlers from +// concurrently reading from the request and writing the response. +// Calling EnableFullDuplex disables this behavior and permits handlers to continue to read +// from the request while concurrently writing the response. +// +// For HTTP/2 requests, the Go HTTP server always permits concurrent reads and responses. +func (c *ResponseController) EnableFullDuplex() error { + rw := c.rw + for { + switch t := rw.(type) { + case interface{ EnableFullDuplex() error }: + return t.EnableFullDuplex() + case rwUnwrapper: + rw = t.Unwrap() + default: + return errNotSupported() + } + } +} + +// errNotSupported returns an error that Is ErrNotSupported, +// but is not == to it. +func errNotSupported() error { + return fmt.Errorf("%w", ErrNotSupported) +} diff --git a/http/server.go b/http/server.go index a749c09..4f6510c 100644 --- a/http/server.go +++ b/http/server.go @@ -2779,6 +2779,23 @@ type Server struct { // value. ConnContext func(ctx context.Context, c net.Conn) context.Context + // TLSNextProto optionally specifies a function to take over + // ownership of the provided TLS connection when an ALPN + // protocol upgrade has occurred. The map key is the protocol + // name negotiated. The Handler argument should be used to + // handle HTTP requests and will initialize the Request's TLS + // and RemoteAddr if not already set. The connection is + // automatically closed when the function returns. + // If TLSNextProto is not nil, HTTP/2 support is not enabled + // automatically. + TLSNextProto map[string]func(*Server, *tls.Conn, Handler) + + // HTTP2 configures HTTP/2 connections. + // + // This field does not yet have any effect. + // See https://go.dev/issue/67813. + HTTP2 *HTTP2Config + inShutdown atomicBool // true when server is in shutdown disableKeepAlives int32 // accessed atomically. diff --git a/http/transport.go b/http/transport.go index 8544ed0..9b6f790 100644 --- a/http/transport.go +++ b/http/transport.go @@ -12,8 +12,14 @@ package http import ( + "context" + "crypto/tls" + "errors" "io" + "net" + "net/url" "sync/atomic" + "time" ) type readTrackingBody struct { @@ -22,6 +28,106 @@ type readTrackingBody struct { didClose atomic.Bool } -type Transport struct{} +// Transport is an HTTP/1.x RoundTripper. TINYGO: in the wasm/browser build the +// actual round trips are performed by the host fetch API (see roundtrip_js.go); +// these fields exist only to satisfy configuration by callers such as +// golang.org/x/net/http2 and google.golang.org/grpc. They are stored but, aside +// from fetch, have no effect at runtime. +type Transport struct { + // TLSClientConfig specifies the TLS configuration to use with tls.Client. + TLSClientConfig *tls.Config + + // TLSNextProto specifies how the Transport switches to an alternate + // protocol (such as HTTP/2) after a TLS ALPN protocol negotiation. + TLSNextProto map[string]func(authority string, c *tls.Conn) RoundTripper + + // DisableKeepAlives, if true, disables HTTP keep-alives. + DisableKeepAlives bool + + // DisableCompression, if true, prevents the Transport from requesting + // compression with an "Accept-Encoding: gzip" request header. + DisableCompression bool + + // IdleConnTimeout is the maximum amount of time an idle connection will + // remain idle before closing itself. Zero means no limit. + IdleConnTimeout time.Duration + + // ResponseHeaderTimeout, if non-zero, specifies the amount of time to wait + // for a server's response headers after fully writing the request. + ResponseHeaderTimeout time.Duration + + // ExpectContinueTimeout, if non-zero, specifies the amount of time to wait + // for a server's first response headers after fully writing the request + // headers if the request has an "Expect: 100-continue" header. + ExpectContinueTimeout time.Duration + + // MaxResponseHeaderBytes specifies a limit on how many response bytes are + // allowed in the server's response header. Zero means to use a default limit. + MaxResponseHeaderBytes int64 + + // MaxIdleConns controls the maximum number of idle (keep-alive) connections + // across all hosts. Zero means no limit. + MaxIdleConns int + + // MaxIdleConnsPerHost, if non-zero, controls the maximum idle (keep-alive) + // connections to keep per-host. + MaxIdleConnsPerHost int + + // MaxConnsPerHost optionally limits the total number of connections per host. + MaxConnsPerHost int + + // HTTP2 configures HTTP/2 connections. This field does not yet have any effect. + HTTP2 *HTTP2Config + + // Dial specifies the dial function for creating unencrypted TCP connections. + // + // Deprecated: Use DialContext instead. TINYGO: stored only; unused by the + // fetch-based round tripper. + Dial func(network, addr string) (net.Conn, error) + + // DialContext specifies the dial function for creating unencrypted TCP + // connections. TINYGO: honored by callers that dial through the Transport + // directly (e.g. the relay WebSocket transport), which is how netbird routes + // connections through the netstack in the browser. + DialContext func(ctx context.Context, network, addr string) (net.Conn, error) +} var DefaultTransport RoundTripper = &Transport{} + +// CloseIdleConnections closes any connections which were previously connected +// from previous requests but are now sitting idle. TINYGO: no-op. +func (t *Transport) CloseIdleConnections() {} + +// Clone returns a copy of t with its exported fields shared. TINYGO: the +// tinygo Transport carries only configuration (round trips go through the host +// fetch API), so a shallow struct copy with duplicated maps is sufficient. +func (t *Transport) Clone() *Transport { + t2 := *t + if t.TLSClientConfig != nil { + t2.TLSClientConfig = t.TLSClientConfig.Clone() + } + if t.TLSNextProto != nil { + npm := make(map[string]func(authority string, c *tls.Conn) RoundTripper, len(t.TLSNextProto)) + for k, v := range t.TLSNextProto { + npm[k] = v + } + t2.TLSNextProto = npm + } + return &t2 +} + +// ProxyFromEnvironment returns the URL of the proxy to use for a given request. +// TINYGO: the wasm build has no environment proxy configuration, so this always +// reports "no proxy" (nil URL, nil error), which callers treat as a direct +// connection. +func ProxyFromEnvironment(req *Request) (*url.URL, error) { + return nil, nil +} + +// ErrSkipAltProtocol is a sentinel error value defined by Transport.RegisterProtocol. +var ErrSkipAltProtocol = errors.New("net/http: skip alternate protocol") + +// RegisterProtocol registers a new protocol with scheme. TINYGO: no-op; the +// wasm build performs round trips via the host fetch API and does not dispatch +// on registered alternate protocols. +func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper) {} diff --git a/interface.go b/interface.go index 25bfb1f..c05ae36 100644 --- a/interface.go +++ b/interface.go @@ -109,3 +109,8 @@ func InterfaceAddrs() ([]Addr, error) { func InterfaceByIndex(index int) (*Interface, error) { return nil, errors.New("InterfaceByIndex not implemented") } + +// InterfaceByName returns the interface specified by name. +func InterfaceByName(name string) (*Interface, error) { + return nil, errors.New("InterfaceByName not implemented") +} diff --git a/lookup.go b/lookup.go index 7b77c26..5dbe657 100644 --- a/lookup.go +++ b/lookup.go @@ -124,5 +124,28 @@ func (r *Resolver) LookupPort(ctx context.Context, network, service string) (por return 0, errors.New("net:LookupPort not implemented") } +// An SRV represents a single DNS SRV record. +type SRV struct { + Target string + Port uint16 + Priority uint16 + Weight uint16 +} + +// LookupSRV tries to resolve an SRV query of the given service, protocol, and +// domain name. +// +// TINYGO: not implemented; netdev provides no SRV record lookup. +func (r *Resolver) LookupSRV(ctx context.Context, service, proto, name string) (string, []*SRV, error) { + return "", nil, errors.New("net:LookupSRV not implemented") +} + +// LookupTXT returns the DNS TXT records for the given domain name. +// +// TINYGO: not implemented; netdev provides no TXT record lookup. +func (r *Resolver) LookupTXT(ctx context.Context, name string) ([]string, error) { + return nil, errors.New("net:LookupTXT not implemented") +} + // errNoSuchHost is returned when the host lookup finds no matching records. var errNoSuchHost = errors.New("no such host") diff --git a/tlssock.go b/tlssock.go index c476f0d..f70875a 100644 --- a/tlssock.go +++ b/tlssock.go @@ -9,6 +9,7 @@ package net import ( + "context" "internal/itoa" "io" "net/netip" @@ -139,3 +140,10 @@ func (c *TLSConn) Handshake() error { panic("TLSConn.Handshake() not implemented") return nil } + +// HandshakeContext runs the client or server handshake protocol if it has not +// yet been run. TINYGO: TLS is offloaded to the network device; this exists to +// satisfy callers (e.g. pion/ice) that require the context-aware variant. +func (c *TLSConn) HandshakeContext(ctx context.Context) error { + return c.Handshake() +} diff --git a/unixsock.go b/unixsock.go index 5cd18f0..d9a24fb 100644 --- a/unixsock.go +++ b/unixsock.go @@ -6,6 +6,11 @@ package net +import ( + "errors" + "time" +) + // BUG(mikio): On JS, WASIP1 and Plan 9, methods and functions related // to UnixConn and UnixListener are not implemented. @@ -44,6 +49,33 @@ func (a *UnixAddr) opAddr() Addr { return a } +// UnixConn is an implementation of the Conn interface for connections to Unix +// domain sockets. +// +// TINYGO: Unix sockets are not implemented (the browser has no filesystem +// sockets). This type exists only to satisfy references and type assertions +// such as those in github.com/gliderlabs/ssh agent forwarding; all methods +// return an error. The corresponding code paths are never reached at runtime in +// the wasm build. +type UnixConn struct { + fd int + laddr *UnixAddr + raddr *UnixAddr +} + +var errUnixNotImplemented = errors.New("net: Unix sockets not implemented") + +func (c *UnixConn) Read(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Write(b []byte) (int, error) { return 0, errUnixNotImplemented } +func (c *UnixConn) Close() error { return errUnixNotImplemented } +func (c *UnixConn) CloseRead() error { return errUnixNotImplemented } +func (c *UnixConn) CloseWrite() error { return errUnixNotImplemented } +func (c *UnixConn) LocalAddr() Addr { return c.laddr } +func (c *UnixConn) RemoteAddr() Addr { return c.raddr } +func (c *UnixConn) SetDeadline(t time.Time) error { return errUnixNotImplemented } +func (c *UnixConn) SetReadDeadline(t time.Time) error { return errUnixNotImplemented } +func (c *UnixConn) SetWriteDeadline(t time.Time) error { return errUnixNotImplemented } + // ResolveUnixAddr returns an address of Unix domain socket end point. // // The network must be a Unix network name.