/ components / ingress / pkg / proxy / proxy.go
proxy.go
  1  // Copyright 2025 Alibaba Group Holding Ltd.
  2  //
  3  // Licensed under the Apache License, Version 2.0 (the "License");
  4  // you may not use this file except in compliance with the License.
  5  // You may obtain a copy of the License at
  6  //
  7  //     http://www.apache.org/licenses/LICENSE-2.0
  8  //
  9  // Unless required by applicable law or agreed to in writing, software
 10  // distributed under the License is distributed on an "AS IS" BASIS,
 11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  // See the License for the specific language governing permissions and
 13  // limitations under the License.
 14  
 15  package proxy
 16  
 17  import (
 18  	"context"
 19  	"errors"
 20  	"fmt"
 21  	"net"
 22  	"net/http"
 23  	"strings"
 24  
 25  	"github.com/alibaba/opensandbox/ingress/pkg/renewintent"
 26  	"github.com/alibaba/opensandbox/ingress/pkg/sandbox"
 27  	"github.com/alibaba/opensandbox/ingress/pkg/signature"
 28  	slogger "github.com/alibaba/opensandbox/internal/logger"
 29  )
 30  
 31  type Proxy struct {
 32  	sandboxProvider      sandbox.Provider
 33  	mode                 Mode
 34  	renewIntentPublisher renewintent.Publisher
 35  
 36  	secure *signature.Verifier
 37  }
 38  
 39  func NewProxy(_ context.Context, sandboxProvider sandbox.Provider, mode Mode, renewIntentPublisher renewintent.Publisher, secure *signature.Verifier) *Proxy {
 40  	return &Proxy{
 41  		sandboxProvider:      sandboxProvider,
 42  		mode:                 mode,
 43  		renewIntentPublisher: renewIntentPublisher,
 44  		secure:               secure,
 45  	}
 46  }
 47  
 48  func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 49  	defer func() {
 50  		if err := recover(); err != nil {
 51  			Logger.With(slogger.Field{Key: "error", Value: err}).Errorf("Proxy: proxy causes panic")
 52  			var errMsg string
 53  			if e, ok := err.(error); ok {
 54  				errMsg = e.Error()
 55  			} else {
 56  				errMsg = fmt.Sprintf("%v", err)
 57  			}
 58  			http.Error(w, errMsg, http.StatusBadGateway)
 59  		}
 60  	}()
 61  
 62  	host, status, err := p.getSandboxHostDefinition(r)
 63  	if err != nil {
 64  		if status == 0 {
 65  			status = http.StatusBadRequest
 66  		}
 67  		http.Error(w, fmt.Sprintf("OpenSandbox Ingress: %v", err), status)
 68  		return
 69  	}
 70  
 71  	targetHost, err, code := p.resolveRealHost(host)
 72  	if err != nil {
 73  		http.Error(w, fmt.Sprintf("OpenSandbox Ingress: %v", err), code)
 74  		return
 75  	}
 76  
 77  	if p.renewIntentPublisher != nil {
 78  		p.renewIntentPublisher.PublishIntent(host.ingressKey, host.port, host.requestURI)
 79  	}
 80  
 81  	// modify if requestURI is not empty
 82  	if host.requestURI != "" {
 83  		r.URL.Path = host.requestURI
 84  	}
 85  
 86  	r.Host = targetHost
 87  	r.URL.Host = targetHost
 88  	r.Header.Del(SandboxIngress)
 89  	r.Header.Del(signature.OpenSandboxSecureAccessCanonical)
 90  
 91  	Logger.With(
 92  		slogger.Field{Key: "target", Value: targetHost},
 93  		slogger.Field{Key: "client", Value: p.getClientIP(r)},
 94  		slogger.Field{Key: "uri", Value: r.RequestURI},
 95  		slogger.Field{Key: "method", Value: r.Method},
 96  	).Infof("ingress requested")
 97  	p.serve(w, r)
 98  }
 99  
100  func (p *Proxy) serve(w http.ResponseWriter, r *http.Request) {
101  	if p.isWebSocketRequest(r) {
102  		if r.URL == nil {
103  			http.Error(w, "invalid request URL", http.StatusBadRequest)
104  			return
105  		}
106  
107  		if r.URL.Scheme == "" {
108  			if r.TLS != nil {
109  				r.URL.Scheme = "wss"
110  			} else {
111  				r.URL.Scheme = "ws"
112  			}
113  		}
114  		NewWebSocketProxy(r.URL).ServeHTTP(w, r)
115  	} else {
116  		if r.URL.Scheme == "" {
117  			if r.TLS != nil {
118  				r.URL.Scheme = "https"
119  			} else {
120  				r.URL.Scheme = "http"
121  			}
122  		}
123  		NewHTTPProxy().ServeHTTP(w, r)
124  	}
125  }
126  
127  func (p *Proxy) isWebSocketRequest(r *http.Request) bool {
128  	if r.Method != http.MethodGet {
129  		return false
130  	}
131  	if r.Header.Get("Upgrade") != "websocket" {
132  		return false
133  	}
134  	if r.Header.Get("Connection") != "Upgrade" {
135  		return false
136  	}
137  	return true
138  }
139  
140  func (p *Proxy) resolveRealHost(host *sandboxHost) (string, error, int) {
141  	endpoint := host.endpoint
142  	if endpoint == "" {
143  		// Fallback lookup (should rarely happen because host parsing now fills endpoint).
144  		info, err := p.sandboxProvider.GetEndpoint(host.ingressKey)
145  		if err != nil {
146  			// Map sandbox errors to HTTP status codes
147  			switch {
148  			case errors.Is(err, sandbox.ErrSandboxNotFound):
149  				return "", err, http.StatusNotFound
150  			case errors.Is(err, sandbox.ErrSandboxNotReady):
151  				return "", err, http.StatusServiceUnavailable
152  			default:
153  				return "", err, http.StatusBadGateway
154  			}
155  		}
156  		endpoint = info.Endpoint
157  	}
158  
159  	// Construct target host with port
160  	targetHost := fmt.Sprintf("%s:%d", endpoint, host.port)
161  	return targetHost, nil, 0
162  }
163  
164  func (p *Proxy) getClientIP(r *http.Request) string {
165  	clientIP, _, _ := net.SplitHostPort(r.RemoteAddr)
166  	if len(r.Header.Get(XForwardedFor)) != 0 {
167  		xff := r.Header.Get(XForwardedFor)
168  		s := strings.Index(xff, ", ")
169  		if s == -1 {
170  			s = len(r.Header.Get(XForwardedFor))
171  		}
172  		clientIP = xff[:s]
173  	} else if len(r.Header.Get(XRealIP)) != 0 {
174  		clientIP = r.Header.Get(XRealIP)
175  	}
176  
177  	return clientIP
178  }