Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/https-backend-ports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"e2b": minor
"@e2b/python-sdk": minor
---

Add `httpsPorts` (JS) / `https_ports` (Python) to the sandbox network config. Ports listed there have their public URLs proxied to the sandbox over HTTPS — use it when the service listening on the port serves TLS itself. This is not TLS passthrough: traffic is still terminated at the E2B proxy and re-encrypted on the hop to the sandbox, and the backend certificate is not verified, so self-signed certificates work. The configured ports are also returned in the sandbox info network config.
2 changes: 2 additions & 0 deletions packages/js-sdk/src/api/schema.gen.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,21 @@ export type SandboxNetworkOpts = {
* @default ${PORT}-sandboxid.e2b.app
*/
maskRequestHost?: string

/**
* Ports whose public URLs should connect to the sandbox using HTTPS.
*
* Use this when the service listening on the port serves HTTPS (TLS)
* itself. This is not TLS passthrough — traffic is still terminated at the
* E2B proxy and re-encrypted on the hop to the sandbox. The backend
* certificate is not verified, so self-signed certificates work.
*
* @example
* ```ts
* await Sandbox.create({ network: { httpsPorts: [3000] } })
* ```
*/
httpsPorts?: number[]
}

/**
Expand All @@ -177,6 +192,7 @@ export type SandboxNetworkInfo = {
rules?: Record<string, SandboxNetworkRuleInfo[]>
allowPublicTraffic?: boolean
maskRequestHost?: string
httpsPorts?: number[]
}

/**
Expand Down Expand Up @@ -687,6 +703,9 @@ function buildNetworkBody(
...(network.maskRequestHost !== undefined
? { maskRequestHost: network.maskRequestHost }
: {}),
...(network.httpsPorts !== undefined
? { httpsPorts: network.httpsPorts }
: {}),
}
}

Expand Down Expand Up @@ -801,6 +820,7 @@ export class SandboxApi {
rules: res.data.network.rules ?? undefined,
allowPublicTraffic: res.data.network.allowPublicTraffic,
maskRequestHost: res.data.network.maskRequestHost,
httpsPorts: res.data.network.httpsPorts,
}
: undefined,
lifecycle: res.data.lifecycle
Expand Down
61 changes: 61 additions & 0 deletions packages/js-sdk/tests/sandbox/network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,67 @@
)
})

describe('httpsPorts option', () => {
const port = 8443

sandboxTest.scoped({
sandboxOpts: {
network: {
httpsPorts: [port],
},
},
})

sandboxTest.skipIf(isDebug)(
'public URL proxies to an HTTPS backend in the sandbox',
async ({ sandbox }) => {
// Generate a self-signed certificate inside the sandbox
const keygen = await sandbox.commands.run(
'openssl req -x509 -newkey rsa:2048 -keyout /tmp/https-backend-key.pem -out /tmp/https-backend-cert.pem -days 1 -nodes -subj "/CN=localhost"'
)
assert.equal(keygen.exitCode, 0)

// Start an HTTPS server on the configured port
sandbox.commands.run(
`python3 -c "
import http.server, ssl
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'https backend')
def log_message(self, *a): pass
server = http.server.ThreadingHTTPServer(('0.0.0.0', ${port}), H)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('/tmp/https-backend-cert.pem', '/tmp/https-backend-key.pem')
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()
"`,
{ background: true }
)

// Poll the public URL until the server responds
const sandboxUrl = `https://${sandbox.getHost(port)}`
let response: Response | undefined
const deadline = Date.now() + 15_000
while (Date.now() < deadline) {
response = await fetch(sandboxUrl)
if (response.status === 200) {
break
}
await new Promise((resolve) => setTimeout(resolve, 1000))
}

assert.equal(response?.status, 200)

Check failure on line 354 in packages/js-sdk/tests/sandbox/network.test.ts

View workflow job for this annotation

GitHub Actions / Production / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/sandbox/network.test.ts > httpsPorts option > public URL proxies to an HTTPS backend in the sandbox

AssertionError: expected 502 to equal 200 - Expected + Received - 200 + 502 ❯ tests/sandbox/network.test.ts:354:14

Check failure on line 354 in packages/js-sdk/tests/sandbox/network.test.ts

View workflow job for this annotation

GitHub Actions / Production / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/sandbox/network.test.ts > httpsPorts option > public URL proxies to an HTTPS backend in the sandbox

AssertionError: expected 502 to equal 200 - Expected + Received - 200 + 502 ❯ tests/sandbox/network.test.ts:354:14

Check failure on line 354 in packages/js-sdk/tests/sandbox/network.test.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (ubuntu-22.04)

[unit] tests/sandbox/network.test.ts > httpsPorts option > public URL proxies to an HTTPS backend in the sandbox

AssertionError: expected 502 to equal 200 - Expected + Received - 200 + 502 ❯ tests/sandbox/network.test.ts:354:14

Check failure on line 354 in packages/js-sdk/tests/sandbox/network.test.ts

View workflow job for this annotation

GitHub Actions / Staging / JS SDK Tests / JS SDK - Build and test (windows-latest)

[unit] tests/sandbox/network.test.ts > httpsPorts option > public URL proxies to an HTTPS backend in the sandbox

AssertionError: expected 502 to equal 200 - Expected + Received - 200 + 502 ❯ tests/sandbox/network.test.ts:354:14
assert.equal(await response?.text(), 'https backend')

// The port is reported back in the sandbox info
const info = await sandbox.getInfo()
assert.deepEqual(info.network?.httpsPorts, [port])
}
)
})

describe('maskRequestHost option', () => {
sandboxTest.scoped({
sandboxOpts: {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions packages/python-sdk/e2b/sandbox/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ class SandboxNetworkOpts(TypedDict):
- Custom subdomain: `"${PORT}-myapp.example.com"`
"""

https_ports: NotRequired[List[int]]
"""
Ports whose public URLs should connect to the sandbox using HTTPS.

Use this when the service listening on the port serves HTTPS (TLS) itself.
This is not TLS passthrough — traffic is still terminated at the E2B proxy
and re-encrypted on the hop to the sandbox. The backend certificate is not
verified, so self-signed certificates work.

Example: ``Sandbox.create(network={"https_ports": [3000]})``
"""


class SandboxNetworkUpdate(TypedDict, total=False):
"""
Expand Down Expand Up @@ -238,6 +250,7 @@ class SandboxNetworkInfo(TypedDict, total=False):
rules: Dict[str, List[SandboxNetworkRuleInfo]]
allow_public_traffic: bool
mask_request_host: str
https_ports: List[int]


class SandboxOnTimeoutPause(TypedDict):
Expand Down Expand Up @@ -402,6 +415,8 @@ def build_network_config(
body["allow_public_traffic"] = network["allow_public_traffic"]
if "mask_request_host" in network:
body["mask_request_host"] = network["mask_request_host"]
if "https_ports" in network:
body["https_ports"] = list(network["https_ports"])

return body

Expand Down Expand Up @@ -447,6 +462,8 @@ def from_client_network_config(
result["allow_public_traffic"] = network.allow_public_traffic
if not isinstance(network.mask_request_host, Unset):
result["mask_request_host"] = network.mask_request_host
if not isinstance(network.https_ports, Unset):
result["https_ports"] = list(network.https_ports)

return result

Expand Down
48 changes: 48 additions & 0 deletions packages/python-sdk/tests/async/sandbox_async/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,54 @@ async def test_update_network_clears_existing_rules(async_sandbox_factory):
assert r2.exit_code == 0


@pytest.mark.skip_debug()
async def test_https_ports(async_sandbox_factory):
"""Test that a port listed in https_ports proxies to an HTTPS backend."""
port = 8443
async_sandbox = await async_sandbox_factory(
network=SandboxNetworkOpts(https_ports=[port])
)

# Generate a self-signed certificate inside the sandbox
keygen = await async_sandbox.commands.run(
"openssl req -x509 -newkey rsa:2048 "
"-keyout /tmp/https-backend-key.pem -out /tmp/https-backend-cert.pem "
'-days 1 -nodes -subj "/CN=localhost"'
)
assert keygen.exit_code == 0

# Start an HTTPS server on the configured port
await async_sandbox.commands.run(
f"""python3 -c "
import http.server, ssl
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'https backend')
def log_message(self, *a): pass
server = http.server.ThreadingHTTPServer(('0.0.0.0', {port}), H)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('/tmp/https-backend-cert.pem', '/tmp/https-backend-key.pem')
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()
" """,
background=True,
)

sandbox_url = f"https://{async_sandbox.get_host(port)}"

async with httpx.AsyncClient() as client:
response = await wait_for_status(client, sandbox_url, 200)
assert response.status_code == 200
assert response.text == "https backend"

# The port is reported back in the sandbox info
info = await async_sandbox.get_info()
assert info.network is not None
assert info.network.get("https_ports") == [port]


@pytest.mark.skip_debug()
async def test_mask_request_host(async_sandbox_factory):
"""Test that mask_request_host modifies the Host header correctly."""
Expand Down
46 changes: 46 additions & 0 deletions packages/python-sdk/tests/sync/sandbox_sync/test_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,52 @@ def test_update_network_clears_existing_rules(sandbox_factory):
assert r2.exit_code == 0


@pytest.mark.skip_debug()
def test_https_ports(sandbox_factory):
"""Test that a port listed in https_ports proxies to an HTTPS backend."""
port = 8443
sandbox = sandbox_factory(network=SandboxNetworkOpts(https_ports=[port]))

# Generate a self-signed certificate inside the sandbox
keygen = sandbox.commands.run(
"openssl req -x509 -newkey rsa:2048 "
"-keyout /tmp/https-backend-key.pem -out /tmp/https-backend-cert.pem "
'-days 1 -nodes -subj "/CN=localhost"'
)
assert keygen.exit_code == 0

# Start an HTTPS server on the configured port
sandbox.commands.run(
f"""python3 -c "
import http.server, ssl
class H(http.server.BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b'https backend')
def log_message(self, *a): pass
server = http.server.ThreadingHTTPServer(('0.0.0.0', {port}), H)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('/tmp/https-backend-cert.pem', '/tmp/https-backend-key.pem')
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()
" """,
background=True,
)

sandbox_url = f"https://{sandbox.get_host(port)}"

with httpx.Client() as client:
response = wait_for_status(client, sandbox_url, 200)
assert response.status_code == 200
assert response.text == "https backend"

# The port is reported back in the sandbox info
info = sandbox.get_info()
assert info.network is not None
assert info.network.get("https_ports") == [port]


@pytest.mark.skip_debug()
def test_mask_request_host(sandbox_factory):
"""Test that mask_request_host modifies the Host header correctly."""
Expand Down
10 changes: 10 additions & 0 deletions spec/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,16 @@ components:
maskRequestHost:
type: string
description: Specify host mask which will be used for all sandbox requests
httpsPorts:
type: array
description: Ports whose public URLs should connect to the sandbox using HTTPS. Backend certificate verification is disabled.
maxItems: 128
uniqueItems: true
items:
type: integer
format: uint32
minimum: 1
maximum: 65535
rules:
type: object
description: >
Expand Down
Loading