Xray-core/proxy/http/server.go
yuhan6665 017f53b5fc
Add session context outbounds as slice (#3356)
* Add session context outbounds as slice

slice is needed for dialer proxy where two outbounds work on top of each other
There are two sets of target addr for example
It also enable Xtls to correctly do splice copy by checking both outbounds are ready to do direct copy

* Fill outbound tag info

* Splice now checks capalibility from all outbounds

* Fix unit tests
2024-05-13 21:52:24 -04:00

333 lines
9.3 KiB
Go

package http
import (
"bufio"
"context"
"encoding/base64"
"io"
"net/http"
"strings"
"time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/log"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
http_proto "github.com/xtls/xray-core/common/protocol/http"
"github.com/xtls/xray-core/common/session"
"github.com/xtls/xray-core/common/signal"
"github.com/xtls/xray-core/common/task"
"github.com/xtls/xray-core/core"
"github.com/xtls/xray-core/features/policy"
"github.com/xtls/xray-core/features/routing"
"github.com/xtls/xray-core/transport/internet/stat"
)
// Server is an HTTP proxy server.
type Server struct {
config *ServerConfig
policyManager policy.Manager
}
// NewServer creates a new HTTP inbound handler.
func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) {
v := core.MustFromContext(ctx)
s := &Server{
config: config,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}
return s, nil
}
func (s *Server) policy() policy.Session {
config := s.config
p := s.policyManager.ForLevel(config.UserLevel)
if config.Timeout > 0 && config.UserLevel == 0 {
p.Timeouts.ConnectionIdle = time.Duration(config.Timeout) * time.Second
}
return p
}
// Network implements proxy.Inbound.
func (*Server) Network() []net.Network {
return []net.Network{net.Network_TCP, net.Network_UNIX}
}
func isTimeout(err error) bool {
nerr, ok := errors.Cause(err).(net.Error)
return ok && nerr.Timeout()
}
func parseBasicAuth(auth string) (username, password string, ok bool) {
const prefix = "Basic "
if !strings.HasPrefix(auth, prefix) {
return
}
c, err := base64.StdEncoding.DecodeString(auth[len(prefix):])
if err != nil {
return
}
cs := string(c)
s := strings.IndexByte(cs, ':')
if s < 0 {
return
}
return cs[:s], cs[s+1:], true
}
type readerOnly struct {
io.Reader
}
func (s *Server) Process(ctx context.Context, network net.Network, conn stat.Connection, dispatcher routing.Dispatcher) error {
inbound := session.InboundFromContext(ctx)
inbound.Name = "http"
inbound.CanSpliceCopy = 2
inbound.User = &protocol.MemoryUser{
Level: s.config.UserLevel,
}
reader := bufio.NewReaderSize(readerOnly{conn}, buf.Size)
Start:
if err := conn.SetReadDeadline(time.Now().Add(s.policy().Timeouts.Handshake)); err != nil {
newError("failed to set read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
request, err := http.ReadRequest(reader)
if err != nil {
trace := newError("failed to read http request").Base(err)
if errors.Cause(err) != io.EOF && !isTimeout(errors.Cause(err)) {
trace.AtWarning()
}
return trace
}
if len(s.config.Accounts) > 0 {
user, pass, ok := parseBasicAuth(request.Header.Get("Proxy-Authorization"))
if !ok || !s.config.HasAccount(user, pass) {
return common.Error2(conn.Write([]byte("HTTP/1.1 407 Proxy Authentication Required\r\nProxy-Authenticate: Basic realm=\"proxy\"\r\n\r\n")))
}
if inbound != nil {
inbound.User.Email = user
}
}
newError("request to Method [", request.Method, "] Host [", request.Host, "] with URL [", request.URL, "]").WriteToLog(session.ExportIDToError(ctx))
if err := conn.SetReadDeadline(time.Time{}); err != nil {
newError("failed to clear read deadline").Base(err).WriteToLog(session.ExportIDToError(ctx))
}
defaultPort := net.Port(80)
if strings.EqualFold(request.URL.Scheme, "https") {
defaultPort = net.Port(443)
}
host := request.Host
if host == "" {
host = request.URL.Host
}
dest, err := http_proto.ParseHost(host, defaultPort)
if err != nil {
return newError("malformed proxy host: ", host).AtWarning().Base(err)
}
ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{
From: conn.RemoteAddr(),
To: request.URL,
Status: log.AccessAccepted,
Reason: "",
})
if strings.EqualFold(request.Method, "CONNECT") {
return s.handleConnect(ctx, request, reader, conn, dest, dispatcher, inbound)
}
keepAlive := (strings.TrimSpace(strings.ToLower(request.Header.Get("Proxy-Connection"))) == "keep-alive")
err = s.handlePlainHTTP(ctx, request, conn, dest, dispatcher)
if err == errWaitAnother {
if keepAlive {
goto Start
}
err = nil
}
return err
}
func (s *Server) handleConnect(ctx context.Context, _ *http.Request, reader *bufio.Reader, conn stat.Connection, dest net.Destination, dispatcher routing.Dispatcher, inbound *session.Inbound) error {
_, err := conn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
if err != nil {
return newError("failed to write back OK response").Base(err)
}
plcy := s.policy()
ctx, cancel := context.WithCancel(ctx)
timer := signal.CancelAfterInactivity(ctx, cancel, plcy.Timeouts.ConnectionIdle)
if inbound != nil {
inbound.Timer = timer
}
ctx = policy.ContextWithBufferPolicy(ctx, plcy.Buffer)
link, err := dispatcher.Dispatch(ctx, dest)
if err != nil {
return err
}
if reader.Buffered() > 0 {
payload, err := buf.ReadFrom(io.LimitReader(reader, int64(reader.Buffered())))
if err != nil {
return err
}
if err := link.Writer.WriteMultiBuffer(payload); err != nil {
return err
}
reader = nil
}
requestDone := func() error {
defer timer.SetTimeout(plcy.Timeouts.DownlinkOnly)
return buf.Copy(buf.NewReader(conn), link.Writer, buf.UpdateActivity(timer))
}
responseDone := func() error {
defer timer.SetTimeout(plcy.Timeouts.UplinkOnly)
v2writer := buf.NewWriter(conn)
if err := buf.Copy(link.Reader, v2writer, buf.UpdateActivity(timer)); err != nil {
return err
}
return nil
}
closeWriter := task.OnSuccess(requestDone, task.Close(link.Writer))
if err := task.Run(ctx, closeWriter, responseDone); err != nil {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return newError("connection ends").Base(err)
}
return nil
}
var errWaitAnother = newError("keep alive")
func (s *Server) handlePlainHTTP(ctx context.Context, request *http.Request, writer io.Writer, dest net.Destination, dispatcher routing.Dispatcher) error {
if !s.config.AllowTransparent && request.URL.Host == "" {
// RFC 2068 (HTTP/1.1) requires URL to be absolute URL in HTTP proxy.
response := &http.Response{
Status: "Bad Request",
StatusCode: 400,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header(make(map[string][]string)),
Body: nil,
ContentLength: 0,
Close: true,
}
response.Header.Set("Proxy-Connection", "close")
response.Header.Set("Connection", "close")
return response.Write(writer)
}
if len(request.URL.Host) > 0 {
request.Host = request.URL.Host
}
http_proto.RemoveHopByHopHeaders(request.Header)
// Prevent UA from being set to golang's default ones
if request.Header.Get("User-Agent") == "" {
request.Header.Set("User-Agent", "")
}
content := &session.Content{
Protocol: "http/1.1",
}
content.SetAttribute(":method", strings.ToUpper(request.Method))
content.SetAttribute(":path", request.URL.Path)
for key := range request.Header {
value := request.Header.Get(key)
content.SetAttribute(strings.ToLower(key), value)
}
ctx = session.ContextWithContent(ctx, content)
link, err := dispatcher.Dispatch(ctx, dest)
if err != nil {
return err
}
// Plain HTTP request is not a stream. The request always finishes before response. Hense request has to be closed later.
defer common.Close(link.Writer)
var result error = errWaitAnother
requestDone := func() error {
request.Header.Set("Connection", "close")
requestWriter := buf.NewBufferedWriter(link.Writer)
common.Must(requestWriter.SetBuffered(false))
if err := request.Write(requestWriter); err != nil {
return newError("failed to write whole request").Base(err).AtWarning()
}
return nil
}
responseDone := func() error {
responseReader := bufio.NewReaderSize(&buf.BufferedReader{Reader: link.Reader}, buf.Size)
response, err := http.ReadResponse(responseReader, request)
if err == nil {
http_proto.RemoveHopByHopHeaders(response.Header)
if response.ContentLength >= 0 {
response.Header.Set("Proxy-Connection", "keep-alive")
response.Header.Set("Connection", "keep-alive")
response.Header.Set("Keep-Alive", "timeout=4")
response.Close = false
} else {
response.Close = true
result = nil
}
defer response.Body.Close()
} else {
newError("failed to read response from ", request.Host).Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx))
response = &http.Response{
Status: "Service Unavailable",
StatusCode: 503,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: http.Header(make(map[string][]string)),
Body: nil,
ContentLength: 0,
Close: true,
}
response.Header.Set("Connection", "close")
response.Header.Set("Proxy-Connection", "close")
}
if err := response.Write(writer); err != nil {
return newError("failed to write response").Base(err).AtWarning()
}
return nil
}
if err := task.Run(ctx, requestDone, responseDone); err != nil {
common.Interrupt(link.Reader)
common.Interrupt(link.Writer)
return newError("connection ends").Base(err)
}
return result
}
func init() {
common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) {
return NewServer(ctx, config.(*ServerConfig))
}))
}