├── .gitignore ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements.txt └── websockets_proxy ├── __init__.py └── websockets_proxy.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .venv 3 | venv* 4 | .idea/ 5 | dist 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Andrei Karavatski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websockets_proxy 2 | 3 | This module will enable you to use [websockets](https://github.com/python-websockets/websockets) package with proxies. 4 | 5 | Proxy heavy-lifting is done by [python-socks](https://github.com/romis2012/python-socks) package. 6 | 7 | # Usage 8 | 9 | To install, use: 10 | 11 | ``` 12 | pip install websockets_proxy 13 | ``` 14 | 15 | All you need is create a proxy object and pass it into `proxy_connect` constructor: 16 | 17 | ```python 18 | from websockets_proxy import Proxy, proxy_connect 19 | 20 | 21 | proxy = Proxy.from_url("...") 22 | async with proxy_connect("wss://example.com", proxy=proxy) as ws: 23 | ... 24 | ``` 25 | 26 | `proxy_connect` constructor accepts the same arguments as regular `connect` constructor, 27 | with the addition of `proxy` and `proxy_conn_timeout`, which are self-explanatory. 28 | 29 | > **With this patch you cannot use `sock` keyword argument, because it is used to connect to a proxy server.** 30 | 31 | > **You must create your `Proxy` objects within a running event loop** (inside an `async` function). 32 | > This is a [python-socks](https://github.com/romis2012/python-socks) limitation. 33 | > If you define your `Proxy` objects outside of running event loop, you'll get this kind of error: `RuntimeError: Task attached to a different loop`. 34 | 35 | # WebSocket proxy checker 36 | 37 | ## Server 38 | Here is a simple `aiohttp` server, which allows you to check, if your proxy connections work as expected: 39 | 40 | ```python 41 | import asyncio 42 | 43 | from aiohttp import web, WSMsgType, WSMessage 44 | 45 | 46 | HOST = '0.0.0.0' 47 | PORT = 9999 48 | 49 | app = web.Application() 50 | 51 | 52 | async def websocket_handler(request: web.Request) -> web.StreamResponse: 53 | ws = web.WebSocketResponse() 54 | await ws.prepare(request) 55 | await ws.send_str(str(request.remote)) 56 | await ws.close() 57 | print('accepted connection from', request.remote) 58 | return ws 59 | 60 | 61 | app.add_routes([ 62 | web.get('/', websocket_handler) 63 | ]) 64 | 65 | 66 | async def main(): 67 | runner = web.AppRunner(app) 68 | await runner.setup() 69 | site = web.TCPSite(runner, host=HOST, port=PORT) 70 | await site.start() 71 | print('server is running') 72 | await asyncio.Future() 73 | 74 | 75 | if __name__ == '__main__': 76 | asyncio.run(main()) 77 | ``` 78 | 79 | 80 | ## Client 81 | An example of a client-side proxy checker script: 82 | 83 | ```python 84 | import asyncio 85 | 86 | from websockets_proxy import Proxy, proxy_connect 87 | from websockets import connect 88 | 89 | 90 | # this script is written with the above checker server in mind 91 | CHECKER_URL = 'ws://address:port' 92 | 93 | 94 | async def main(): 95 | async with connect(CHECKER_URL) as ws: 96 | async for msg in ws: 97 | ip_no_proxy = msg 98 | print("Your IP:", ip_no_proxy) 99 | print('.') 100 | # be sure to create your "Proxy" objects inside an async function 101 | proxy = Proxy.from_url("http://login:password@address:port") 102 | async with proxy_connect(CHECKER_URL, proxy=proxy) as ws: 103 | async for msg in ws: 104 | ip_with_proxy = msg 105 | print("(async with) Proxy IP", ip_with_proxy) 106 | print('.') 107 | 108 | ws = await proxy_connect(CHECKER_URL, proxy=proxy) 109 | async for msg in ws: 110 | ip_with_proxy = msg 111 | print("(await) Proxy IP", ip_with_proxy) 112 | await ws.close() 113 | print('.') 114 | 115 | 116 | if __name__ == "__main__": 117 | asyncio.run(main()) 118 | 119 | ``` 120 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "websockets_proxy" 7 | version = "0.1.3" 8 | requires-python = ">= 3.8" 9 | authors = [ 10 | {name = "Andrei Karavatski", email = "verolomnyy@gmail.com"} 11 | ] 12 | maintainers = [ 13 | {name = "Andrei Karavatski", email = "verolomnyy@gmail.com"} 14 | ] 15 | description = "Connect to websockets through a proxy server." 16 | readme = {file = "README.md", content-type = "text/markdown"} 17 | license = {file = "LICENSE"} 18 | classifiers = [ 19 | "Development Status :: 5 - Production/Stable", 20 | "Intended Audience :: Developers", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | "Programming Language :: Python", 24 | "Programming Language :: Python :: 3", 25 | "Programming Language :: Python :: 3 :: Only", 26 | "Programming Language :: Python :: 3.8", 27 | "Programming Language :: Python :: 3.9", 28 | "Programming Language :: Python :: 3.10", 29 | "Programming Language :: Python :: 3.11", 30 | "Programming Language :: Python :: 3.12", 31 | "Programming Language :: Python :: 3.13", 32 | "Framework :: AsyncIO", 33 | "Topic :: Internet :: WWW/HTTP", 34 | "Topic :: Software Development :: Libraries", 35 | "Topic :: Software Development :: Libraries :: Python Modules", 36 | "Typing :: Typed", 37 | "Topic :: Internet :: Proxy Servers" 38 | ] 39 | keywords = [ 40 | "async", 41 | "websockets", 42 | "websocket client", 43 | "websocket proxy", 44 | "websockets proxy", 45 | "http", 46 | "https", 47 | "ws", 48 | "wss", 49 | "socks5", 50 | "socks4", 51 | "http proxy", 52 | ] 53 | dependencies = [ 54 | "python-socks[asyncio]", 55 | "websockets" 56 | ] 57 | 58 | [project.urls] 59 | Homepage = "https://github.com/racinette/websockets_proxy" 60 | Issues = "https://github.com/racinette/websockets_proxy/issues" 61 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websockets 2 | python-socks[asyncio] -------------------------------------------------------------------------------- /websockets_proxy/__init__.py: -------------------------------------------------------------------------------- 1 | from .websockets_proxy import proxy_connect, Proxy 2 | 3 | 4 | __all__ = [ 5 | "proxy_connect", 6 | "Proxy" 7 | ] 8 | -------------------------------------------------------------------------------- /websockets_proxy/websockets_proxy.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | from urllib.parse import urlparse 3 | 4 | from websockets.legacy.client import Connect, WebSocketClientProtocol 5 | from python_socks.async_.asyncio import Proxy 6 | 7 | 8 | class ProxyConnect(Connect): 9 | def __init__( # noqa 10 | self, 11 | uri: str, 12 | *, 13 | proxy: Optional[Proxy], 14 | proxy_conn_timeout: Optional[Union[int, float]] = None, 15 | **kwargs, 16 | ) -> None: 17 | self.uri = uri 18 | # This looks strange, but 19 | if "sock" in kwargs: 20 | raise ValueError( 21 | "do not supply your own 'sock' kwarg - it's used internally by the wrapper" 22 | ) 23 | kwargs.pop("host", None) 24 | kwargs.pop("port", None) 25 | u = urlparse(uri) 26 | host = u.hostname 27 | if u.port: 28 | port = u.port 29 | else: 30 | if u.scheme == "ws": 31 | port = 80 32 | elif u.scheme == "wss": 33 | port = 443 34 | # setting for ssl (because we specify sock instead of host, port) 35 | kwargs["server_hostname"] = host 36 | else: 37 | raise ValueError("unknown scheme") 38 | self.__proxy: Proxy = proxy 39 | self.__proxy_conn_timeout: Optional[Union[int, float]] = ( 40 | proxy_conn_timeout 41 | ) 42 | self.__host: str = host 43 | self.__port: int = port 44 | # pass it to the super().__init__ call 45 | self.__kwargs = kwargs 46 | # HACK: We deliberately don't call 47 | # the super().__init__ constructor method YET! 48 | 49 | def set_proxy(self, proxy: Proxy) -> None: 50 | self.__proxy = proxy 51 | 52 | async def __await_impl_proxy__(self) -> WebSocketClientProtocol: 53 | # creating patched socket 54 | sock = await self.__proxy.connect( 55 | dest_host=self.__host, 56 | dest_port=self.__port, 57 | timeout=self.__proxy_conn_timeout, # type: ignore 58 | ) 59 | self.__kwargs["sock"] = sock 60 | # HACK: THE super().__init__ IS DELIBERATELY CALLED HERE! 61 | # It is because we need an already 62 | # connected socket object inside the constructor, 63 | # but we've only just got it inside of this method 64 | super().__init__(self.uri, **self.__kwargs) # noqa 65 | try: 66 | await_impl = getattr(self, "__await_impl_timeout__") 67 | except AttributeError: 68 | # newer versions removed the __await_impl_timeout__ method 69 | await_impl = getattr(self, "__await_impl__") 70 | proto = await await_impl() 71 | return proto 72 | 73 | def __await__(self): 74 | return self.__await_impl_proxy__().__await__() 75 | 76 | 77 | proxy_connect = ProxyConnect 78 | 79 | __all__ = ["proxy_connect", "Proxy"] 80 | --------------------------------------------------------------------------------