mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-05 06:56:55 +02:00
457 lines
14 KiB
Go
457 lines
14 KiB
Go
// Copyright 2025 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 (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"github.com/enetx/http/httptrace"
|
|
"net/url"
|
|
"sync"
|
|
)
|
|
|
|
// A ClientConn is a client connection to an HTTP server.
|
|
//
|
|
// Unlike a [Transport], a ClientConn represents a single connection.
|
|
// Most users should use a Transport rather than creating client connections directly.
|
|
type ClientConn struct {
|
|
cc genericClientConn
|
|
|
|
stateHookMu sync.Mutex
|
|
userStateHook func(*ClientConn)
|
|
stateHookRunning bool
|
|
lastAvailable int
|
|
lastInFlight int
|
|
lastClosed bool
|
|
}
|
|
|
|
// newClientConner is the interface implemented by HTTP/2 transports to create new client conns.
|
|
//
|
|
// The http package (this package) needs a way to ask the http2 package to
|
|
// create a client connection.
|
|
//
|
|
// Transport.TLSNextProto["h2"] contains a function which appears to do this,
|
|
// but for historical reasons it does not: The TLSNextProto function adds a
|
|
// *tls.Conn to the http2.Transport's connection pool and returns a RoundTripper
|
|
// which is backed by that connection pool. NewClientConn needs a way to get a
|
|
// single client connection out of the http2 package.
|
|
//
|
|
// The http2 package registers a RoundTripper with Transport.RegisterProtocol.
|
|
// If this RoundTripper implements newClientConner, then Transport.NewClientConn will use
|
|
// it to create new HTTP/2 client connections.
|
|
type newClientConner interface {
|
|
// NewClientConn creates a new client connection from a net.Conn.
|
|
//
|
|
// The RoundTripper returned by NewClientConn must implement genericClientConn.
|
|
// (We don't define NewClientConn as returning genericClientConn,
|
|
// because either we'd need to make genericClientConn an exported type
|
|
// or define it as a type alias. Neither is particularly appealing.)
|
|
//
|
|
// The state hook passed here is the internal state hook
|
|
// (ClientConn.maybeRunStateHook). The internal state hook calls
|
|
// the user state hook (if any), which is set by the user with
|
|
// ClientConn.SetStateHook.
|
|
//
|
|
// The client connection should arrange to call the internal state hook
|
|
// when the connection closes, when requests complete, and when the
|
|
// connection concurrency limit changes.
|
|
//
|
|
// The client connection must call the internal state hook when the connection state
|
|
// changes asynchronously, such as when a request completes.
|
|
//
|
|
// The internal state hook need not be called after synchronous changes to the state:
|
|
// Close, Reserve, Release, and RoundTrip calls which don't start a request
|
|
// do not need to call the hook.
|
|
//
|
|
// The general idea is that if we call (for example) Close,
|
|
// we know that the connection state has probably changed and we
|
|
// don't need the state hook to tell us that.
|
|
// However, if the connection closes asynchronously
|
|
// (because, for example, the other end of the conn closed it),
|
|
// the state hook needs to inform us.
|
|
NewClientConn(nc net.Conn, internalStateHook func()) (RoundTripper, error)
|
|
}
|
|
|
|
// genericClientConn is an interface implemented by HTTP/2 client conns
|
|
// returned from newClientConner.NewClientConn.
|
|
//
|
|
// See the newClientConner doc comment for more information.
|
|
type genericClientConn interface {
|
|
Close() error
|
|
Err() error
|
|
RoundTrip(req *Request) (*Response, error)
|
|
Reserve() error
|
|
Release()
|
|
Available() int
|
|
InFlight() int
|
|
}
|
|
|
|
// NewClientConn creates a new client connection to the given address.
|
|
//
|
|
// If scheme is "http", the connection is unencrypted.
|
|
// If scheme is "https", the connection uses TLS.
|
|
//
|
|
// The protocol used for the new connection is determined by the scheme,
|
|
// Transport.Protocols configuration field, and protocols supported by the
|
|
// server. See Transport.Protocols for more details.
|
|
//
|
|
// If Transport.Proxy is set and indicates that a request sent to the given
|
|
// address should use a proxy, the new connection uses that proxy.
|
|
//
|
|
// NewClientConn always creates a new connection,
|
|
// even if the Transport has an existing cached connection to the given host.
|
|
//
|
|
// The new connection is not added to the Transport's connection cache,
|
|
// and will not be used by [Transport.RoundTrip].
|
|
// It does not count against the MaxIdleConns and MaxConnsPerHost limits.
|
|
//
|
|
// The caller is responsible for closing the new connection.
|
|
func (t *Transport) NewClientConn(ctx context.Context, scheme, address string) (*ClientConn, error) {
|
|
t.nextProtoOnce.Do(t.onceSetNextProtoDefaults)
|
|
|
|
switch scheme {
|
|
case "http", "https":
|
|
default:
|
|
return nil, fmt.Errorf("net/http: invalid scheme %q", scheme)
|
|
}
|
|
|
|
host, port, err := net.SplitHostPort(address)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if port == "" {
|
|
port = schemePort(scheme)
|
|
}
|
|
|
|
var proxyURL *url.URL
|
|
if t.Proxy != nil {
|
|
// Transport.Proxy takes a *Request, so create a fake one to pass it.
|
|
req := &Request{
|
|
ctx: ctx,
|
|
Method: "GET",
|
|
URL: &url.URL{
|
|
Scheme: scheme,
|
|
Host: host,
|
|
Path: "/",
|
|
},
|
|
Proto: "HTTP/1.1",
|
|
ProtoMajor: 1,
|
|
ProtoMinor: 1,
|
|
Header: make(Header),
|
|
Body: NoBody,
|
|
Host: host,
|
|
}
|
|
var err error
|
|
proxyURL, err = t.Proxy(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
cm := connectMethod{
|
|
targetScheme: scheme,
|
|
targetAddr: net.JoinHostPort(host, port),
|
|
proxyURL: proxyURL,
|
|
}
|
|
|
|
// The state hook is a bit tricky:
|
|
// The persistConn has a state hook which calls ClientConn.maybeRunStateHook,
|
|
// which in turn calls the user-provided state hook (if any).
|
|
//
|
|
// ClientConn.maybeRunStateHook handles debouncing hook calls for both
|
|
// HTTP/1 and HTTP/2.
|
|
//
|
|
// Since there's no need to change the persistConn's hook, we set it at creation time.
|
|
cc := &ClientConn{}
|
|
const isClientConn = true
|
|
pconn, err := t.dialConn(ctx, cm, isClientConn, cc.maybeRunStateHook)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Note that cc.maybeRunStateHook may have been called
|
|
// in the short window between dialConn and now.
|
|
// This is fine.
|
|
cc.stateHookMu.Lock()
|
|
defer cc.stateHookMu.Unlock()
|
|
if pconn.alt != nil {
|
|
// If pconn.alt is set, this is a connection implemented in another package
|
|
// (probably x/net/http2) or the bundled copy in h2_bundle.go.
|
|
gc, ok := pconn.alt.(genericClientConn)
|
|
if !ok {
|
|
return nil, errors.New("http: NewClientConn returned something that is not a ClientConn")
|
|
}
|
|
cc.cc = gc
|
|
cc.lastAvailable = gc.Available()
|
|
} else {
|
|
// This is an HTTP/1 connection.
|
|
pconn.availch = make(chan struct{}, 1)
|
|
pconn.availch <- struct{}{}
|
|
cc.cc = http1ClientConn{pconn}
|
|
cc.lastAvailable = 1
|
|
}
|
|
return cc, nil
|
|
}
|
|
|
|
// Close closes the connection.
|
|
// Outstanding RoundTrip calls are interrupted.
|
|
func (cc *ClientConn) Close() error {
|
|
defer cc.maybeRunStateHook()
|
|
return cc.cc.Close()
|
|
}
|
|
|
|
// Err reports any fatal connection errors.
|
|
// It returns nil if the connection is usable.
|
|
// If it returns non-nil, the connection can no longer be used.
|
|
func (cc *ClientConn) Err() error {
|
|
return cc.cc.Err()
|
|
}
|
|
|
|
func validateClientConnRequest(req *Request) error {
|
|
if req.URL == nil {
|
|
return errors.New("http: nil Request.URL")
|
|
}
|
|
if req.Header == nil {
|
|
return errors.New("http: nil Request.Header")
|
|
}
|
|
// Validate the outgoing headers.
|
|
if err := validateHeaders(req.Header); err != "" {
|
|
return fmt.Errorf("http: invalid header %s", err)
|
|
}
|
|
// Validate the outgoing trailers too.
|
|
if err := validateHeaders(req.Trailer); err != "" {
|
|
return fmt.Errorf("http: invalid trailer %s", err)
|
|
}
|
|
if req.Method != "" && !validMethod(req.Method) {
|
|
return fmt.Errorf("http: invalid method %q", req.Method)
|
|
}
|
|
if req.URL.Host == "" {
|
|
return errors.New("http: no Host in request URL")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RoundTrip implements the [RoundTripper] interface.
|
|
//
|
|
// The request is sent on the client connection,
|
|
// regardless of the URL being requested or any proxy settings.
|
|
//
|
|
// If the connection is at its concurrency limit,
|
|
// RoundTrip waits for the connection to become available
|
|
// before sending the request.
|
|
func (cc *ClientConn) RoundTrip(req *Request) (*Response, error) {
|
|
defer cc.maybeRunStateHook()
|
|
if err := validateClientConnRequest(req); err != nil {
|
|
cc.Release()
|
|
return nil, err
|
|
}
|
|
return cc.cc.RoundTrip(req)
|
|
}
|
|
|
|
// Available reports the number of requests that may be sent
|
|
// to the connection without blocking.
|
|
// It returns 0 if the connection is closed.
|
|
func (cc *ClientConn) Available() int {
|
|
return cc.cc.Available()
|
|
}
|
|
|
|
// InFlight reports the number of requests in flight,
|
|
// including reserved requests.
|
|
// It returns 0 if the connection is closed.
|
|
func (cc *ClientConn) InFlight() int {
|
|
return cc.cc.InFlight()
|
|
}
|
|
|
|
// Reserve reserves a concurrency slot on the connection.
|
|
// If Reserve returns nil, one additional RoundTrip call may be made
|
|
// without waiting for an existing request to complete.
|
|
//
|
|
// The reserved concurrency slot is accounted as an in-flight request.
|
|
// A successful call to RoundTrip will decrement the Available count
|
|
// and increment the InFlight count.
|
|
//
|
|
// Each successful call to Reserve should be followed by exactly one call
|
|
// to RoundTrip or Release, which will consume or release the reservation.
|
|
//
|
|
// If the connection is closed or at its concurrency limit,
|
|
// Reserve returns an error.
|
|
func (cc *ClientConn) Reserve() error {
|
|
defer cc.maybeRunStateHook()
|
|
return cc.cc.Reserve()
|
|
}
|
|
|
|
// Release releases an unused concurrency slot reserved by Reserve.
|
|
// If there are no reserved concurrency slots, it has no effect.
|
|
func (cc *ClientConn) Release() {
|
|
defer cc.maybeRunStateHook()
|
|
cc.cc.Release()
|
|
}
|
|
|
|
// shouldRunStateHook returns the user's state hook if we should call it,
|
|
// or nil if we don't need to call it at this time.
|
|
func (cc *ClientConn) shouldRunStateHook(stopRunning bool) func(*ClientConn) {
|
|
cc.stateHookMu.Lock()
|
|
defer cc.stateHookMu.Unlock()
|
|
if cc.cc == nil {
|
|
return nil
|
|
}
|
|
if stopRunning {
|
|
cc.stateHookRunning = false
|
|
}
|
|
if cc.userStateHook == nil {
|
|
return nil
|
|
}
|
|
if cc.stateHookRunning {
|
|
return nil
|
|
}
|
|
var (
|
|
available = cc.Available()
|
|
inFlight = cc.InFlight()
|
|
closed = cc.Err() != nil
|
|
)
|
|
var hook func(*ClientConn)
|
|
if available > cc.lastAvailable || inFlight < cc.lastInFlight || closed != cc.lastClosed {
|
|
hook = cc.userStateHook
|
|
cc.stateHookRunning = true
|
|
}
|
|
cc.lastAvailable = available
|
|
cc.lastInFlight = inFlight
|
|
cc.lastClosed = closed
|
|
return hook
|
|
}
|
|
|
|
func (cc *ClientConn) maybeRunStateHook() {
|
|
hook := cc.shouldRunStateHook(false)
|
|
if hook == nil {
|
|
return
|
|
}
|
|
// Run the hook synchronously.
|
|
//
|
|
// This means that if, for example, the user calls resp.Body.Close to finish a request,
|
|
// the Close call will synchronously run the hook, giving the hook the chance to
|
|
// return the ClientConn to a connection pool before the next request is made.
|
|
hook(cc)
|
|
// The connection state may have changed while the hook was running,
|
|
// in which case we need to run it again.
|
|
//
|
|
// If we do need to run the hook again, do so in a new goroutine to avoid blocking
|
|
// the current goroutine indefinitely.
|
|
hook = cc.shouldRunStateHook(true)
|
|
if hook != nil {
|
|
go func() {
|
|
for hook != nil {
|
|
hook(cc)
|
|
hook = cc.shouldRunStateHook(true)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// SetStateHook arranges for f to be called when the state of the connection changes.
|
|
// At most one call to f is made at a time.
|
|
// If the connection's state has changed since it was created,
|
|
// f is called immediately in a separate goroutine.
|
|
// f may be called synchronously from RoundTrip or Response.Body.Close.
|
|
//
|
|
// If SetStateHook is called multiple times, the new hook replaces the old one.
|
|
// If f is nil, no further calls will be made to f after SetStateHook returns.
|
|
//
|
|
// f is called when Available increases (more requests may be sent on the connection),
|
|
// InFlight decreases (existing requests complete), or Err begins returning non-nil
|
|
// (the connection is no longer usable).
|
|
func (cc *ClientConn) SetStateHook(f func(*ClientConn)) {
|
|
cc.stateHookMu.Lock()
|
|
cc.userStateHook = f
|
|
cc.stateHookMu.Unlock()
|
|
cc.maybeRunStateHook()
|
|
}
|
|
|
|
// http1ClientConn is a genericClientConn implementation backed by
|
|
// an HTTP/1 *persistConn (pconn.alt is nil).
|
|
type http1ClientConn struct {
|
|
pconn *persistConn
|
|
}
|
|
|
|
func (cc http1ClientConn) RoundTrip(req *Request) (*Response, error) {
|
|
ctx := req.Context()
|
|
trace := httptrace.ContextClientTrace(ctx)
|
|
|
|
// Convert Request.Cancel into context cancelation.
|
|
ctx, cancel := context.WithCancelCause(req.Context())
|
|
if req.Cancel != nil {
|
|
go awaitLegacyCancel(ctx, cancel, req)
|
|
}
|
|
|
|
treq := &transportRequest{Request: req, trace: trace, ctx: ctx, cancel: cancel}
|
|
resp, err := cc.pconn.roundTrip(treq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Request = req
|
|
return resp, nil
|
|
}
|
|
|
|
func (cc http1ClientConn) Close() error {
|
|
cc.pconn.close(errors.New("ClientConn closed"))
|
|
return nil
|
|
}
|
|
|
|
func (cc http1ClientConn) Err() error {
|
|
select {
|
|
case <-cc.pconn.closech:
|
|
return cc.pconn.closed
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (cc http1ClientConn) Available() int {
|
|
cc.pconn.mu.Lock()
|
|
defer cc.pconn.mu.Unlock()
|
|
if cc.pconn.closed != nil || cc.pconn.reserved || cc.pconn.inFlight {
|
|
return 0
|
|
}
|
|
return 1
|
|
}
|
|
|
|
func (cc http1ClientConn) InFlight() int {
|
|
cc.pconn.mu.Lock()
|
|
defer cc.pconn.mu.Unlock()
|
|
if cc.pconn.closed == nil && (cc.pconn.reserved || cc.pconn.inFlight) {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (cc http1ClientConn) Reserve() error {
|
|
cc.pconn.mu.Lock()
|
|
defer cc.pconn.mu.Unlock()
|
|
if cc.pconn.closed != nil {
|
|
return cc.pconn.closed
|
|
}
|
|
select {
|
|
case <-cc.pconn.availch:
|
|
default:
|
|
return errors.New("connection is unavailable")
|
|
}
|
|
cc.pconn.reserved = true
|
|
return nil
|
|
}
|
|
|
|
func (cc http1ClientConn) Release() {
|
|
cc.pconn.mu.Lock()
|
|
defer cc.pconn.mu.Unlock()
|
|
if cc.pconn.reserved {
|
|
select {
|
|
case cc.pconn.availch <- struct{}{}:
|
|
default:
|
|
panic("cannot release reservation")
|
|
}
|
|
cc.pconn.reserved = false
|
|
}
|
|
}
|