Skip to content

Commit d3ebdf2

Browse files
committed
orchestrator session http proxy
1 parent cbdc101 commit d3ebdf2

File tree

2 files changed

+187
-3
lines changed

2 files changed

+187
-3
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
package proxy
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"encoding/json"
7+
"fmt"
8+
"github.com/e2b-dev/infra/packages/shared/pkg/meters"
9+
"github.com/e2b-dev/infra/packages/shared/pkg/smap"
10+
"go.uber.org/zap"
11+
"net/http"
12+
"net/http/httputil"
13+
"net/url"
14+
"strconv"
15+
"strings"
16+
"time"
17+
)
18+
19+
//go:embed proxy_browser_502.html
20+
var proxyBrowser502PageHtml string
21+
22+
var browserIdentityKeywords = []string{
23+
"mozilla", "chrome", "safari", "firefox", "edge", "opera", "msie",
24+
}
25+
26+
type SessionProxy struct {
27+
sandboxes *smap.Map[string]
28+
server *http.Server
29+
}
30+
31+
func New(port uint) *SessionProxy {
32+
server := &http.Server{Addr: fmt.Sprintf(":%d", port)}
33+
34+
return &SessionProxy{
35+
server: server,
36+
sandboxes: smap.New[string](),
37+
}
38+
}
39+
40+
func (p *SessionProxy) AddSandbox(sandboxID, ip string) {
41+
p.sandboxes.Insert(sandboxID, ip)
42+
}
43+
44+
func (p *SessionProxy) RemoveSandbox(sandboxID string) {
45+
p.sandboxes.Remove(sandboxID)
46+
}
47+
48+
func (p *SessionProxy) Start() error {
49+
// similar values to our old the nginx configuration
50+
serverTransport := &http.Transport{
51+
Proxy: http.ProxyFromEnvironment,
52+
MaxIdleConns: 1024, // Matches worker_connections
53+
MaxIdleConnsPerHost: 8192, // Matches keepalive_requests
54+
IdleConnTimeout: 620 * time.Second, // Matches keepalive_timeout
55+
TLSHandshakeTimeout: 10 * time.Second, // Similar to client_header_timeout
56+
ResponseHeaderTimeout: 24 * time.Hour, // Matches proxy_read_timeout
57+
DisableKeepAlives: false, // Allow keep-alive
58+
}
59+
60+
p.server.Handler = http.HandlerFunc(p.proxyHandler(serverTransport))
61+
return p.server.ListenAndServe()
62+
}
63+
64+
func (p *SessionProxy) Shutdown(ctx context.Context) {
65+
err := p.server.Shutdown(ctx)
66+
if err != nil {
67+
zap.L().Error("failed to shutdown proxy server", zap.Error(err))
68+
}
69+
}
70+
71+
func (p *SessionProxy) proxyHandler(transport *http.Transport) func(w http.ResponseWriter, r *http.Request) {
72+
activeConnections, err := meters.GetUpDownCounter(meters.ActiveConnectionsCounterMeterName)
73+
if err != nil {
74+
zap.L().Error("failed to create active connections counter", zap.Error(err))
75+
}
76+
77+
return func(w http.ResponseWriter, r *http.Request) {
78+
if activeConnections != nil {
79+
activeConnections.Add(r.Context(), 1)
80+
defer func() {
81+
activeConnections.Add(r.Context(), -1)
82+
}()
83+
}
84+
85+
// Extract sandbox id from the host (<port>-<sandbox id>-<old client id>.e2b.dev)
86+
hostSplit := strings.Split(r.Host, "-")
87+
if len(hostSplit) < 2 {
88+
zap.L().Warn("invalid host to proxy", zap.String("host", r.Host))
89+
http.Error(w, "Invalid host", http.StatusBadRequest)
90+
return
91+
}
92+
93+
sandboxID := hostSplit[1]
94+
sandboxPortRaw := hostSplit[0]
95+
sandboxPort, sandboxPortErr := strconv.ParseUint(sandboxPortRaw, 10, 64)
96+
if sandboxPortErr != nil {
97+
zap.L().Warn("invalid sandbox port", zap.String("sandbox_port", sandboxPortRaw))
98+
http.Error(w, "Invalid sandbox port", http.StatusBadRequest)
99+
}
100+
101+
sbxIp, sbxFound := p.sandboxes.Get(sandboxID)
102+
if !sbxFound {
103+
zap.L().Warn("sandbox not found", zap.String("sandbox_id", sandboxID))
104+
http.Error(w, "Sandbox not found", http.StatusNotFound)
105+
return
106+
}
107+
108+
logger := zap.L().With(zap.String("sandbox_id", sandboxID), zap.String("sandbox_ip", sbxIp), zap.Uint64("sandbox_req_port", sandboxPort), zap.String("sandbox_port_path", r.URL.Path))
109+
110+
// We've resolved the node to proxy the request to
111+
logger.Debug("Proxying request")
112+
targetUrl := &url.URL{
113+
Scheme: "http",
114+
Host: fmt.Sprintf("%s:%d", sbxIp, sandboxPort),
115+
}
116+
117+
// Proxy the request
118+
proxy := httputil.NewSingleHostReverseProxy(targetUrl)
119+
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
120+
logger.Error("Reverse proxy error")
121+
122+
if p.isBrowser(r.UserAgent()) {
123+
w.WriteHeader(http.StatusBadGateway)
124+
w.Header().Add("Content-Type", "text/html")
125+
w.Write(p.buildHtmlClosedPortError(sandboxID, r.Host, sandboxPort))
126+
return
127+
}
128+
129+
w.WriteHeader(http.StatusBadGateway)
130+
w.Header().Add("Content-Type", "application/json")
131+
w.Write(p.buildJsonClosedPortError(sandboxID, sandboxPort))
132+
}
133+
134+
proxy.ModifyResponse = func(resp *http.Response) error {
135+
if resp.StatusCode >= 500 {
136+
logger.Error("Backend responded with error", zap.Int("status_code", resp.StatusCode))
137+
} else {
138+
logger.Info("Backend responded", zap.Int("status_code", resp.StatusCode))
139+
}
140+
141+
return nil
142+
}
143+
144+
proxy.Transport = transport
145+
proxy.ServeHTTP(w, r)
146+
}
147+
}
148+
149+
func (p *SessionProxy) buildHtmlClosedPortError(sandboxId string, host string, port uint64) []byte {
150+
replacements := map[string]string{
151+
"{{sandbox_id}}": sandboxId,
152+
"{{sandbox_port}}": strconv.FormatUint(port, 10),
153+
"{{sandbox_host}}": host,
154+
}
155+
156+
adjustedErrTemplate := proxyBrowser502PageHtml
157+
for placeholder, value := range replacements {
158+
adjustedErrTemplate = strings.ReplaceAll(adjustedErrTemplate, placeholder, value)
159+
}
160+
161+
return []byte(adjustedErrTemplate)
162+
}
163+
164+
func (p *SessionProxy) buildJsonClosedPortError(sandboxId string, port uint64) []byte {
165+
response := map[string]interface{}{
166+
"error": "The sandbox is running but port is not open",
167+
"sandbox_id": sandboxId,
168+
"port": port,
169+
}
170+
171+
responseBytes, _ := json.Marshal(response)
172+
return responseBytes
173+
}
174+
175+
func (p *SessionProxy) isBrowser(userAgent string) bool {
176+
userAgent = strings.ToLower(userAgent)
177+
for _, keyword := range browserIdentityKeywords {
178+
if strings.Contains(userAgent, keyword) {
179+
return true
180+
}
181+
}
182+
183+
return false
184+
}

