diff --git a/poetry.lock b/poetry.lock index a14165335..901ae569a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -976,14 +976,14 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyam [[package]] name = "tenacity" -version = "8.2.0" +version = "8.2.1" description = "Retry code until it succeeds" category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "tenacity-8.2.0-py3-none-any.whl", hash = "sha256:b723061a78ed0f4524190eae321d3d84100227d51c5677035b6615d91895e0d6"}, - {file = "tenacity-8.2.0.tar.gz", hash = "sha256:a43bcd8910406e0884ca0db3db7bed581f389c1d05165e992a1ddabfc81df05e"}, + {file = "tenacity-8.2.1-py3-none-any.whl", hash = "sha256:dd1b769ca7002fda992322939feca5bee4fa11f39146b0af14e0b8d9f27ea854"}, + {file = "tenacity-8.2.1.tar.gz", hash = "sha256:c7bb4b86425b977726a7b49971542d4f67baf72096597d283f3ffd01f33b92df"}, ] [package.extras] @@ -1100,16 +1100,95 @@ typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "websockets" +version = "10.4" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "websockets-10.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d58804e996d7d2307173d56c297cf7bc132c52df27a3efaac5e8d43e36c21c48"}, + {file = "websockets-10.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc0b82d728fe21a0d03e65f81980abbbcb13b5387f733a1a870672c5be26edab"}, + {file = "websockets-10.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ba089c499e1f4155d2a3c2a05d2878a3428cf321c848f2b5a45ce55f0d7d310c"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33d69ca7612f0ddff3316b0c7b33ca180d464ecac2d115805c044bf0a3b0d032"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62e627f6b6d4aed919a2052efc408da7a545c606268d5ab5bfab4432734b82b4"}, + {file = "websockets-10.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ea7b82bfcae927eeffc55d2ffa31665dc7fec7b8dc654506b8e5a518eb4d50"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e0cb5cc6ece6ffa75baccfd5c02cffe776f3f5c8bf486811f9d3ea3453676ce8"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:ae5e95cfb53ab1da62185e23b3130e11d64431179debac6dc3c6acf08760e9b1"}, + {file = "websockets-10.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7c584f366f46ba667cfa66020344886cf47088e79c9b9d39c84ce9ea98aaa331"}, + {file = "websockets-10.4-cp310-cp310-win32.whl", hash = "sha256:b029fb2032ae4724d8ae8d4f6b363f2cc39e4c7b12454df8df7f0f563ed3e61a"}, + {file = "websockets-10.4-cp310-cp310-win_amd64.whl", hash = "sha256:8dc96f64ae43dde92530775e9cb169979f414dcf5cff670455d81a6823b42089"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47a2964021f2110116cc1125b3e6d87ab5ad16dea161949e7244ec583b905bb4"}, + {file = "websockets-10.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e789376b52c295c4946403bd0efecf27ab98f05319df4583d3c48e43c7342c2f"}, + {file = "websockets-10.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d3f0b61c45c3fa9a349cf484962c559a8a1d80dae6977276df8fd1fa5e3cb8c"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f55b5905705725af31ccef50e55391621532cd64fbf0bc6f4bac935f0fccec46"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00c870522cdb69cd625b93f002961ffb0c095394f06ba8c48f17eef7c1541f96"}, + {file = "websockets-10.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f38706e0b15d3c20ef6259fd4bc1700cd133b06c3c1bb108ffe3f8947be15fa"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f2c38d588887a609191d30e902df2a32711f708abfd85d318ca9b367258cfd0c"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:fe10ddc59b304cb19a1bdf5bd0a7719cbbc9fbdd57ac80ed436b709fcf889106"}, + {file = "websockets-10.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:90fcf8929836d4a0e964d799a58823547df5a5e9afa83081761630553be731f9"}, + {file = "websockets-10.4-cp311-cp311-win32.whl", hash = "sha256:b9968694c5f467bf67ef97ae7ad4d56d14be2751000c1207d31bf3bb8860bae8"}, + {file = "websockets-10.4-cp311-cp311-win_amd64.whl", hash = "sha256:a7a240d7a74bf8d5cb3bfe6be7f21697a28ec4b1a437607bae08ac7acf5b4882"}, + {file = "websockets-10.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:74de2b894b47f1d21cbd0b37a5e2b2392ad95d17ae983e64727e18eb281fe7cb"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3a686ecb4aa0d64ae60c9c9f1a7d5d46cab9bfb5d91a2d303d00e2cd4c4c5cc"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d15c968ea7a65211e084f523151dbf8ae44634de03c801b8bd070b74e85033"}, + {file = "websockets-10.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00213676a2e46b6ebf6045bc11d0f529d9120baa6f58d122b4021ad92adabd41"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e23173580d740bf8822fd0379e4bf30aa1d5a92a4f252d34e893070c081050df"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dd500e0a5e11969cdd3320935ca2ff1e936f2358f9c2e61f100a1660933320ea"}, + {file = "websockets-10.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4239b6027e3d66a89446908ff3027d2737afc1a375f8fd3eea630a4842ec9a0c"}, + {file = "websockets-10.4-cp37-cp37m-win32.whl", hash = "sha256:8a5cc00546e0a701da4639aa0bbcb0ae2bb678c87f46da01ac2d789e1f2d2038"}, + {file = "websockets-10.4-cp37-cp37m-win_amd64.whl", hash = "sha256:a9f9a735deaf9a0cadc2d8c50d1a5bcdbae8b6e539c6e08237bc4082d7c13f28"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c1289596042fad2cdceb05e1ebf7aadf9995c928e0da2b7a4e99494953b1b94"}, + {file = "websockets-10.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0cff816f51fb33c26d6e2b16b5c7d48eaa31dae5488ace6aae468b361f422b63"}, + {file = "websockets-10.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dd9becd5fe29773d140d68d607d66a38f60e31b86df75332703757ee645b6faf"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45ec8e75b7dbc9539cbfafa570742fe4f676eb8b0d3694b67dabe2f2ceed8aa6"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f72e5cd0f18f262f5da20efa9e241699e0cf3a766317a17392550c9ad7b37d8"}, + {file = "websockets-10.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185929b4808b36a79c65b7865783b87b6841e852ef5407a2fb0c03381092fa3b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7d27a7e34c313b3a7f91adcd05134315002aaf8540d7b4f90336beafaea6217c"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:884be66c76a444c59f801ac13f40c76f176f1bfa815ef5b8ed44321e74f1600b"}, + {file = "websockets-10.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:931c039af54fc195fe6ad536fde4b0de04da9d5916e78e55405436348cfb0e56"}, + {file = "websockets-10.4-cp38-cp38-win32.whl", hash = "sha256:db3c336f9eda2532ec0fd8ea49fef7a8df8f6c804cdf4f39e5c5c0d4a4ad9a7a"}, + {file = "websockets-10.4-cp38-cp38-win_amd64.whl", hash = "sha256:48c08473563323f9c9debac781ecf66f94ad5a3680a38fe84dee5388cf5acaf6"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:40e826de3085721dabc7cf9bfd41682dadc02286d8cf149b3ad05bff89311e4f"}, + {file = "websockets-10.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:56029457f219ade1f2fc12a6504ea61e14ee227a815531f9738e41203a429112"}, + {file = "websockets-10.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5fc088b7a32f244c519a048c170f14cf2251b849ef0e20cbbb0fdf0fdaf556f"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fc8709c00704194213d45e455adc106ff9e87658297f72d544220e32029cd3d"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0154f7691e4fe6c2b2bc275b5701e8b158dae92a1ab229e2b940efe11905dff4"}, + {file = "websockets-10.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c6d2264f485f0b53adf22697ac11e261ce84805c232ed5dbe6b1bcb84b00ff0"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9bc42e8402dc5e9905fb8b9649f57efcb2056693b7e88faa8fb029256ba9c68c"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:edc344de4dac1d89300a053ac973299e82d3db56330f3494905643bb68801269"}, + {file = "websockets-10.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:84bc2a7d075f32f6ed98652db3a680a17a4edb21ca7f80fe42e38753a58ee02b"}, + {file = "websockets-10.4-cp39-cp39-win32.whl", hash = "sha256:c94ae4faf2d09f7c81847c63843f84fe47bf6253c9d60b20f25edfd30fb12588"}, + {file = "websockets-10.4-cp39-cp39-win_amd64.whl", hash = "sha256:bbccd847aa0c3a69b5f691a84d2341a4f8a629c6922558f2a70611305f902d74"}, + {file = "websockets-10.4-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:82ff5e1cae4e855147fd57a2863376ed7454134c2bf49ec604dfe71e446e2193"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d210abe51b5da0ffdbf7b43eed0cfdff8a55a1ab17abbec4301c9ff077dd0342"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:942de28af58f352a6f588bc72490ae0f4ccd6dfc2bd3de5945b882a078e4e179"}, + {file = "websockets-10.4-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9b27d6c1c6cd53dc93614967e9ce00ae7f864a2d9f99fe5ed86706e1ecbf485"}, + {file = "websockets-10.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:3d3cac3e32b2c8414f4f87c1b2ab686fa6284a980ba283617404377cd448f631"}, + {file = "websockets-10.4-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:da39dd03d130162deb63da51f6e66ed73032ae62e74aaccc4236e30edccddbb0"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389f8dbb5c489e305fb113ca1b6bdcdaa130923f77485db5b189de343a179393"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09a1814bb15eff7069e51fed0826df0bc0702652b5cb8f87697d469d79c23576"}, + {file = "websockets-10.4-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff64a1d38d156d429404aaa84b27305e957fd10c30e5880d1765c9480bea490f"}, + {file = "websockets-10.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b343f521b047493dc4022dd338fc6db9d9282658862756b4f6fd0e996c1380e1"}, + {file = "websockets-10.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:932af322458da7e4e35df32f050389e13d3d96b09d274b22a7aa1808f292fee4"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a4162139374a49eb18ef5b2f4da1dd95c994588f5033d64e0bbfda4b6b6fcf"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c57e4c1349fbe0e446c9fa7b19ed2f8a4417233b6984277cce392819123142d3"}, + {file = "websockets-10.4-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b627c266f295de9dea86bd1112ed3d5fafb69a348af30a2422e16590a8ecba13"}, + {file = "websockets-10.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:05a7233089f8bd355e8cbe127c2e8ca0b4ea55467861906b80d2ebc7db4d6b72"}, + {file = "websockets-10.4.tar.gz", hash = "sha256:eef610b23933c54d5d921c92578ae5f89813438fded840c2e9809d378dc765d3"}, +] + [[package]] name = "zipp" -version = "3.12.1" +version = "3.13.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.12.1-py3-none-any.whl", hash = "sha256:6c4fe274b8f85ec73c37a8e4e3fa00df9fb9335da96fb789e3b96b318e5097b3"}, - {file = "zipp-3.12.1.tar.gz", hash = "sha256:a3cac813d40993596b39ea9e93a18e8a2076d5c378b8bc88ec32ab264e04ad02"}, + {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"}, + {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"}, ] [package.extras] @@ -1119,4 +1198,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "29af23359858d18a743ff85426e7496fb7e47bdda6d746294aea4a360df6e617" +content-hash = "0a0cbe4cdf4f07a69b36c76988a3ae25f7d3856a6ab40ace5ee0682c357723d8" diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index 388b737a8..fedf85b8e 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/pynecone/.templates/web/utils/state.js @@ -139,10 +139,14 @@ export const updateState = async (state, setState, result, setResult, router, so * @param setResult The function to set the result. * @param endpoint The endpoint to connect to. */ -export const connect = async (socket, state, setState, result, setResult, router, endpoint) => { +export const connect = async (socket, state, setState, result, setResult, router, endpoint, transports) => { + // Get backend URL object from the endpoint + const endpoint_url = new URL(endpoint) // Create the socket. socket.current = io(endpoint, { - 'path': '/event', + path: endpoint_url['pathname'], + transports: transports, + autoUnref: false, }); // Once the socket is open, hydrate the page. diff --git a/pynecone/__init__.py b/pynecone/__init__.py index 72dc7e2cc..b0fcb441f 100644 --- a/pynecone/__init__.py +++ b/pynecone/__init__.py @@ -9,7 +9,7 @@ from .components import * from .components.component import custom_component as component from .components.graphing.victory import data from .config import Config -from .constants import Env +from .constants import Env, Transports from .event import EventChain, console_log, redirect, window_alert from .middleware import Middleware from .model import Model, session diff --git a/pynecone/app.py b/pynecone/app.py index c2a65b34c..5e67eaccb 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -35,6 +35,9 @@ class App(Base): # The Socket.IO AsyncServer. sio: Optional[AsyncServer] = None + # The socket app. + socket_app: Optional[ASGIApp] = None + # The state class to use for the app. state: Type[State] = DefaultState @@ -47,7 +50,7 @@ class App(Base): # Middleware to add to the app. middleware: List[Middleware] = [] - # events handlers to trigger when a page load + # Event handlers to trigger when a page loads. load_events: Dict[str, EventHandler] = {} def __init__(self, *args, **kwargs): @@ -59,6 +62,9 @@ class App(Base): """ super().__init__(*args, **kwargs) + # Get the config + config = utils.get_config() + # Add middleware. self.middleware.append(HydrateMiddleware()) @@ -68,21 +74,30 @@ class App(Base): # Set up the API. self.api = FastAPI() + # Set up CORS options. + cors_allowed_origins = config.cors_allowed_origins + if config.cors_allowed_origins == [constants.CORS_ALLOWED_ORIGINS]: + cors_allowed_origins = "*" + # Set up the Socket.IO AsyncServer. - self.sio = AsyncServer(async_mode="asgi", cors_allowed_origins="*") + self.sio = AsyncServer( + async_mode="asgi", + cors_allowed_origins=cors_allowed_origins, + cors_credentials=config.cors_credentials, + max_http_buffer_size=config.polling_max_http_buffer_size, + ) # Create the socket app. Note event endpoint constant replaces the default 'socket.io' path. - socket_app = ASGIApp(self.sio, socketio_path=str(constants.Endpoint.EVENT)) + self.socket_app = ASGIApp(self.sio, socketio_path="") - # Create the event namespace and attach the main app. Not related to the path above. - event_namespace = EventNamespace("/event") - event_namespace.app = self + # Create the event namespace and attach the main app. Not related to any paths. + event_namespace = EventNamespace("/event", self) # Register the event namespace with the socket. self.sio.register_namespace(event_namespace) # Mount the socket app with the API. - self.api.mount("/", socket_app) + self.api.mount(str(constants.Endpoint.EVENT), self.socket_app) def __repr__(self) -> str: """Get the string representation of the app. @@ -324,12 +339,17 @@ class App(Base): compiler.compile_components(custom_components) -async def process(app: App, event: Event) -> StateUpdate: +async def process( + app: App, event: Event, sid: str, headers: Dict, client_ip: str +) -> StateUpdate: """Process an event. Args: app: The app to process the event for. event: The event to process. + sid: The Socket.IO session id. + headers: The client headers. + client_ip: The client_ip. Returns: The state update after processing the event. @@ -337,12 +357,15 @@ async def process(app: App, event: Event) -> StateUpdate: # Get the state for the session. state = app.state_manager.get_state(event.token) - # pass router_data to the state of the App + # Pass router_data to the state of the App. state.router_data = event.router_data # also pass router_data to all substates for _, substate in state.substates.items(): substate.router_data = event.router_data state.router_data[constants.RouteVar.CLIENT_TOKEN] = event.token + state.router_data[constants.RouteVar.SESSION_ID] = sid + state.router_data[constants.RouteVar.HEADERS] = headers + state.router_data[constants.RouteVar.CLIENT_IP] = client_ip # Preprocess the event. pre = app.preprocess(state, event) @@ -365,9 +388,19 @@ async def process(app: App, event: Event) -> StateUpdate: class EventNamespace(AsyncNamespace): """The event namespace.""" - # The backend API object. + # The application object. app: App + def __init__(self, namespace: str, app: App): + """Initialize the event namespace. + + Args: + namespace: The namespace. + app: The application object. + """ + super().__init__(namespace) + self.app = app + def on_connect(self, sid, environ): """Event for when the websocket disconnects. @@ -395,8 +428,21 @@ class EventNamespace(AsyncNamespace): # Get the event. event = Event.parse_raw(data) + # Get the event environment. + assert self.app.sio is not None + environ = self.app.sio.get_environ(sid, self.namespace) + + # Get the client headers. + headers = { + k.decode("utf-8"): v.decode("utf-8") + for (k, v) in environ["asgi.scope"]["headers"] + } + + # Get the client IP + client_ip = environ["REMOTE_ADDR"] + # Process the event. - update = await process(self.app, event) + update = await process(self.app, event, sid, headers, client_ip) # Emit the event. await self.emit(str(constants.SocketEvent.EVENT), update.json(), to=sid) diff --git a/pynecone/compiler/templates.py b/pynecone/compiler/templates.py index b265210ec..02ddda664 100644 --- a/pynecone/compiler/templates.py +++ b/pynecone/compiler/templates.py @@ -165,7 +165,7 @@ USE_EFFECT = join( " return;", " }}", f" if (!{SOCKET}.current) {{{{", - f" connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT})", + f" connect({SOCKET}, {{state}}, {{set_state}}, {RESULT}, {SET_RESULT}, {ROUTER}, {EVENT_ENDPOINT}, {{transports}})", " }}", " const update = async () => {{", f" if ({RESULT}.{STATE} != null) {{{{", diff --git a/pynecone/compiler/utils.py b/pynecone/compiler/utils.py index 40a072bb3..7263df013 100644 --- a/pynecone/compiler/utils.py +++ b/pynecone/compiler/utils.py @@ -150,7 +150,10 @@ def compile_effects(state: Type[State]) -> str: """ state_name = state.get_name() set_state = templates.format_state_setter(state_name) - return templates.USE_EFFECT(state=state_name, set_state=set_state) + transports = constants.Transports.POLLING_WEBSOCKET.get_transports() + return templates.USE_EFFECT( + state=state_name, set_state=set_state, transports=transports + ) def compile_render(component: Component) -> str: diff --git a/pynecone/config.py b/pynecone/config.py index 7bd905d7c..f799febf6 100644 --- a/pynecone/config.py +++ b/pynecone/config.py @@ -38,3 +38,17 @@ class Config(Base): # Additional frontend packages to install. frontend_packages: List[str] = [] + + # Backend transport methods. + backend_transports: Optional[ + constants.Transports + ] = constants.Transports.POLLING_WEBSOCKET + + # List of origins that are allowed to connect to the backend API. + cors_allowed_origins: Optional[list] = [constants.CORS_ALLOWED_ORIGINS] + + # Whether credentials (cookies, authentication) are allowed in requests to the backend API. + cors_credentials: Optional[bool] = True + + # The maximum size of a message when using the polling backend transport. + polling_max_http_buffer_size: Optional[int] = constants.POLLING_MAX_HTTP_BUFFER_SIZE diff --git a/pynecone/constants.py b/pynecone/constants.py index b66859509..a7c199d7e 100644 --- a/pynecone/constants.py +++ b/pynecone/constants.py @@ -206,6 +206,36 @@ class SocketEvent(Enum): return str(self.value) +class Transports(Enum): + """Socket transports used by the pynecone backend API.""" + + POLLING_WEBSOCKET = "['polling', 'websocket']" + WEBSOCKET_POLLING = "['websocket', 'polling']" + WEBSOCKET_ONLY = "['websocket']" + POLLING_ONLY = "['polling']" + + def __str__(self) -> str: + """Get the string representation of the transports. + + Returns: + The transports string. + """ + return str(self.value) + + def get_transports(self) -> str: + """Get the transports config for the backend. + + Returns: + The transports config for the backend. + """ + # Import here to avoid circular imports. + from pynecone import utils + + # Get the transports from the config. + config = utils.get_config() + return str(config.backend_transports) + + class RouteArgType(SimpleNamespace): """Type of dynamic route arg extracted from URI route.""" @@ -217,8 +247,11 @@ class RouteArgType(SimpleNamespace): class RouteVar(SimpleNamespace): """Names of variables used in the router_data dict stored in State.""" + CLIENT_IP = "ip" CLIENT_TOKEN = "token" + HEADERS = "headers" PATH = "pathname" + SESSION_ID = "sid" QUERY = "query" @@ -245,3 +278,7 @@ DESCRIPTION_404 = "The page was not found" USE_COLOR_MODE = "useColorMode" COLOR_MODE = "colorMode" TOGGLE_COLOR_MODE = "toggleColorMode" + +# Server socket configuration variables +CORS_ALLOWED_ORIGINS = "*" +POLLING_MAX_HTTP_BUFFER_SIZE = 1000 * 1000 diff --git a/pynecone/state.py b/pynecone/state.py index a499a9342..75db05045 100644 --- a/pynecone/state.py +++ b/pynecone/state.py @@ -354,6 +354,30 @@ class State(Base, ABC): """ return self.router_data.get(constants.RouteVar.CLIENT_TOKEN, "") + def get_sid(self) -> str: + """Return the session ID of the client associated with this state. + + Returns: + The session ID of the client. + """ + return self.router_data.get(constants.RouteVar.SESSION_ID, "") + + def get_headers(self) -> Dict: + """Return the headers of the client associated with this state. + + Returns: + The headers of the client. + """ + return self.router_data.get(constants.RouteVar.HEADERS, {}) + + def get_client_ip(self) -> str: + """Return the IP of the client associated with this state. + + Returns: + The IP of the client. + """ + return self.router_data.get(constants.RouteVar.CLIENT_IP, "") + def get_current_page(self) -> str: """Obtain the path of current page from the router data. diff --git a/pyproject.toml b/pyproject.toml index 2f86b5f9d..56a82f091 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ redis = "^4.3.5" httpx = "^0.23.1" python-socketio = "^5.7.2" psutil = "^5.9.4" +websockets = "^10.4" [tool.poetry.dev-dependencies] pytest = "^7.1.2" diff --git a/tests/test_state.py b/tests/test_state.py index fc505cb68..db2de362d 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -627,6 +627,48 @@ def test_get_token(test_state): assert test_state.get_token() == token +def test_get_sid(test_state): + """Test getting session id. + + Args: + test_state: A state. + """ + assert test_state.get_sid() == "" + + sid = "9fpxSzPb9aFMb4wFAAAH" + test_state.router_data = {RouteVar.SESSION_ID: sid} + + assert test_state.get_sid() == sid + + +def test_get_headers(test_state): + """Test getting client headers. + + Args: + test_state: A state. + """ + assert test_state.get_headers() == {} + + headers = {"host": "localhost:8000", "connection": "keep-alive"} + test_state.router_data = {RouteVar.HEADERS: headers} + + assert test_state.get_headers() == headers + + +def test_get_client_ip(test_state): + """Test getting client IP. + + Args: + test_state: A state. + """ + assert test_state.get_client_ip() == "" + + client_ip = "127.0.0.1" + test_state.router_data = {RouteVar.CLIENT_IP: client_ip} + + assert test_state.get_client_ip() == client_ip + + def test_get_current_page(test_state): assert test_state.get_current_page() == ""