diff --git a/README.md b/README.md index b4fe3fa..7e6bea8 100644 --- a/README.md +++ b/README.md @@ -310,3 +310,9 @@ Tests in `tests/integration/test_engine_mtls.py` use real TLS servers for discov inventory, both watch transports, trust validation and cancellation. Opt-in real-Engine inventory checks use `RSTREAM_PYTHON_E2E_ENGINE_API=1` with the existing E2E credential configuration. + +## HTTP CORS + +Pass `http_cors=HTTPCORS(allowed_origins=("https://dashboard.example",), allowed_methods=("GET", "PUT"), allowed_headers=("Authorization",), exposed_headers=("ETag",), max_age=600)` to `create_tunnel` or `CreateTunnelOptions`. Import `HTTPCORS` from `rstream`. + +CORS requires a published HTTP tunnel. Allowed origins must be explicit HTTP(S) origins without paths or wildcards. Omitted method and request-header lists use GET/HEAD/POST and Authorization/Content-Type; credentials are disabled and max age is zero by default. An absent policy preserves upstream CORS. Engine answers valid preflights before request authentication, but every actual request still requires its usual authorization. No filesystem or upstream access is granted by a preflight. Deploy an Engine supporting protocol patch 1.4.6 before using this SDK version; the new Engine remains compatible with supported older clients. diff --git a/proto/rstream.proto b/proto/rstream.proto index 84aa301..0e098cf 100644 --- a/proto/rstream.proto +++ b/proto/rstream.proto @@ -14,7 +14,7 @@ extend google.protobuf.FieldOptions { string access = 51234; } -option (protocol_version) = "1.4.5"; +option (protocol_version) = "1.4.6"; package rstream.io_rstrm.protobuf; @@ -64,6 +64,7 @@ enum ErrorCode { // 7000-7999: Runtime capacity exhausted ERROR_CODE_CAPACITY_EXHAUSTED = 7000; + ERROR_CODE_RESOURCE_CONFLICT = 8000; // 9000-9999: Internal errors ERROR_CODE_INTERNAL = 9000; @@ -102,6 +103,16 @@ message ServerDetails { // Tunnel message structure used for creating and managing tunnels +// Optional browser access policy for a published HTTP tunnel. +message HttpCors { + repeated string allowed_origins = 1; + repeated string allowed_methods = 2; + repeated string allowed_headers = 3; + repeated string exposed_headers = 4; + bool allow_credentials = 5; + int64 max_age = 6; +} + message TunnelProperties { google.protobuf.StringValue id = 1 [(access) = "read-only"]; google.protobuf.Timestamp creation_date = 2 [(access) = "read-only"]; @@ -129,6 +140,7 @@ message TunnelProperties { google.protobuf.BoolValue upstream_tls = 24 [(access) = "read-write"]; google.protobuf.BoolValue datagram_guaranteed_delivery = 25 [(access) = "read-write"]; google.protobuf.BoolValue allow_cross_region_routing = 26 [(access) = "read-write"]; + HttpCors http_cors = 27 [(access) = "read-write"]; } // When a client opens a new control channel to the server diff --git a/src/rstream/__init__.py b/src/rstream/__init__.py index 5099100..37a8760 100644 --- a/src/rstream/__init__.py +++ b/src/rstream/__init__.py @@ -27,6 +27,7 @@ from rstream.stream import RstreamStream from rstream.tunnel import BytestreamTunnel, format_forwarding_address from rstream.types import ( + HTTPCORS, CreateTunnelOptions, ServerDetails, TunnelAuth, @@ -52,6 +53,7 @@ def __getattr__(name: str) -> ModuleType: __all__ = [ + "HTTPCORS", "BytestreamTunnel", "Client", "ClientFilters", diff --git a/src/rstream/_proto/rstream_pb2.py b/src/rstream/_proto/rstream_pb2.py index 6d682a4..eb9a6be 100644 --- a/src/rstream/_proto/rstream_pb2.py +++ b/src/rstream/_proto/rstream_pb2.py @@ -27,14 +27,14 @@ from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rrstream.proto\x12\x19rstream.io_rstrm.protobuf\x1a google/protobuf/descriptor.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"/\n\tIpAddress\x12\x0c\n\x02v4\x18\x01 \x01(\x07H\x00\x12\x0c\n\x02v6\x18\x02 \x01(\x0cH\x00\x42\x06\n\x04\x61\x64\x64r\"j\n\x05\x45rror\x12\x32\n\x04\x63ode\x18\x01 \x01(\x0e\x32$.rstream.io_rstrm.protobuf.ErrorCode\x12-\n\x07message\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xd5\x02\n\rClientDetails\x12+\n\x05\x61gent\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07version\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12(\n\x02os\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04\x61rch\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05token\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x36\n\x10protocol_version\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xd2\x02\n\rServerDetails\x12+\n\x05\x61gent\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07version\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04plan\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12.\n\x08provider\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12,\n\x06region\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12,\n\x06update\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x90\r\n\x10TunnelProperties\x12\x37\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\r\x92\x82\x19\tread-only\x12@\n\rcreation_date\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\r\x92\x82\x19\tread-only\x12:\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12:\n\x04type\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12;\n\x07publish\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12>\n\x08protocol\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12W\n\x06labels\x18\x07 \x03(\x0b\x32\x37.rstream.io_rstrm.protobuf.TunnelProperties.LabelsEntryB\x0e\x92\x82\x19\nread-write\x12\x1d\n\x05geoip\x18\x08 \x03(\tB\x0e\x92\x82\x19\nread-write\x12#\n\x0btrusted_ips\x18\t \x03(\tB\x0e\x92\x82\x19\nread-write\x12;\n\x04host\x18\n \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0f\x18\x01\x92\x82\x19\tread-only\x12>\n\x08tls_mode\x18\x0b \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12!\n\ttls_alpns\x18\x0c \x03(\tB\x0e\x92\x82\x19\nread-write\x12\x45\n\x0ftls_min_version\x18\r \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12#\n\x0btls_ciphers\x18\x0e \x03(\tB\x0e\x92\x82\x19\nread-write\x12=\n\tmtls_auth\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12G\n\x0fmtls_cacert_pem\x18\x10 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x10\x18\x01\x92\x82\x19\nread-write\x12\x42\n\x0chttp_version\x18\x11 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12\x42\n\x0chttp_use_tls\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x10\x18\x01\x92\x82\x19\nread-write\x12>\n\ntoken_auth\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12@\n\x0crstream_auth\x18\x14 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12\x42\n\x0e\x63hallenge_mode\x18\x15 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12>\n\x08hostname\x18\x16 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12:\n\x04port\x18\x17 \x01(\x0b\x32\x1c.google.protobuf.UInt32ValueB\x0e\x92\x82\x19\nread-write\x12@\n\x0cupstream_tls\x18\x18 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12P\n\x1c\x64\x61tagram_guaranteed_delivery\x18\x19 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12N\n\x1a\x61llow_cross_region_routing\x18\x1a \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9e\x01\n\x15OpenControlChannelReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x43\n\x08liveness\x18\x02 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.ControlChannelLiveness\"\xb9\x02\n\x15OpenControlChannelRsp\x12\x41\n\x02ok\x18\x01 \x01(\x0b\x32\x33.rstream.io_rstrm.protobuf.OpenControlChannelRsp.OkH\x00\x12\x31\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x1a\x9e\x01\n\x02Ok\x12\x11\n\tclient_id\x18\x01 \x01(\t\x12@\n\x0eserver_details\x18\x02 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ServerDetails\x12\x43\n\x08liveness\x18\x03 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.ControlChannelLivenessB\t\n\x07payload\"\x18\n\x16\x43loseControlChannelReq\"\x18\n\x16\x43loseControlChannelRsp\"k\n\rOpenTunnelReq\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x46\n\x11tunnel_properties\x18\x02 \x01(\x0b\x32+.rstream.io_rstrm.protobuf.TunnelProperties\"\xab\x01\n\rOpenTunnelRsp\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12H\n\x11tunnel_properties\x18\x02 \x01(\x0b\x32+.rstream.io_rstrm.protobuf.TunnelPropertiesH\x00\x12\x31\n\x05\x65rror\x18\x03 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x42\t\n\x07payload\"#\n\x0e\x43loseTunnelReq\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\"#\n\x0e\x43loseTunnelRsp\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\"\xd1\x01\n\x0cProxyConnReq\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12,\n\x06secret\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x37\n\tsource_ip\x18\x04 \x01(\x0b\x32$.rstream.io_rstrm.protobuf.IpAddress\x12\x34\n\x0eproxy_endpoint\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"R\n\x0cProxyConnRsp\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12/\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"\x8d\x01\n\x08ProxyReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12,\n\x08zero_rtt\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\";\n\x08ProxyRsp\x12/\n\x05\x65rror\x18\x01 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"\xc9\x01\n\tStreamReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x16\n\x0etunnel_id_name\x18\x02 \x01(\t\x12,\n\x08zero_rtt\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x34\n\x10\x64\x61tagram_channel\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"^\n\tStreamRsp\x12\x13\n\tstream_id\x18\x01 \x01(\tH\x00\x12\x31\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x42\t\n\x07payload\"Z\n\x14\x44\x61tagramChannelClose\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12/\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"U\n\x16\x43ontrolChannelLiveness\x12\x1d\n\x15heartbeat_interval_ms\x18\x01 \x01(\r\x12\x1c\n\x14heartbeat_timeout_ms\x18\x02 \x01(\r\"6\n\tHeartbeat\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x63knowledgement\x18\x02 \x01(\x04\" \n\rServerMessage\x12\x0f\n\x07message\x18\x01 \x01(\t\"\xcc\t\n\x07Message\x12T\n\x18open_control_channel_req\x18\x01 \x01(\x0b\x32\x30.rstream.io_rstrm.protobuf.OpenControlChannelReqH\x00\x12T\n\x18open_control_channel_rsp\x18\x02 \x01(\x0b\x32\x30.rstream.io_rstrm.protobuf.OpenControlChannelRspH\x00\x12V\n\x19\x63lose_control_channel_req\x18\x03 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.CloseControlChannelReqH\x00\x12V\n\x19\x63lose_control_channel_rsp\x18\x04 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.CloseControlChannelRspH\x00\x12\x43\n\x0fopen_tunnel_req\x18\x05 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.OpenTunnelReqH\x00\x12\x43\n\x0fopen_tunnel_rsp\x18\x06 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.OpenTunnelRspH\x00\x12\x45\n\x10\x63lose_tunnel_req\x18\x07 \x01(\x0b\x32).rstream.io_rstrm.protobuf.CloseTunnelReqH\x00\x12\x45\n\x10\x63lose_tunnel_rsp\x18\x08 \x01(\x0b\x32).rstream.io_rstrm.protobuf.CloseTunnelRspH\x00\x12\x41\n\x0eproxy_conn_req\x18\t \x01(\x0b\x32\'.rstream.io_rstrm.protobuf.ProxyConnReqH\x00\x12\x41\n\x0eproxy_conn_rsp\x18\n \x01(\x0b\x32\'.rstream.io_rstrm.protobuf.ProxyConnRspH\x00\x12\x38\n\tproxy_req\x18\x0b \x01(\x0b\x32#.rstream.io_rstrm.protobuf.ProxyReqH\x00\x12\x38\n\tproxy_rsp\x18\x0c \x01(\x0b\x32#.rstream.io_rstrm.protobuf.ProxyRspH\x00\x12:\n\nstream_req\x18\r \x01(\x0b\x32$.rstream.io_rstrm.protobuf.StreamReqH\x00\x12:\n\nstream_rsp\x18\x0e \x01(\x0b\x32$.rstream.io_rstrm.protobuf.StreamRspH\x00\x12\x39\n\theartbeat\x18\x0f \x01(\x0b\x32$.rstream.io_rstrm.protobuf.HeartbeatH\x00\x12\x42\n\x0eserver_message\x18\x10 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ServerMessageH\x00\x12Q\n\x16\x64\x61tagram_channel_close\x18\x11 \x01(\x0b\x32/.rstream.io_rstrm.protobuf.DatagramChannelCloseH\x00\x42\t\n\x07payload*\xb5\x03\n\tErrorCode\x12\x1a\n\x16\x45RROR_CODE_UNSPECIFIED\x10\x00\x12\x1c\n\x17\x45RROR_CODE_UNAUTHORIZED\x10\xe8\x07\x12\x1f\n\x1a\x45RROR_CODE_INVALID_REQUEST\x10\xd0\x0f\x12(\n#ERROR_CODE_PROTOCOL_VERSION_MISSING\x10\xda\x0f\x12(\n#ERROR_CODE_PROTOCOL_VERSION_INVALID\x10\xe4\x0f\x12-\n(ERROR_CODE_PROTOCOL_VERSION_INCOMPATIBLE\x10\xee\x0f\x12 \n\x1b\x45RROR_CODE_TUNNEL_NOT_FOUND\x10\xb8\x17\x12\x1e\n\x19\x45RROR_CODE_INVALID_STREAM\x10\xa0\x1f\x12%\n ERROR_CODE_FEATURE_NOT_AVAILABLE\x10\x88\'\x12#\n\x1e\x45RROR_CODE_SERVICE_UNAVAILABLE\x10\xf0.\x12\"\n\x1d\x45RROR_CODE_CAPACITY_EXHAUSTED\x10\xd8\x36\x12\x18\n\x13\x45RROR_CODE_INTERNAL\x10\xa8\x46:8\n\x10protocol_version\x12\x1c.google.protobuf.FileOptions\x18\xb8\x8e\x03 \x01(\t:/\n\x06\x61\x63\x63\x65ss\x12\x1d.google.protobuf.FieldOptions\x18\xa2\x90\x03 \x01(\tB\x0fZ\x04./pb\xc2\xf3\x18\x05\x31.4.5b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rrstream.proto\x12\x19rstream.io_rstrm.protobuf\x1a google/protobuf/descriptor.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1egoogle/protobuf/wrappers.proto\"/\n\tIpAddress\x12\x0c\n\x02v4\x18\x01 \x01(\x07H\x00\x12\x0c\n\x02v6\x18\x02 \x01(\x0cH\x00\x42\x06\n\x04\x61\x64\x64r\"j\n\x05\x45rror\x12\x32\n\x04\x63ode\x18\x01 \x01(\x0e\x32$.rstream.io_rstrm.protobuf.ErrorCode\x12-\n\x07message\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xd5\x02\n\rClientDetails\x12+\n\x05\x61gent\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07version\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12(\n\x02os\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04\x61rch\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05token\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x36\n\x10protocol_version\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xd2\x02\n\rServerDetails\x12+\n\x05\x61gent\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63hannel\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07version\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04plan\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12.\n\x08provider\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12,\n\x06region\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12,\n\x06update\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x9a\x01\n\x08HttpCors\x12\x17\n\x0f\x61llowed_origins\x18\x01 \x03(\t\x12\x17\n\x0f\x61llowed_methods\x18\x02 \x03(\t\x12\x17\n\x0f\x61llowed_headers\x18\x03 \x03(\t\x12\x17\n\x0f\x65xposed_headers\x18\x04 \x03(\t\x12\x19\n\x11\x61llow_credentials\x18\x05 \x01(\x08\x12\x0f\n\x07max_age\x18\x06 \x01(\x03\"\xd8\r\n\x10TunnelProperties\x12\x37\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\r\x92\x82\x19\tread-only\x12@\n\rcreation_date\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\r\x92\x82\x19\tread-only\x12:\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12:\n\x04type\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12;\n\x07publish\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12>\n\x08protocol\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12W\n\x06labels\x18\x07 \x03(\x0b\x32\x37.rstream.io_rstrm.protobuf.TunnelProperties.LabelsEntryB\x0e\x92\x82\x19\nread-write\x12\x1d\n\x05geoip\x18\x08 \x03(\tB\x0e\x92\x82\x19\nread-write\x12#\n\x0btrusted_ips\x18\t \x03(\tB\x0e\x92\x82\x19\nread-write\x12;\n\x04host\x18\n \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0f\x18\x01\x92\x82\x19\tread-only\x12>\n\x08tls_mode\x18\x0b \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12!\n\ttls_alpns\x18\x0c \x03(\tB\x0e\x92\x82\x19\nread-write\x12\x45\n\x0ftls_min_version\x18\r \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12#\n\x0btls_ciphers\x18\x0e \x03(\tB\x0e\x92\x82\x19\nread-write\x12=\n\tmtls_auth\x18\x0f \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12G\n\x0fmtls_cacert_pem\x18\x10 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x10\x18\x01\x92\x82\x19\nread-write\x12\x42\n\x0chttp_version\x18\x11 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12\x42\n\x0chttp_use_tls\x18\x12 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x10\x18\x01\x92\x82\x19\nread-write\x12>\n\ntoken_auth\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12@\n\x0crstream_auth\x18\x14 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12\x42\n\x0e\x63hallenge_mode\x18\x15 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12>\n\x08hostname\x18\x16 \x01(\x0b\x32\x1c.google.protobuf.StringValueB\x0e\x92\x82\x19\nread-write\x12:\n\x04port\x18\x17 \x01(\x0b\x32\x1c.google.protobuf.UInt32ValueB\x0e\x92\x82\x19\nread-write\x12@\n\x0cupstream_tls\x18\x18 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12P\n\x1c\x64\x61tagram_guaranteed_delivery\x18\x19 \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12N\n\x1a\x61llow_cross_region_routing\x18\x1a \x01(\x0b\x32\x1a.google.protobuf.BoolValueB\x0e\x92\x82\x19\nread-write\x12\x46\n\thttp_cors\x18\x1b \x01(\x0b\x32#.rstream.io_rstrm.protobuf.HttpCorsB\x0e\x92\x82\x19\nread-write\x1a-\n\x0bLabelsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"\x9e\x01\n\x15OpenControlChannelReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x43\n\x08liveness\x18\x02 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.ControlChannelLiveness\"\xb9\x02\n\x15OpenControlChannelRsp\x12\x41\n\x02ok\x18\x01 \x01(\x0b\x32\x33.rstream.io_rstrm.protobuf.OpenControlChannelRsp.OkH\x00\x12\x31\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x1a\x9e\x01\n\x02Ok\x12\x11\n\tclient_id\x18\x01 \x01(\t\x12@\n\x0eserver_details\x18\x02 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ServerDetails\x12\x43\n\x08liveness\x18\x03 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.ControlChannelLivenessB\t\n\x07payload\"\x18\n\x16\x43loseControlChannelReq\"\x18\n\x16\x43loseControlChannelRsp\"k\n\rOpenTunnelReq\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12\x46\n\x11tunnel_properties\x18\x02 \x01(\x0b\x32+.rstream.io_rstrm.protobuf.TunnelProperties\"\xab\x01\n\rOpenTunnelRsp\x12\x12\n\nrequest_id\x18\x01 \x01(\t\x12H\n\x11tunnel_properties\x18\x02 \x01(\x0b\x32+.rstream.io_rstrm.protobuf.TunnelPropertiesH\x00\x12\x31\n\x05\x65rror\x18\x03 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x42\t\n\x07payload\"#\n\x0e\x43loseTunnelReq\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\"#\n\x0e\x43loseTunnelRsp\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\"\xd1\x01\n\x0cProxyConnReq\x12\x11\n\ttunnel_id\x18\x01 \x01(\t\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12,\n\x06secret\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x37\n\tsource_ip\x18\x04 \x01(\x0b\x32$.rstream.io_rstrm.protobuf.IpAddress\x12\x34\n\x0eproxy_endpoint\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"R\n\x0cProxyConnRsp\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12/\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"\x8d\x01\n\x08ProxyReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x11\n\tstream_id\x18\x02 \x01(\t\x12,\n\x08zero_rtt\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\";\n\x08ProxyRsp\x12/\n\x05\x65rror\x18\x01 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"\xc9\x01\n\tStreamReq\x12@\n\x0e\x63lient_details\x18\x01 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ClientDetails\x12\x16\n\x0etunnel_id_name\x18\x02 \x01(\t\x12,\n\x08zero_rtt\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12\x34\n\x10\x64\x61tagram_channel\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\"^\n\tStreamRsp\x12\x13\n\tstream_id\x18\x01 \x01(\tH\x00\x12\x31\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.ErrorH\x00\x42\t\n\x07payload\"Z\n\x14\x44\x61tagramChannelClose\x12\x11\n\tstream_id\x18\x01 \x01(\t\x12/\n\x05\x65rror\x18\x02 \x01(\x0b\x32 .rstream.io_rstrm.protobuf.Error\"U\n\x16\x43ontrolChannelLiveness\x12\x1d\n\x15heartbeat_interval_ms\x18\x01 \x01(\r\x12\x1c\n\x14heartbeat_timeout_ms\x18\x02 \x01(\r\"6\n\tHeartbeat\x12\x10\n\x08sequence\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x63knowledgement\x18\x02 \x01(\x04\" \n\rServerMessage\x12\x0f\n\x07message\x18\x01 \x01(\t\"\xcc\t\n\x07Message\x12T\n\x18open_control_channel_req\x18\x01 \x01(\x0b\x32\x30.rstream.io_rstrm.protobuf.OpenControlChannelReqH\x00\x12T\n\x18open_control_channel_rsp\x18\x02 \x01(\x0b\x32\x30.rstream.io_rstrm.protobuf.OpenControlChannelRspH\x00\x12V\n\x19\x63lose_control_channel_req\x18\x03 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.CloseControlChannelReqH\x00\x12V\n\x19\x63lose_control_channel_rsp\x18\x04 \x01(\x0b\x32\x31.rstream.io_rstrm.protobuf.CloseControlChannelRspH\x00\x12\x43\n\x0fopen_tunnel_req\x18\x05 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.OpenTunnelReqH\x00\x12\x43\n\x0fopen_tunnel_rsp\x18\x06 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.OpenTunnelRspH\x00\x12\x45\n\x10\x63lose_tunnel_req\x18\x07 \x01(\x0b\x32).rstream.io_rstrm.protobuf.CloseTunnelReqH\x00\x12\x45\n\x10\x63lose_tunnel_rsp\x18\x08 \x01(\x0b\x32).rstream.io_rstrm.protobuf.CloseTunnelRspH\x00\x12\x41\n\x0eproxy_conn_req\x18\t \x01(\x0b\x32\'.rstream.io_rstrm.protobuf.ProxyConnReqH\x00\x12\x41\n\x0eproxy_conn_rsp\x18\n \x01(\x0b\x32\'.rstream.io_rstrm.protobuf.ProxyConnRspH\x00\x12\x38\n\tproxy_req\x18\x0b \x01(\x0b\x32#.rstream.io_rstrm.protobuf.ProxyReqH\x00\x12\x38\n\tproxy_rsp\x18\x0c \x01(\x0b\x32#.rstream.io_rstrm.protobuf.ProxyRspH\x00\x12:\n\nstream_req\x18\r \x01(\x0b\x32$.rstream.io_rstrm.protobuf.StreamReqH\x00\x12:\n\nstream_rsp\x18\x0e \x01(\x0b\x32$.rstream.io_rstrm.protobuf.StreamRspH\x00\x12\x39\n\theartbeat\x18\x0f \x01(\x0b\x32$.rstream.io_rstrm.protobuf.HeartbeatH\x00\x12\x42\n\x0eserver_message\x18\x10 \x01(\x0b\x32(.rstream.io_rstrm.protobuf.ServerMessageH\x00\x12Q\n\x16\x64\x61tagram_channel_close\x18\x11 \x01(\x0b\x32/.rstream.io_rstrm.protobuf.DatagramChannelCloseH\x00\x42\t\n\x07payload*\xd8\x03\n\tErrorCode\x12\x1a\n\x16\x45RROR_CODE_UNSPECIFIED\x10\x00\x12\x1c\n\x17\x45RROR_CODE_UNAUTHORIZED\x10\xe8\x07\x12\x1f\n\x1a\x45RROR_CODE_INVALID_REQUEST\x10\xd0\x0f\x12(\n#ERROR_CODE_PROTOCOL_VERSION_MISSING\x10\xda\x0f\x12(\n#ERROR_CODE_PROTOCOL_VERSION_INVALID\x10\xe4\x0f\x12-\n(ERROR_CODE_PROTOCOL_VERSION_INCOMPATIBLE\x10\xee\x0f\x12 \n\x1b\x45RROR_CODE_TUNNEL_NOT_FOUND\x10\xb8\x17\x12\x1e\n\x19\x45RROR_CODE_INVALID_STREAM\x10\xa0\x1f\x12%\n ERROR_CODE_FEATURE_NOT_AVAILABLE\x10\x88\'\x12#\n\x1e\x45RROR_CODE_SERVICE_UNAVAILABLE\x10\xf0.\x12\"\n\x1d\x45RROR_CODE_CAPACITY_EXHAUSTED\x10\xd8\x36\x12!\n\x1c\x45RROR_CODE_RESOURCE_CONFLICT\x10\xc0>\x12\x18\n\x13\x45RROR_CODE_INTERNAL\x10\xa8\x46:8\n\x10protocol_version\x12\x1c.google.protobuf.FileOptions\x18\xb8\x8e\x03 \x01(\t:/\n\x06\x61\x63\x63\x65ss\x12\x1d.google.protobuf.FieldOptions\x18\xa2\x90\x03 \x01(\tB\x0fZ\x04./pb\xc2\xf3\x18\x05\x31.4.6b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'rstream_pb2', _globals) if not _descriptor._USE_C_DESCRIPTORS: _globals['DESCRIPTOR']._loaded_options = None - _globals['DESCRIPTOR']._serialized_options = b'Z\004./pb\302\363\030\0051.4.5' + _globals['DESCRIPTOR']._serialized_options = b'Z\004./pb\302\363\030\0051.4.6' _globals['_TUNNELPROPERTIES_LABELSENTRY']._loaded_options = None _globals['_TUNNELPROPERTIES_LABELSENTRY']._serialized_options = b'8\001' _globals['_TUNNELPROPERTIES'].fields_by_name['id']._loaded_options = None @@ -89,8 +89,10 @@ _globals['_TUNNELPROPERTIES'].fields_by_name['datagram_guaranteed_delivery']._serialized_options = b'\222\202\031\nread-write' _globals['_TUNNELPROPERTIES'].fields_by_name['allow_cross_region_routing']._loaded_options = None _globals['_TUNNELPROPERTIES'].fields_by_name['allow_cross_region_routing']._serialized_options = b'\222\202\031\nread-write' - _globals['_ERRORCODE']._serialized_start=5856 - _globals['_ERRORCODE']._serialized_end=6293 + _globals['_TUNNELPROPERTIES'].fields_by_name['http_cors']._loaded_options = None + _globals['_TUNNELPROPERTIES'].fields_by_name['http_cors']._serialized_options = b'\222\202\031\nread-write' + _globals['_ERRORCODE']._serialized_start=6085 + _globals['_ERRORCODE']._serialized_end=6557 _globals['_IPADDRESS']._serialized_start=143 _globals['_IPADDRESS']._serialized_end=190 _globals['_ERROR']._serialized_start=192 @@ -99,48 +101,50 @@ _globals['_CLIENTDETAILS']._serialized_end=642 _globals['_SERVERDETAILS']._serialized_start=645 _globals['_SERVERDETAILS']._serialized_end=983 - _globals['_TUNNELPROPERTIES']._serialized_start=986 - _globals['_TUNNELPROPERTIES']._serialized_end=2666 - _globals['_TUNNELPROPERTIES_LABELSENTRY']._serialized_start=2621 - _globals['_TUNNELPROPERTIES_LABELSENTRY']._serialized_end=2666 - _globals['_OPENCONTROLCHANNELREQ']._serialized_start=2669 - _globals['_OPENCONTROLCHANNELREQ']._serialized_end=2827 - _globals['_OPENCONTROLCHANNELRSP']._serialized_start=2830 - _globals['_OPENCONTROLCHANNELRSP']._serialized_end=3143 - _globals['_OPENCONTROLCHANNELRSP_OK']._serialized_start=2974 - _globals['_OPENCONTROLCHANNELRSP_OK']._serialized_end=3132 - _globals['_CLOSECONTROLCHANNELREQ']._serialized_start=3145 - _globals['_CLOSECONTROLCHANNELREQ']._serialized_end=3169 - _globals['_CLOSECONTROLCHANNELRSP']._serialized_start=3171 - _globals['_CLOSECONTROLCHANNELRSP']._serialized_end=3195 - _globals['_OPENTUNNELREQ']._serialized_start=3197 - _globals['_OPENTUNNELREQ']._serialized_end=3304 - _globals['_OPENTUNNELRSP']._serialized_start=3307 - _globals['_OPENTUNNELRSP']._serialized_end=3478 - _globals['_CLOSETUNNELREQ']._serialized_start=3480 - _globals['_CLOSETUNNELREQ']._serialized_end=3515 - _globals['_CLOSETUNNELRSP']._serialized_start=3517 - _globals['_CLOSETUNNELRSP']._serialized_end=3552 - _globals['_PROXYCONNREQ']._serialized_start=3555 - _globals['_PROXYCONNREQ']._serialized_end=3764 - _globals['_PROXYCONNRSP']._serialized_start=3766 - _globals['_PROXYCONNRSP']._serialized_end=3848 - _globals['_PROXYREQ']._serialized_start=3851 - _globals['_PROXYREQ']._serialized_end=3992 - _globals['_PROXYRSP']._serialized_start=3994 - _globals['_PROXYRSP']._serialized_end=4053 - _globals['_STREAMREQ']._serialized_start=4056 - _globals['_STREAMREQ']._serialized_end=4257 - _globals['_STREAMRSP']._serialized_start=4259 - _globals['_STREAMRSP']._serialized_end=4353 - _globals['_DATAGRAMCHANNELCLOSE']._serialized_start=4355 - _globals['_DATAGRAMCHANNELCLOSE']._serialized_end=4445 - _globals['_CONTROLCHANNELLIVENESS']._serialized_start=4447 - _globals['_CONTROLCHANNELLIVENESS']._serialized_end=4532 - _globals['_HEARTBEAT']._serialized_start=4534 - _globals['_HEARTBEAT']._serialized_end=4588 - _globals['_SERVERMESSAGE']._serialized_start=4590 - _globals['_SERVERMESSAGE']._serialized_end=4622 - _globals['_MESSAGE']._serialized_start=4625 - _globals['_MESSAGE']._serialized_end=5853 + _globals['_HTTPCORS']._serialized_start=986 + _globals['_HTTPCORS']._serialized_end=1140 + _globals['_TUNNELPROPERTIES']._serialized_start=1143 + _globals['_TUNNELPROPERTIES']._serialized_end=2895 + _globals['_TUNNELPROPERTIES_LABELSENTRY']._serialized_start=2850 + _globals['_TUNNELPROPERTIES_LABELSENTRY']._serialized_end=2895 + _globals['_OPENCONTROLCHANNELREQ']._serialized_start=2898 + _globals['_OPENCONTROLCHANNELREQ']._serialized_end=3056 + _globals['_OPENCONTROLCHANNELRSP']._serialized_start=3059 + _globals['_OPENCONTROLCHANNELRSP']._serialized_end=3372 + _globals['_OPENCONTROLCHANNELRSP_OK']._serialized_start=3203 + _globals['_OPENCONTROLCHANNELRSP_OK']._serialized_end=3361 + _globals['_CLOSECONTROLCHANNELREQ']._serialized_start=3374 + _globals['_CLOSECONTROLCHANNELREQ']._serialized_end=3398 + _globals['_CLOSECONTROLCHANNELRSP']._serialized_start=3400 + _globals['_CLOSECONTROLCHANNELRSP']._serialized_end=3424 + _globals['_OPENTUNNELREQ']._serialized_start=3426 + _globals['_OPENTUNNELREQ']._serialized_end=3533 + _globals['_OPENTUNNELRSP']._serialized_start=3536 + _globals['_OPENTUNNELRSP']._serialized_end=3707 + _globals['_CLOSETUNNELREQ']._serialized_start=3709 + _globals['_CLOSETUNNELREQ']._serialized_end=3744 + _globals['_CLOSETUNNELRSP']._serialized_start=3746 + _globals['_CLOSETUNNELRSP']._serialized_end=3781 + _globals['_PROXYCONNREQ']._serialized_start=3784 + _globals['_PROXYCONNREQ']._serialized_end=3993 + _globals['_PROXYCONNRSP']._serialized_start=3995 + _globals['_PROXYCONNRSP']._serialized_end=4077 + _globals['_PROXYREQ']._serialized_start=4080 + _globals['_PROXYREQ']._serialized_end=4221 + _globals['_PROXYRSP']._serialized_start=4223 + _globals['_PROXYRSP']._serialized_end=4282 + _globals['_STREAMREQ']._serialized_start=4285 + _globals['_STREAMREQ']._serialized_end=4486 + _globals['_STREAMRSP']._serialized_start=4488 + _globals['_STREAMRSP']._serialized_end=4582 + _globals['_DATAGRAMCHANNELCLOSE']._serialized_start=4584 + _globals['_DATAGRAMCHANNELCLOSE']._serialized_end=4674 + _globals['_CONTROLCHANNELLIVENESS']._serialized_start=4676 + _globals['_CONTROLCHANNELLIVENESS']._serialized_end=4761 + _globals['_HEARTBEAT']._serialized_start=4763 + _globals['_HEARTBEAT']._serialized_end=4817 + _globals['_SERVERMESSAGE']._serialized_start=4819 + _globals['_SERVERMESSAGE']._serialized_end=4851 + _globals['_MESSAGE']._serialized_start=4854 + _globals['_MESSAGE']._serialized_end=6082 # @@protoc_insertion_point(module_scope) diff --git a/src/rstream/_proto/rstream_pb2.pyi b/src/rstream/_proto/rstream_pb2.pyi index f7c2bfc..cc114d7 100644 --- a/src/rstream/_proto/rstream_pb2.pyi +++ b/src/rstream/_proto/rstream_pb2.pyi @@ -25,6 +25,7 @@ class ErrorCode(int, metaclass=_enum_type_wrapper.EnumTypeWrapper): ERROR_CODE_FEATURE_NOT_AVAILABLE: _ClassVar[ErrorCode] ERROR_CODE_SERVICE_UNAVAILABLE: _ClassVar[ErrorCode] ERROR_CODE_CAPACITY_EXHAUSTED: _ClassVar[ErrorCode] + ERROR_CODE_RESOURCE_CONFLICT: _ClassVar[ErrorCode] ERROR_CODE_INTERNAL: _ClassVar[ErrorCode] ERROR_CODE_UNSPECIFIED: ErrorCode ERROR_CODE_UNAUTHORIZED: ErrorCode @@ -37,6 +38,7 @@ ERROR_CODE_INVALID_STREAM: ErrorCode ERROR_CODE_FEATURE_NOT_AVAILABLE: ErrorCode ERROR_CODE_SERVICE_UNAVAILABLE: ErrorCode ERROR_CODE_CAPACITY_EXHAUSTED: ErrorCode +ERROR_CODE_RESOURCE_CONFLICT: ErrorCode ERROR_CODE_INTERNAL: ErrorCode PROTOCOL_VERSION_FIELD_NUMBER: _ClassVar[int] protocol_version: _descriptor.FieldDescriptor @@ -95,8 +97,24 @@ class ServerDetails(_message.Message): update: _wrappers_pb2.StringValue def __init__(self, agent: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., channel: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., version: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., plan: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., provider: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., region: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., update: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ...) -> None: ... +class HttpCors(_message.Message): + __slots__ = ("allowed_origins", "allowed_methods", "allowed_headers", "exposed_headers", "allow_credentials", "max_age") + ALLOWED_ORIGINS_FIELD_NUMBER: _ClassVar[int] + ALLOWED_METHODS_FIELD_NUMBER: _ClassVar[int] + ALLOWED_HEADERS_FIELD_NUMBER: _ClassVar[int] + EXPOSED_HEADERS_FIELD_NUMBER: _ClassVar[int] + ALLOW_CREDENTIALS_FIELD_NUMBER: _ClassVar[int] + MAX_AGE_FIELD_NUMBER: _ClassVar[int] + allowed_origins: _containers.RepeatedScalarFieldContainer[str] + allowed_methods: _containers.RepeatedScalarFieldContainer[str] + allowed_headers: _containers.RepeatedScalarFieldContainer[str] + exposed_headers: _containers.RepeatedScalarFieldContainer[str] + allow_credentials: bool + max_age: int + def __init__(self, allowed_origins: _Optional[_Iterable[str]] = ..., allowed_methods: _Optional[_Iterable[str]] = ..., allowed_headers: _Optional[_Iterable[str]] = ..., exposed_headers: _Optional[_Iterable[str]] = ..., allow_credentials: _Optional[bool] = ..., max_age: _Optional[int] = ...) -> None: ... + class TunnelProperties(_message.Message): - __slots__ = ("id", "creation_date", "name", "type", "publish", "protocol", "labels", "geoip", "trusted_ips", "host", "tls_mode", "tls_alpns", "tls_min_version", "tls_ciphers", "mtls_auth", "mtls_cacert_pem", "http_version", "http_use_tls", "token_auth", "rstream_auth", "challenge_mode", "hostname", "port", "upstream_tls", "datagram_guaranteed_delivery", "allow_cross_region_routing") + __slots__ = ("id", "creation_date", "name", "type", "publish", "protocol", "labels", "geoip", "trusted_ips", "host", "tls_mode", "tls_alpns", "tls_min_version", "tls_ciphers", "mtls_auth", "mtls_cacert_pem", "http_version", "http_use_tls", "token_auth", "rstream_auth", "challenge_mode", "hostname", "port", "upstream_tls", "datagram_guaranteed_delivery", "allow_cross_region_routing", "http_cors") class LabelsEntry(_message.Message): __slots__ = ("key", "value") KEY_FIELD_NUMBER: _ClassVar[int] @@ -130,6 +148,7 @@ class TunnelProperties(_message.Message): UPSTREAM_TLS_FIELD_NUMBER: _ClassVar[int] DATAGRAM_GUARANTEED_DELIVERY_FIELD_NUMBER: _ClassVar[int] ALLOW_CROSS_REGION_ROUTING_FIELD_NUMBER: _ClassVar[int] + HTTP_CORS_FIELD_NUMBER: _ClassVar[int] id: _wrappers_pb2.StringValue creation_date: _timestamp_pb2.Timestamp name: _wrappers_pb2.StringValue @@ -156,7 +175,8 @@ class TunnelProperties(_message.Message): upstream_tls: _wrappers_pb2.BoolValue datagram_guaranteed_delivery: _wrappers_pb2.BoolValue allow_cross_region_routing: _wrappers_pb2.BoolValue - def __init__(self, id: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., creation_date: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., name: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., type: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., publish: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., protocol: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., labels: _Optional[_Mapping[str, str]] = ..., geoip: _Optional[_Iterable[str]] = ..., trusted_ips: _Optional[_Iterable[str]] = ..., host: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_mode: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_alpns: _Optional[_Iterable[str]] = ..., tls_min_version: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_ciphers: _Optional[_Iterable[str]] = ..., mtls_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., mtls_cacert_pem: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., http_version: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., http_use_tls: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., token_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., rstream_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., challenge_mode: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., hostname: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., port: _Optional[_Union[_wrappers_pb2.UInt32Value, _Mapping]] = ..., upstream_tls: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., datagram_guaranteed_delivery: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., allow_cross_region_routing: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ...) -> None: ... + http_cors: HttpCors + def __init__(self, id: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., creation_date: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., name: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., type: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., publish: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., protocol: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., labels: _Optional[_Mapping[str, str]] = ..., geoip: _Optional[_Iterable[str]] = ..., trusted_ips: _Optional[_Iterable[str]] = ..., host: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_mode: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_alpns: _Optional[_Iterable[str]] = ..., tls_min_version: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., tls_ciphers: _Optional[_Iterable[str]] = ..., mtls_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., mtls_cacert_pem: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., http_version: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., http_use_tls: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., token_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., rstream_auth: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., challenge_mode: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., hostname: _Optional[_Union[_wrappers_pb2.StringValue, _Mapping]] = ..., port: _Optional[_Union[_wrappers_pb2.UInt32Value, _Mapping]] = ..., upstream_tls: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., datagram_guaranteed_delivery: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., allow_cross_region_routing: _Optional[_Union[_wrappers_pb2.BoolValue, _Mapping]] = ..., http_cors: _Optional[_Union[HttpCors, _Mapping]] = ...) -> None: ... class OpenControlChannelReq(_message.Message): __slots__ = ("client_details", "liveness") diff --git a/src/rstream/control.py b/src/rstream/control.py index 4f69d4e..94889a0 100644 --- a/src/rstream/control.py +++ b/src/rstream/control.py @@ -26,6 +26,7 @@ from rstream.stream import RstreamStream from rstream.tunnel import BytestreamTunnel from rstream.types import ( + HTTPCORS, CreateTunnelOptions, HTTPVersion, ServerDetails, @@ -125,6 +126,7 @@ async def create_tunnel( upstream_tls: bool | None = None, allow_cross_region_routing: bool | None = None, auth: TunnelAuth | None = None, + http_cors: HTTPCORS | None = None, ) -> BytestreamTunnel: explicit_options = CreateTunnelOptions( name=name, @@ -148,6 +150,7 @@ async def create_tunnel( upstream_tls=upstream_tls, allow_cross_region_routing=allow_cross_region_routing, auth=auth, + http_cors=http_cors, ) if options is not None and explicit_options != CreateTunnelOptions(): raise RuntimeError( @@ -494,6 +497,13 @@ def _preserve_payloads_after(error: BaseException) -> bool: def _normalize_bytestream_options(options: CreateTunnelOptions) -> TunnelProperties: + if options.http_cors is not None and ( + options.publish is False or options.protocol not in (None, "http") + ): + raise RuntimeError( + "CORS requires a published HTTP tunnel.", + code="ERR_RSTREAM_INVALID_TUNNEL", + ) if options.port is not None and options.protocol != "tcp": raise RuntimeError( "A published port requires protocol='tcp'.", @@ -564,6 +574,7 @@ def _normalize_bytestream_options(options: CreateTunnelOptions) -> TunnelPropert tls_ciphers=tuple(options.tls_ciphers), mtls_auth=options.mtls_auth, http_version=options.http_version, + http_cors=options.http_cors, token_auth=token_auth, rstream_auth=rstream_auth, challenge_mode=challenge_mode, diff --git a/src/rstream/engine_api.py b/src/rstream/engine_api.py index 7508761..6bf42f4 100644 --- a/src/rstream/engine_api.py +++ b/src/rstream/engine_api.py @@ -16,7 +16,7 @@ from rstream.config import TLSOptions, create_ssl_context from rstream.errors import RuntimeError -from rstream.types import TunnelProperties +from rstream.types import HTTPCORS, TunnelProperties WatchTransport = Literal["sse", "websocket"] @@ -149,7 +149,9 @@ def tunnel_inventory_from_json(data: Mapping[str, object]) -> TunnelInventory: def tunnel_properties_from_json(data: Mapping[str, object]) -> TunnelProperties: labels = data.get("labels") + cors = data.get("http_cors") return TunnelProperties( + http_cors=_http_cors_from_json(cors) if isinstance(cors, Mapping) else None, id=_optional_str(data, "id"), creation_date=_optional_datetime(data, "creation_date"), name=_optional_str(data, "name"), @@ -431,3 +433,14 @@ def _string_tuple(data: Mapping[str, object], key: str) -> tuple[str, ...]: if not isinstance(value, list): return () return tuple(item for item in value if isinstance(item, str)) + + +def _http_cors_from_json(data: Mapping[str, object]) -> HTTPCORS: + return HTTPCORS( + allowed_origins=_string_tuple(data, "allowed_origins"), + allowed_methods=_string_tuple(data, "allowed_methods"), + allowed_headers=_string_tuple(data, "allowed_headers"), + exposed_headers=_string_tuple(data, "exposed_headers"), + allow_credentials=_optional_bool(data, "allow_credentials") or False, + max_age=_optional_int(data, "max_age") or 0, + ) diff --git a/src/rstream/protocol.py b/src/rstream/protocol.py index b05c00c..54fda72 100644 --- a/src/rstream/protocol.py +++ b/src/rstream/protocol.py @@ -15,6 +15,7 @@ from rstream._proto import rstream_pb2 as pb from rstream.errors import EngineError, ProtocolError from rstream.types import ( + HTTPCORS, HTTPVersion, ServerDetails, TLSMode, @@ -159,11 +160,34 @@ def tunnel_properties_to_pb(properties: TunnelProperties) -> pb.TunnelProperties result.allow_cross_region_routing.CopyFrom( BoolValue(value=properties.allow_cross_region_routing) ) + if properties.http_cors is not None: + config = properties.http_cors + result.http_cors.CopyFrom( + pb.HttpCors( + allowed_origins=config.allowed_origins, + allowed_methods=config.allowed_methods, + allowed_headers=config.allowed_headers, + exposed_headers=config.exposed_headers, + allow_credentials=config.allow_credentials, + max_age=config.max_age, + ) + ) return result def tunnel_properties_from_pb(properties: pb.TunnelProperties) -> TunnelProperties: + config = properties.http_cors return TunnelProperties( + http_cors=HTTPCORS( + allowed_origins=tuple(config.allowed_origins), + allowed_methods=tuple(config.allowed_methods), + allowed_headers=tuple(config.allowed_headers), + exposed_headers=tuple(config.exposed_headers), + allow_credentials=config.allow_credentials, + max_age=config.max_age, + ) + if properties.HasField("http_cors") + else None, id=wrapper_string(optional_field(properties, "id", StringValue)), creation_date=wrapper_datetime( optional_field(properties, "creation_date", Timestamp) diff --git a/src/rstream/types.py b/src/rstream/types.py index 78bd4b1..d02f001 100644 --- a/src/rstream/types.py +++ b/src/rstream/types.py @@ -22,6 +22,18 @@ class TunnelAuth: challenge: bool | None = None +@dataclass(frozen=True) +class HTTPCORS: + """Optional browser access policy for a published HTTP tunnel.""" + + allowed_origins: tuple[str, ...] + allowed_methods: tuple[str, ...] = () + allowed_headers: tuple[str, ...] = () + exposed_headers: tuple[str, ...] = () + allow_credentials: bool = False + max_age: int = 0 + + @dataclass(frozen=True) class TunnelProperties: """Tunnel properties accepted by the engine.""" @@ -51,6 +63,7 @@ class TunnelProperties: upstream_tls: bool | None = None datagram_guaranteed_delivery: bool | None = None allow_cross_region_routing: bool | None = None + http_cors: HTTPCORS | None = None @dataclass(frozen=True) @@ -78,6 +91,7 @@ class CreateTunnelOptions: upstream_tls: bool | None = None allow_cross_region_routing: bool | None = None auth: TunnelAuth | None = None + http_cors: HTTPCORS | None = None @dataclass(frozen=True) diff --git a/tests/unit/test_http_cors.py b/tests/unit/test_http_cors.py new file mode 100644 index 0000000..9fa30cf --- /dev/null +++ b/tests/unit/test_http_cors.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import pytest + +from rstream import HTTPCORS, CreateTunnelOptions, TunnelProperties +from rstream._proto import rstream_pb2 as pb +from rstream.control import _normalize_bytestream_options +from rstream.engine_api import tunnel_properties_from_json +from rstream.errors import RuntimeError +from rstream.protocol import tunnel_properties_from_pb, tunnel_properties_to_pb + + +def test_policy_round_trip_and_absence() -> None: + policy = HTTPCORS( + allowed_origins=("https://dashboard.example",), + allowed_methods=("GET", "PUT"), + allowed_headers=("Authorization",), + exposed_headers=("ETag",), + allow_credentials=True, + max_age=600, + ) + properties = _normalize_bytestream_options(CreateTunnelOptions(http_cors=policy)) + wire = tunnel_properties_to_pb(properties).SerializeToString() + decoded = tunnel_properties_from_pb(pb.TunnelProperties.FromString(wire)) + assert decoded.http_cors == policy + assert not tunnel_properties_to_pb(TunnelProperties()).HasField("http_cors") + assert tunnel_properties_from_pb(pb.TunnelProperties()).http_cors is None + assert tunnel_properties_from_json( + { + "http_cors": { + "allowed_origins": ["https://dashboard.example"], + "max_age": 120, + } + } + ).http_cors == HTTPCORS(("https://dashboard.example",), max_age=120) + assert tunnel_properties_from_json({}).http_cors is None + + +@pytest.mark.parametrize( + "options", + [ + CreateTunnelOptions( + protocol="tls", http_cors=HTTPCORS(("https://dashboard.example",)) + ), + CreateTunnelOptions( + publish=False, http_cors=HTTPCORS(("https://dashboard.example",)) + ), + ], +) +def test_policy_requires_published_http(options: CreateTunnelOptions) -> None: + with pytest.raises(RuntimeError, match="published HTTP"): + _normalize_bytestream_options(options)