packages/orchestrator/internal/proxy/proxy_browser_502.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
<img src="https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Symbol%20Gradient-Kr5pnWlK3ZhzBcRGf6Am4cNbJvY1Ge.svg" alt="Logo" class="logo">
1111
<div class="error-header">
1212
<h1 class="error-title">Closed Port Error</h1>
13-
<p class="error-message">The sandbox <span class="highlight" id="sandbox-id">$dbk_session_id</span> is running but there&#39s no service running on port <span class="highlight" id="port-number">$dbk_port</span>.</p>
13+
<p class="error-message">The sandbox <span class="highlight" id="sandbox-id">{{sandbox_id}}</span> is running but there&#39s no service running on port <span class="highlight" id="port-number">{{sandbox_port}}</span>.</p>
1414
</div>
1515
<div class="error-details">
16-
<span class="sandbox-url">$host</span>
17-
<div class="error-code">Connection refused on port <span class="highlight" id="port-number-code">$dbk_port</span></div>
16+
<span class="sandbox-url">{{sandbox_host}}</span>
17+
<div class="error-code">Connection refused on port <span class="highlight" id="port-number-code">{{sandbox_port}}</span></div>
1818
</div>
1919
<p class="help-text">Please ensure that your service is properly configured and running on the specified port.</p>
2020
<a class="debug-link" href="https://e2b.dev/docs/sdk-reference/cli/v1.0.9/sandbox#e2b-sandbox-logs">Check the sandbox logs for more information →</a>

0 commit comments

Comments
 (0)