mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 04:27:39 +03:00
296 lines
7.6 KiB
Go
296 lines
7.6 KiB
Go
package http
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/apernet/hysteria/core/v2/client"
|
|
)
|
|
|
|
const (
|
|
httpClientTimeout = 10 * time.Second
|
|
)
|
|
|
|
// Server is an HTTP server using a Hysteria client as outbound.
|
|
type Server struct {
|
|
HyClient client.Client
|
|
AuthFunc func(username, password string) bool // nil = no authentication
|
|
AuthRealm string
|
|
EventLogger EventLogger
|
|
|
|
httpClient *http.Client
|
|
}
|
|
|
|
type EventLogger interface {
|
|
ConnectRequest(addr net.Addr, reqAddr string)
|
|
ConnectError(addr net.Addr, reqAddr string, err error)
|
|
HTTPRequest(addr net.Addr, reqURL string)
|
|
HTTPError(addr net.Addr, reqURL string, err error)
|
|
}
|
|
|
|
func (s *Server) Serve(listener net.Listener) error {
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go s.dispatch(conn)
|
|
}
|
|
}
|
|
|
|
func (s *Server) dispatch(conn net.Conn) {
|
|
bufReader := bufio.NewReader(conn)
|
|
for {
|
|
req, err := http.ReadRequest(bufReader)
|
|
if err != nil {
|
|
// Connection error or invalid request
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
if s.AuthFunc != nil {
|
|
authOK := false
|
|
// Check the Proxy-Authorization header
|
|
pAuth := req.Header.Get("Proxy-Authorization")
|
|
if strings.HasPrefix(pAuth, "Basic ") {
|
|
userPass, err := base64.URLEncoding.DecodeString(pAuth[6:])
|
|
if err == nil {
|
|
userPassParts := strings.SplitN(string(userPass), ":", 2)
|
|
if len(userPassParts) == 2 {
|
|
authOK = s.AuthFunc(userPassParts[0], userPassParts[1])
|
|
}
|
|
}
|
|
}
|
|
if !authOK {
|
|
// Proxy authentication required
|
|
_ = sendProxyAuthRequired(conn, req, s.AuthRealm)
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
}
|
|
if req.Method == http.MethodConnect {
|
|
if bufReader.Buffered() > 0 {
|
|
// There is still data in the buffered reader.
|
|
// We need to get it out and put it into a cachedConn,
|
|
// so that handleConnect can read it.
|
|
data := make([]byte, bufReader.Buffered())
|
|
_, err := io.ReadFull(bufReader, data)
|
|
if err != nil {
|
|
// Read from buffer failed, is this possible?
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
cachedConn := &cachedConn{
|
|
Conn: conn,
|
|
Buffer: *bytes.NewBuffer(data),
|
|
}
|
|
s.handleConnect(cachedConn, req)
|
|
} else {
|
|
// No data in the buffered reader, we can just pass the original connection.
|
|
s.handleConnect(conn, req)
|
|
}
|
|
// handleConnect will take over the connection,
|
|
// i.e. it will not return until the connection is closed.
|
|
// When it returns, there will be no more requests from this connection,
|
|
// so we simply exit the loop.
|
|
return
|
|
} else {
|
|
// handleRequest on the other hand handles one request at a time,
|
|
// and returns when the request is done. It returns a bool indicating
|
|
// whether the connection should be kept alive, but itself never closes
|
|
// the connection.
|
|
keepAlive := s.handleRequest(conn, req)
|
|
if !keepAlive {
|
|
_ = conn.Close()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// cachedConn is a net.Conn wrapper that first Read()s from a buffer,
|
|
// and then from the underlying net.Conn when the buffer is drained.
|
|
type cachedConn struct {
|
|
net.Conn
|
|
Buffer bytes.Buffer
|
|
}
|
|
|
|
func (c *cachedConn) Read(b []byte) (int, error) {
|
|
if c.Buffer.Len() > 0 {
|
|
n, err := c.Buffer.Read(b)
|
|
if err == io.EOF {
|
|
// Buffer is drained, hide it from the caller
|
|
err = nil
|
|
}
|
|
return n, err
|
|
}
|
|
return c.Conn.Read(b)
|
|
}
|
|
|
|
func (s *Server) handleConnect(conn net.Conn, req *http.Request) {
|
|
defer conn.Close()
|
|
|
|
port := req.URL.Port()
|
|
if port == "" {
|
|
// HTTP defaults to port 80
|
|
port = "80"
|
|
}
|
|
reqAddr := net.JoinHostPort(req.URL.Hostname(), port)
|
|
|
|
// Connect request & error log
|
|
if s.EventLogger != nil {
|
|
s.EventLogger.ConnectRequest(conn.RemoteAddr(), reqAddr)
|
|
}
|
|
var closeErr error
|
|
defer func() {
|
|
if s.EventLogger != nil {
|
|
s.EventLogger.ConnectError(conn.RemoteAddr(), reqAddr, closeErr)
|
|
}
|
|
}()
|
|
|
|
// Dial
|
|
rConn, err := s.HyClient.TCP(reqAddr)
|
|
if err != nil {
|
|
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
|
|
closeErr = err
|
|
return
|
|
}
|
|
defer rConn.Close()
|
|
|
|
// Send 200 OK response and start relaying
|
|
_ = sendSimpleResponse(conn, req, http.StatusOK)
|
|
copyErrChan := make(chan error, 2)
|
|
go func() {
|
|
_, err := io.Copy(rConn, conn)
|
|
copyErrChan <- err
|
|
}()
|
|
go func() {
|
|
_, err := io.Copy(conn, rConn)
|
|
copyErrChan <- err
|
|
}()
|
|
closeErr = <-copyErrChan
|
|
}
|
|
|
|
func (s *Server) handleRequest(conn net.Conn, req *http.Request) bool {
|
|
// Some clients use Connection, some use Proxy-Connection
|
|
// https://www.oreilly.com/library/view/http-the-definitive/1565925092/re40.html
|
|
keepAlive := req.ProtoAtLeast(1, 1) &&
|
|
(strings.ToLower(req.Header.Get("Proxy-Connection")) == "keep-alive" ||
|
|
strings.ToLower(req.Header.Get("Connection")) == "keep-alive")
|
|
req.RequestURI = "" // Outgoing request should not have RequestURI
|
|
|
|
removeHopByHopHeaders(req.Header)
|
|
removeExtraHTTPHostPort(req)
|
|
|
|
if req.URL.Scheme == "" || req.URL.Host == "" {
|
|
_ = sendSimpleResponse(conn, req, http.StatusBadRequest)
|
|
return false
|
|
}
|
|
|
|
// Request & error log
|
|
if s.EventLogger != nil {
|
|
s.EventLogger.HTTPRequest(conn.RemoteAddr(), req.URL.String())
|
|
}
|
|
var closeErr error
|
|
defer func() {
|
|
if s.EventLogger != nil {
|
|
s.EventLogger.HTTPError(conn.RemoteAddr(), req.URL.String(), closeErr)
|
|
}
|
|
}()
|
|
|
|
if s.httpClient == nil {
|
|
s.initHTTPClient()
|
|
}
|
|
|
|
// Do the request and send the response back
|
|
resp, err := s.httpClient.Do(req)
|
|
if err != nil {
|
|
closeErr = err
|
|
_ = sendSimpleResponse(conn, req, http.StatusBadGateway)
|
|
return false
|
|
}
|
|
|
|
removeHopByHopHeaders(resp.Header)
|
|
if keepAlive {
|
|
resp.Header.Set("Connection", "keep-alive")
|
|
resp.Header.Set("Proxy-Connection", "keep-alive")
|
|
resp.Header.Set("Keep-Alive", "timeout=60")
|
|
}
|
|
|
|
closeErr = resp.Write(conn)
|
|
return closeErr == nil && keepAlive
|
|
}
|
|
|
|
func (s *Server) initHTTPClient() {
|
|
s.httpClient = &http.Client{
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
// HyClient doesn't support context for now
|
|
return s.HyClient.TCP(addr)
|
|
},
|
|
},
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
return http.ErrUseLastResponse
|
|
},
|
|
Timeout: httpClientTimeout,
|
|
}
|
|
}
|
|
|
|
func removeHopByHopHeaders(header http.Header) {
|
|
header.Del("Proxy-Connection") // Not in RFC but common
|
|
// https://www.ietf.org/rfc/rfc2616.txt
|
|
header.Del("Connection")
|
|
header.Del("Keep-Alive")
|
|
header.Del("Proxy-Authenticate")
|
|
header.Del("Proxy-Authorization")
|
|
header.Del("TE")
|
|
header.Del("Trailers")
|
|
header.Del("Transfer-Encoding")
|
|
header.Del("Upgrade")
|
|
}
|
|
|
|
func removeExtraHTTPHostPort(req *http.Request) {
|
|
host := req.Host
|
|
if host == "" {
|
|
host = req.URL.Host
|
|
}
|
|
if pHost, port, err := net.SplitHostPort(host); err == nil && port == "80" {
|
|
host = pHost
|
|
}
|
|
req.Host = host
|
|
req.URL.Host = host
|
|
}
|
|
|
|
// sendSimpleResponse sends a simple HTTP response with the given status code.
|
|
func sendSimpleResponse(conn net.Conn, req *http.Request, statusCode int) error {
|
|
resp := &http.Response{
|
|
StatusCode: statusCode,
|
|
Status: http.StatusText(statusCode),
|
|
Proto: req.Proto,
|
|
ProtoMajor: req.ProtoMajor,
|
|
ProtoMinor: req.ProtoMinor,
|
|
Header: http.Header{},
|
|
}
|
|
return resp.Write(conn)
|
|
}
|
|
|
|
// sendProxyAuthRequired sends a 407 Proxy Authentication Required response.
|
|
func sendProxyAuthRequired(conn net.Conn, req *http.Request, realm string) error {
|
|
resp := &http.Response{
|
|
StatusCode: http.StatusProxyAuthRequired,
|
|
Status: http.StatusText(http.StatusProxyAuthRequired),
|
|
Proto: req.Proto,
|
|
ProtoMajor: req.ProtoMajor,
|
|
ProtoMinor: req.ProtoMinor,
|
|
Header: http.Header{},
|
|
}
|
|
resp.Header.Set("Proxy-Authenticate", fmt.Sprintf("Basic realm=%q", realm))
|
|
return resp.Write(conn)
|
|
}
|