Skip to content
Merged
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/cuddly-horses-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': minor
'e2b': minor
---

add possibility to mask the Host in public requests with custom value
6 changes: 6 additions & 0 deletions .changeset/loud-bottles-provide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@e2b/python-sdk': minor
'e2b': minor
---

add ability to secure public traffic using token
8 changes: 8 additions & 0 deletions packages/js-sdk/src/sandbox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ export class Sandbox extends SandboxApi {
*/
readonly sandboxDomain: string

/**
* Traffic access token for accessing sandbox services with restricted public traffic.
*/
readonly trafficAccessToken?: string

protected readonly envdPort = 49983
protected readonly mcpPort = 50005

Expand All @@ -113,6 +118,7 @@ export class Sandbox extends SandboxApi {
sandboxDomain?: string
envdVersion: string
envdAccessToken?: string
trafficAccessToken?: string
}
) {
super()
Expand All @@ -123,6 +129,7 @@ export class Sandbox extends SandboxApi {
this.sandboxDomain = opts.sandboxDomain ?? this.connectionConfig.domain

this.envdAccessToken = opts.envdAccessToken
this.trafficAccessToken = opts.trafficAccessToken
this.envdApiUrl = this.connectionConfig.getSandboxUrl(this.sandboxId, {
sandboxDomain: this.sandboxDomain,
envdPort: this.envdPort,
Expand Down Expand Up @@ -420,6 +427,7 @@ export class Sandbox extends SandboxApi {
sandboxId,
sandboxDomain: sandbox.sandboxDomain,
envdAccessToken: sandbox.envdAccessToken,
trafficAccessToken: sandbox.trafficAccessToken,
envdVersion: sandbox.envdVersion,
...config,
}) as InstanceType<S>
Expand Down
15 changes: 15 additions & 0 deletions packages/js-sdk/src/sandbox/sandboxApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,19 @@ export type SandboxNetworkOpts = {
* - To deny traffic to a specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
*/
denyOut?: string[]

/**
* Specify if the sandbox URLs should be accessible only with authentication.
* @default true
*/
allowPublicTraffic?: boolean

/** Specify host mask which will be used for all sandbox requests in the header.
* You can use the ${PORT} variable that will be replaced with the actual port number of the service.
*
* @default ${PORT}-sandboxid.e2b.app
*/
maskRequestHost?: string
}

/**
Expand Down Expand Up @@ -552,6 +565,7 @@ export class SandboxApi {
sandboxDomain: res.data!.domain || undefined,
envdVersion: res.data!.envdVersion,
envdAccessToken: res.data!.envdAccessToken,
trafficAccessToken: res.data!.trafficAccessToken || undefined,
}
}

Expand Down Expand Up @@ -590,6 +604,7 @@ export class SandboxApi {
sandboxDomain: res.data!.domain || undefined,
envdVersion: res.data!.envdVersion,
envdAccessToken: res.data!.envdAccessToken,
trafficAccessToken: res.data!.trafficAccessToken || undefined,
}
}
}
Expand Down
127 changes: 127 additions & 0 deletions packages/js-sdk/tests/sandbox/network.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,130 @@ describe('allow takes precedence over deny', () => {
}
)
})

describe('allowPublicTraffic=false', () => {
sandboxTest.scoped({
sandboxOpts: {
network: {
allowPublicTraffic: false,
},
},
})

sandboxTest.skipIf(isDebug)(
'sandbox requires traffic access token',
async ({ sandbox }) => {
// Verify the sandbox was created successfully and has a traffic access token
assert(sandbox.trafficAccessToken)

// Start a simple HTTP server in the sandbox
const port = 8080
sandbox.commands.run(`python3 -m http.server ${port}`, {
background: true,
})

// Wait for server to start
await new Promise((resolve) => setTimeout(resolve, 3000))

// Get the public URL for the sandbox
const sandboxUrl = `https://${sandbox.getHost(port)}`

// Test 1: Request without traffic access token should fail with 403
const response1 = await fetch(sandboxUrl)
assert.equal(response1.status, 403)

// Test 2: Request with valid traffic access token should succeed
const response2 = await fetch(sandboxUrl, {
headers: {
'e2b-traffic-access-token': sandbox.trafficAccessToken,
},
})
assert.equal(response2.status, 200)
}
)
})

describe('allowPublicTraffic=true', () => {
sandboxTest.scoped({
sandboxOpts: {
network: {
allowPublicTraffic: true,
},
},
})

sandboxTest.skipIf(isDebug)(
'sandbox works without token',
async ({ sandbox }) => {
// Start a simple HTTP server in the sandbox
const port = 8080
sandbox.commands.run(`python3 -m http.server ${port}`, {
background: true,
})

// Wait for server to start
await new Promise((resolve) => setTimeout(resolve, 3000))

// Get the public URL for the sandbox
const sandboxUrl = `https://${sandbox.getHost(port)}`

// Request without traffic access token should succeed (public access enabled)
const response = await fetch(sandboxUrl)
assert.equal(response.status, 200)
}
)
})

describe('maskRequestHost option', () => {
sandboxTest.scoped({
sandboxOpts: {
network: {
maskRequestHost: 'custom-host.example.com:${PORT}',
},
},
})

sandboxTest.skipIf(isDebug)(
'verify maskRequestHost modifies Host header correctly',
async ({ sandbox }) => {
// Install netcat for testing
await sandbox.commands.run('apt-get update', { user: 'root' })
await sandbox.commands.run('apt-get install -y netcat-traditional', {
user: 'root',
})

const port = 8080
const outputFile = '/tmp/nc_output.txt'

// Start netcat listener in background to capture request headers
sandbox.commands.run(`nc -l -p ${port} > ${outputFile}`, {
background: true,
user: 'root',
})

// Wait for netcat to start
await new Promise((resolve) => setTimeout(resolve, 3000))

// Get the public URL for the sandbox
const sandboxUrl = `https://${sandbox.getHost(port)}`

// Make a request from OUTSIDE the sandbox through the proxy
// The Host header should be modified according to maskRequestHost
try {
await fetch(sandboxUrl, { signal: AbortSignal.timeout(5000) })
} catch (error) {
// Request may fail since netcat doesn't respond properly, but headers are captured
}

// Read the captured output from inside the sandbox
const result = await sandbox.commands.run(`cat ${outputFile}`, {
user: 'root',
})

// Verify the Host header was modified according to maskRequestHost
assert.include(result.stdout, 'Host:')
assert.include(result.stdout, 'custom-host.example.com')
assert.include(result.stdout, `${port}`)
}
)
})
16 changes: 9 additions & 7 deletions packages/python-sdk/e2b/api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import os
from types import TracebackType
import json
import logging
from typing import Optional, Union
from httpx import Limits, BaseTransport, AsyncBaseTransport
import os
from dataclasses import dataclass
from types import TracebackType
from typing import Optional, Union

from httpx import AsyncBaseTransport, BaseTransport, Limits

from e2b.api.client.client import AuthenticatedClient
from e2b.connection_config import ConnectionConfig
from e2b.api.client.types import Response
from e2b.api.metadata import default_headers
from e2b.connection_config import ConnectionConfig
from e2b.exceptions import (
AuthenticationException,
SandboxException,
RateLimitException,
SandboxException,
)
from e2b.api.client.types import Response

logger = logging.getLogger(__name__)

Expand All @@ -31,6 +32,7 @@ class SandboxCreateResponse:
sandbox_domain: Optional[str]
envd_version: str
envd_access_token: str
traffic_access_token: Optional[str]


def handle_api_exception(
Expand Down
13 changes: 10 additions & 3 deletions packages/python-sdk/e2b/sandbox/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import urllib.parse
from packaging.version import Version

from typing import Optional, TypedDict

from e2b.sandbox.signature import get_signature
from packaging.version import Version

from e2b.connection_config import ConnectionConfig, default_username
from e2b.envd.api import ENVD_API_FILES_ROUTE
from e2b.envd.versions import ENVD_DEFAULT_USER
from e2b.sandbox.signature import get_signature


class SandboxOpts(TypedDict):
Expand All @@ -15,6 +15,7 @@ class SandboxOpts(TypedDict):
envd_version: Version
envd_access_token: Optional[str]
sandbox_url: Optional[str]
traffic_access_token: Optional[str]
connection_config: ConnectionConfig


Expand All @@ -33,12 +34,14 @@ def __init__(
envd_access_token: Optional[str],
sandbox_domain: Optional[str],
connection_config: ConnectionConfig,
traffic_access_token: Optional[str] = None,
):
self.__connection_config = connection_config
self.__sandbox_id = sandbox_id
self.__sandbox_domain = sandbox_domain or self.connection_config.domain
self.__envd_version = envd_version
self.__envd_access_token = envd_access_token
self.__traffic_access_token = traffic_access_token
self.__envd_api_url = self.connection_config.get_sandbox_url(
self.sandbox_id, self.sandbox_domain
)
Expand All @@ -65,6 +68,10 @@ def connection_config(self) -> ConnectionConfig:
def _envd_version(self) -> Version:
return self.__envd_version

@property
def traffic_access_token(self) -> Optional[str]:
return self.__traffic_access_token

@property
def sandbox_domain(self) -> Optional[str]:
return self.__sandbox_domain
Expand Down
15 changes: 15 additions & 0 deletions packages/python-sdk/e2b/sandbox/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ class SandboxNetworkOpts(TypedDict):
- To deny traffic to specific addresses: `["1.1.1.1", "8.8.8.0/24"]`
"""

allow_public_traffic: NotRequired[bool]
"""
Controls whether sandbox URLs should be publicly accessible or require authentication.
Defaults to True.
"""

mask_request_host: NotRequired[str]
"""
Allows specifying a custom host mask for all sandbox requests.
Supports ${PORT} variable. Defaults to "${PORT}-sandboxid.e2b.app".

Examples:
- Custom subdomain: `"${PORT}-myapp.example.com"`
"""


@dataclass
class SandboxInfo:
Expand Down
4 changes: 4 additions & 0 deletions packages/python-sdk/e2b/sandbox_async/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ async def _cls_connect(
sandbox_domain=sandbox.domain,
envd_version=Version(sandbox.envd_version),
envd_access_token=envd_access_token,
traffic_access_token=sandbox.traffic_access_token,
connection_config=connection_config,
)

Expand All @@ -689,6 +690,7 @@ async def _create(
sandbox_domain = None
envd_version = ENVD_DEBUG_FALLBACK
envd_access_token = None
traffic_access_token = None
else:
response = await SandboxApi._create_sandbox(
template=template or cls.default_template,
Expand All @@ -707,6 +709,7 @@ async def _create(
sandbox_domain = response.sandbox_domain
envd_version = Version(response.envd_version)
envd_access_token = response.envd_access_token
traffic_access_token = response.traffic_access_token

if envd_access_token is not None and not isinstance(
envd_access_token, Unset
Expand All @@ -726,5 +729,6 @@ async def _create(
sandbox_domain=sandbox_domain,
envd_version=envd_version,
envd_access_token=envd_access_token,
traffic_access_token=traffic_access_token,
connection_config=connection_config,
)
1 change: 1 addition & 0 deletions packages/python-sdk/e2b/sandbox_async/sandbox_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ async def _create_sandbox(
sandbox_domain=res.parsed.domain,
envd_version=res.parsed.envd_version,
envd_access_token=res.parsed.envd_access_token,
traffic_access_token=res.parsed.traffic_access_token,
)

@classmethod
Expand Down
4 changes: 4 additions & 0 deletions packages/python-sdk/e2b/sandbox_sync/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ def _cls_connect(
connection_config=connection_config,
envd_version=Version(sandbox.envd_version),
envd_access_token=envd_access_token,
traffic_access_token=sandbox.traffic_access_token,
)

@classmethod
Expand All @@ -681,6 +682,7 @@ def _create(
sandbox_domain = None
envd_version = ENVD_DEBUG_FALLBACK
envd_access_token = None
traffic_access_token = None
else:
response = SandboxApi._create_sandbox(
template=template or cls.default_template,
Expand All @@ -699,6 +701,7 @@ def _create(
sandbox_domain = response.sandbox_domain
envd_version = Version(response.envd_version)
envd_access_token = response.envd_access_token
traffic_access_token = response.traffic_access_token

if envd_access_token is not None and not isinstance(
envd_access_token, Unset
Expand All @@ -718,5 +721,6 @@ def _create(
sandbox_domain=sandbox_domain,
envd_version=envd_version,
envd_access_token=envd_access_token,
traffic_access_token=traffic_access_token,
connection_config=connection_config,
)
Loading