├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── PyRoxy ├── Exceptions │ └── __init__.py ├── GeoIP │ ├── Sqlite │ │ ├── COPYRIGHT.txt │ │ ├── GeoLite2-Country.mmdb │ │ └── LICENSE.txt │ └── __init__.py ├── Tools │ └── __init__.py └── __init__.py ├── README.md ├── requirements.txt ├── setup.py └── test.py /.gitignore: -------------------------------------------------------------------------------- 1 | /venv/ 2 | */__pycache__ 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MH_ProDev 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. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | recursive-include PyRoxy.GeoIP GeoLite2-Country.mmdb 3 | recursive-include PyRoxy.GeoIP *.txt 4 | -------------------------------------------------------------------------------- /PyRoxy/Exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["ProxyInvalidHost", "ProxyInvalidPort", "ProxyParseError"] 2 | 3 | 4 | class ProxyParseError(Exception): 5 | pass 6 | 7 | 8 | class ProxyInvalidPort(ProxyParseError): 9 | 10 | def __init__(self, port: int): 11 | ProxyParseError.__init__( 12 | self, "'%d' is too %s" % (port, "small" if port < 1 else "long")) 13 | 14 | 15 | class ProxyInvalidHost(ProxyParseError): 16 | 17 | def __init__(self, host: str): 18 | ProxyParseError.__init__(self, "'%s' is an Invalid IP Address" % host) 19 | -------------------------------------------------------------------------------- /PyRoxy/GeoIP/Sqlite/COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Database and Contents Copyright (c) 2022 MaxMind, Inc. 2 | -------------------------------------------------------------------------------- /PyRoxy/GeoIP/Sqlite/GeoLite2-Country.mmdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MatrixTM/PyRoxy/ea0f88dbc0573292ba5672124f69e4e7a31b544d/PyRoxy/GeoIP/Sqlite/GeoLite2-Country.mmdb -------------------------------------------------------------------------------- /PyRoxy/GeoIP/Sqlite/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Use of this MaxMind product is governed by MaxMind's GeoLite2 End User License Agreement, which can be viewed at https://www.maxmind.com/en/geolite2/eula. 2 | 3 | This database incorporates GeoNames [https://www.geonames.org] geographical data, which is made available under the Creative Commons Attribution 4.0 License. To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/. 4 | -------------------------------------------------------------------------------- /PyRoxy/GeoIP/__init__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path as _path 2 | from maxminddb import open_database as _open 3 | 4 | __dir__ = _path(__file__).parent 5 | __all__ = [ "get", "get_with_prefix_len"] 6 | 7 | _reader = _open(__dir__ / 'Sqlite/GeoLite2-Country.mmdb') 8 | 9 | def get(ip: str): 10 | return _reader.get(ip) 11 | 12 | 13 | def get_with_prefix_len(ip: str): 14 | return _reader.get_with_prefix_len(ip) 15 | -------------------------------------------------------------------------------- /PyRoxy/Tools/__init__.py: -------------------------------------------------------------------------------- 1 | from re import compile, IGNORECASE, MULTILINE 2 | from contextlib import suppress 3 | from os import urandom 4 | from socket import inet_ntop, inet_ntoa, AF_INET6 5 | from string import ascii_letters 6 | from struct import pack 7 | from struct import pack as data_pack 8 | from sys import maxsize 9 | from typing import Callable, Any, List 10 | 11 | __all__ = ["Patterns", "Random"] 12 | 13 | 14 | class Random: 15 | latters: List[str] = list(ascii_letters) 16 | rand_str: Callable[[int], str] = lambda length=16: ''.join( 17 | Random.rand_choice(*Random.latters) for _ in range(length)) 18 | rand_char: Callable[[int], chr] = lambda length=16: "".join( 19 | [chr(Random.rand_int(0, 1000)) for _ in range(length)]) 20 | rand_ipv4: Callable[[], str] = lambda: inet_ntoa( 21 | data_pack('>I', Random.rand_int(1, 0xffffffff))) 22 | rand_ipv6: Callable[[], str] = lambda: inet_ntop( 23 | AF_INET6, pack('>QQ', Random.rand_bits(64), Random.rand_bits(64))) 24 | rand_int: Callable[[int, int], 25 | int] = lambda minimum=0, maximum=maxsize: int( 26 | Random.rand_float(minimum, maximum)) 27 | rand_choice: Callable[[List[Any]], Any] = lambda *data: data[ 28 | (Random.rand_int(maximum=len(data) - 2) or 0)] 29 | rand: Callable[ 30 | [], int] = lambda: (int.from_bytes(urandom(7), 'big') >> 3) * (2**-53) 31 | 32 | @staticmethod 33 | def rand_bits(maximum: int = 255) -> int: 34 | numbytes = (maximum + 7) // 8 35 | return int.from_bytes(urandom(numbytes), 36 | 'big') >> (numbytes * 8 - maximum) 37 | 38 | @staticmethod 39 | def rand_float(minimum: float = 0.0, 40 | maximum: float = (maxsize * 1.0)) -> float: 41 | with suppress(ZeroDivisionError): 42 | return abs((Random.rand() * maximum) % (minimum - 43 | (maximum + 1))) + minimum 44 | return 0.0 45 | 46 | 47 | class Patterns: 48 | Port = compile( 49 | "^((6553[0-5])|(655[0-2][0-9])|(65[0-4][0-9]" 50 | "{2})|(6[0-4][0-9]{3})|([1-5][0-9]{4})|([0-5]{0,5})|([0-9]{1,4}))$") 51 | IP = compile("(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)") 52 | IPPort = compile("^((?:\d{1,3}\.){3}\d{1,3}):(\d{1,5})$") 53 | Proxy = compile( 54 | r"^(?:\[|)(?:\s+|)((?:socks[45]|http(?:s|)))(?:[]|]|)(?:\s+|)(?:](?:\s+|)|\|(?:\s+|)|://(?:\s+|)|)" 55 | r"((?:(?:\d+.){3}\d+|\S+[.]\w{2,3}))" 56 | r"(?:[:]|)((?:\d+|))" 57 | r"(?::(.+):(.+)|)$", IGNORECASE | MULTILINE) 58 | URL = compile("\S+[.]\w{2,3}") 59 | -------------------------------------------------------------------------------- /PyRoxy/__init__.py: -------------------------------------------------------------------------------- 1 | from concurrent.futures import ThreadPoolExecutor, as_completed 2 | from contextlib import suppress 3 | from enum import IntEnum, auto 4 | from ipaddress import ip_address 5 | from multiprocessing import cpu_count 6 | from pathlib import Path 7 | from socket import socket, SOCK_STREAM, AF_INET, gethostbyname 8 | from typing import AnyStr, Set, Collection, Any 9 | 10 | from socks import socksocket, SOCKS4, SOCKS5, HTTP 11 | from yarl import URL 12 | 13 | from PyRoxy import GeoIP, Tools 14 | from PyRoxy.Exceptions import ProxyInvalidHost, ProxyInvalidPort, ProxyParseError 15 | from PyRoxy.Tools import Patterns 16 | 17 | __version__ = "1.0b5" 18 | __auther__ = "MH_ProDev" 19 | __all__ = ["ProxyUtiles", "ProxyType", "ProxySocket", "ProxyChecker", "Proxy"] 20 | 21 | 22 | class ProxyType(IntEnum): 23 | HTTP = auto() 24 | HTTPS = auto() 25 | SOCKS4 = auto() 26 | SOCKS5 = auto() 27 | 28 | def asPySocksType(self): 29 | return SOCKS5 if self == ProxyType.SOCKS5 else \ 30 | SOCKS4 if self == ProxyType.SOCKS4 else \ 31 | HTTP 32 | 33 | @staticmethod 34 | def stringToProxyType(n: str): 35 | return (ProxyType.SOCKS5 if n.lower() == "socks5" else 36 | ProxyType.SOCKS4 if n.lower() == "socks4" else ProxyType.HTTP 37 | ) if not (n.isdigit()) else ( 38 | ProxyType.SOCKS5 if int(n) == 5 else 39 | ProxyType.SOCKS4 if int(n) == 4 else ProxyType.HTTP) 40 | 41 | 42 | class Proxy(object): 43 | user: Any 44 | password: Any 45 | country: Any 46 | port: int 47 | type: ProxyType 48 | host: AnyStr 49 | 50 | def __init__(self, 51 | host: str, 52 | port: int = 0, 53 | proxy_type: ProxyType = ProxyType.HTTP, 54 | user=None, 55 | password=None): 56 | if Patterns.URL.match(host): host = gethostbyname(host) 57 | assert self.validate(host, port) 58 | self.host = host 59 | self.type = proxy_type 60 | self.port = port 61 | self.country = GeoIP.get(host) 62 | if self.country: 63 | self.country = self.country["registered_country"]["iso_code"] 64 | self.user = user or None 65 | self.password = password or None 66 | 67 | def __str__(self): 68 | return "%s://%s:%d%s" % (self.type.name.lower(), self.host, self.port, 69 | (":%s:%s" % (self.user, self.password) 70 | if self.password and self.user else "")) 71 | 72 | def __repr__(self): 73 | return "<%s %s Proxy %s:%d>" % (self.type.name, self.country.upper(), 74 | self.host, self.port) 75 | 76 | @staticmethod 77 | def fromString(string: str): 78 | with suppress(Exception): 79 | proxy: Any = Patterns.Proxy.search(string) 80 | return Proxy( 81 | proxy.group(2), 82 | int(proxy.group(3)) 83 | if proxy.group(3) and proxy.group(3).isdigit() else 80, 84 | ProxyType.stringToProxyType(proxy.group(1)), proxy.group(4), 85 | proxy.group(5)) 86 | return None 87 | 88 | def ip_port(self): 89 | return "%s:%d" % (self.host, self.port) 90 | 91 | @staticmethod 92 | def validate(host: str, port: int): 93 | with suppress(ValueError): 94 | if not ip_address(host): 95 | raise ProxyInvalidHost(host) 96 | if not Tools.Patterns.Port.match(str(port)): 97 | raise ProxyInvalidPort(port) 98 | return True 99 | raise ProxyInvalidHost(host) 100 | 101 | # noinspection PyShadowingBuiltins 102 | def open_socket(self, 103 | family=AF_INET, 104 | type=SOCK_STREAM, 105 | proto=-1, 106 | fileno=None): 107 | return ProxySocket(self, family, type, proto, fileno) 108 | 109 | def wrap(self, sock: Any): 110 | if isinstance(sock, socket): 111 | return self.open_socket(sock.family, sock.type, sock.proto, 112 | sock.fileno()) 113 | sock.proxies = self.asRequest() 114 | return sock 115 | 116 | def asRequest(self): 117 | return { 118 | "http": self.__str__(), 119 | "https": self.__str__().replace("http://", "https://") 120 | } 121 | 122 | # noinspection PyUnreachableCode 123 | def check(self, url: Any = "https://httpbin.org/get", timeout=5): 124 | if not isinstance(url, URL): url = URL(url) 125 | with suppress(Exception): 126 | with self.open_socket() as sock: 127 | sock.settimeout(timeout) 128 | sock.connect((url.host, url.port or 80)) 129 | return True 130 | return False 131 | 132 | 133 | # noinspection PyShadowingBuiltins 134 | class ProxySocket(socksocket): 135 | 136 | def __init__(self, 137 | proxy: Proxy, 138 | family=-1, 139 | type=-1, 140 | proto=-1, 141 | fileno=None): 142 | super().__init__(family, type, proto, fileno) 143 | if proxy.port: 144 | if proxy.user and proxy.password: 145 | self.setproxy(proxy.type.asPySocksType(), 146 | proxy.host, 147 | proxy.port, 148 | username=proxy.user, 149 | password=proxy.password) 150 | return 151 | self.setproxy(proxy.type.asPySocksType(), proxy.host, proxy.port) 152 | return 153 | if proxy.user and proxy.password: 154 | self.setproxy(proxy.type.asPySocksType(), 155 | proxy.host, 156 | username=proxy.user, 157 | password=proxy.password) 158 | return 159 | self.setproxy(proxy.type.asPySocksType(), proxy.host) 160 | 161 | 162 | class ProxyChecker: 163 | 164 | @staticmethod 165 | def checkAll(proxies: Collection[Proxy], 166 | url: Any = "https://httpbin.org/get", 167 | timeout=5, 168 | threads=1000): 169 | with ThreadPoolExecutor( 170 | max(min(round(len(proxies) * cpu_count()), threads), 171 | 1)) as executor: 172 | future_to_proxy = { 173 | executor.submit(proxy.check, url, timeout): proxy 174 | for proxy in proxies 175 | } 176 | return { 177 | future_to_proxy[future] 178 | for future in as_completed(future_to_proxy) if future.result() 179 | } 180 | 181 | 182 | class ProxyUtiles: 183 | 184 | @staticmethod 185 | def parseAll(proxies: Collection[str], 186 | ptype: ProxyType = ProxyType.HTTP) -> Set[Proxy]: 187 | final = { 188 | *ProxyUtiles.parseAllIPPort(proxies, ptype), 189 | *ProxyUtiles.parseNoraml(proxies) 190 | } 191 | if None in final: final.remove(None) 192 | return final 193 | 194 | @staticmethod 195 | def parseNoraml(proxies: Collection[str]) -> Set[Proxy]: 196 | res = set(map(Proxy.fromString, proxies)) 197 | if None in res: res.remove(None) 198 | return res 199 | 200 | @staticmethod 201 | def parseAllIPPort(proxies: Collection[str], 202 | ptype: ProxyType = ProxyType.HTTP) -> Set[Proxy]: 203 | resu = set() 204 | for pr in proxies: 205 | pr = Patterns.IPPort.search(pr) 206 | if not pr: continue 207 | with suppress(Exception): 208 | resu.add(Proxy(pr.group(1), int(pr.group(2)), ptype)) 209 | return resu 210 | 211 | @staticmethod 212 | def readFromFile(path: Any) -> Set[Proxy]: 213 | if isinstance(path, Path): 214 | with path.open("r+") as read: 215 | lines = read.readlines() 216 | else: 217 | with open(path, "r+") as read: 218 | lines = read.readlines() 219 | 220 | return ProxyUtiles.parseAll([prox.strip() for prox in lines]) 221 | 222 | @staticmethod 223 | def readIPPortFromFile(path: Any) -> Set[Proxy]: 224 | if isinstance(path, Path): 225 | with path.open("r+") as read: 226 | lines = read.readlines() 227 | else: 228 | with open(path, "r+") as read: 229 | lines = read.readlines() 230 | 231 | return ProxyUtiles.parseAllIPPort([prox.strip() for prox in lines]) 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PyRoxy 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | maxminddb>=2.2.0 2 | requests>=2.27.1 3 | yarl>=1.7.2 4 | pysocks>=1.7.1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='PyRoxy', 4 | version="1.0b5", 5 | packages=['PyRoxy', 'PyRoxy.GeoIP', 'PyRoxy.Tools', 'PyRoxy.Exceptions'], 6 | url='https://github.com/MHProDev/PyRoxy', 7 | license='MIT', 8 | author="MH_ProDev", 9 | install_requires=[ 10 | "maxminddb>=2.2.0", "requests>=2.27.1", "yarl>=1.7.2", 11 | "pysocks>=1.7.1" 12 | ], 13 | include_package_data=True, 14 | package_data={ 15 | 'PyRoxy.GeoIP': ['Sqlite/*.txt', "Sqlite/GeoLite2-Country.mmdb"], 16 | '': ["LICENSE.md"] 17 | }) 18 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from PyRoxy import ProxyUtiles, ProxyChecker 2 | 3 | if __name__ == '__main__': 4 | ps = ProxyUtiles.readFromFile("test.txt") 5 | pc = ProxyChecker.checkAll(ps) 6 | print(pc) 7 | --------------------------------------------------------------------------------