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 }