├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── client.py ├── install_client.sh ├── server.py ├── socks.py ├── speedtest.py └── ss-server.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM v2ray/official 2 | MAINTAINER HyperApp 3 | 4 | ARG BRANCH=manyuser 5 | ARG WORK=/root 6 | 7 | RUN apk --no-cache add python \ 8 | libsodium \ 9 | wget 10 | 11 | RUN wget -qO- --no-check-certificate https://github.com/shadowsocksr/shadowsocksr/archive/$BRANCH.tar.gz | tar -xzf - -C $WORK 12 | 13 | 14 | 15 | ENV SMART_PORT 8000 16 | ENV USERNMAE admin 17 | ENV PASSWORD smartsocks 18 | EXPOSE $SMART_PORT 19 | 20 | ADD server.py /usr/local/bin/smart-server 21 | ADD ss-server.sh /usr/local/bin/ss-server 22 | 23 | CMD smart-server -p $SMART_PORT -u $USERNAME -P $PASSWORD 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Baye 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartSock 2 | 3 | 4 | DEPRECATION WARNING: NO LONGER BEING MAINTAINED 5 | ---- 6 | 7 | helps you automatically setup and choose the fastest socks server. 8 | 9 | SmartSocks 是一个服务端和客户端自动协商最快传输协议的工具。客户端会自动尝试 SSR/V2Ray 推荐的配置组合,然后使用 `speedtest` 来测速。 10 | 11 | 12 | ## 支持的应用 13 | 14 | - [x] shadowsocksR 15 | - [x] V2Ray (tcp & kcp) 16 | - [ ] kcptun (TODO) 17 | 18 | ### TODO 19 | 20 | - [x] 自动尝试各种加密/协议并测速 21 | - [ ] 根据测速结果自动在本地开放一个 socks 端口 22 | 23 | 24 | ## 服务端 25 | 26 | ### 安装 27 | 28 | 在 `HyperApp → 商店 → 网络` 分组下面找到 `SmartSocks` 安装,配置时只要填入一个主控端口,和用户名密码即可。 29 | 30 | 31 | ## 客户端 32 | 33 | ### 支持的应用 34 | 35 | ### 安装 (Linux & macOS) 36 | 37 | #### 下载依赖 38 | 39 | `curl -sL https://raw.githubusercontent.com/waylybaye/SmartSocks/master/install_client.sh | bash` 40 | 41 | #### 使用方法 42 | 43 | ```bash 44 | cd smartsocks 45 | python client.py -s 服务器地址 -p 主控端口 -u 用户名 -P 密码 -S socks端口 46 | ``` 47 | 48 | * `主控端口` 是指SmartSocks 服务端监听的端口 49 | * `socks端口` 是指 SSR/V2ray 监听的端口 50 | 51 | 运行后客户端会自动尝试各种推荐的加密/协议组合,并测速。 52 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | # encoding: utf8 3 | from __future__ import unicode_literals, print_function 4 | 5 | import copy 6 | import os 7 | import json 8 | import tempfile 9 | import time 10 | import uuid 11 | import atexit 12 | import base64 13 | import signal 14 | import itertools 15 | import subprocess 16 | from collections import OrderedDict 17 | 18 | LOCAL_PORT = 1117 19 | 20 | import socks 21 | import socket 22 | # socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", LOCAL_PORT) 23 | # socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", 19870) 24 | raw_socket = socket.socket 25 | socket.socket = socks.socksocket 26 | 27 | import httplib 28 | import urllib2 29 | import speedtest 30 | 31 | 32 | try: 33 | from argparse import ArgumentParser as ArgParser 34 | except ImportError: 35 | from optparse import OptionParser as ArgParser 36 | 37 | __VERSION__ = '0.0.1' 38 | LATEST_PROCESS = None 39 | reset = '\033[0m' 40 | red = '\033[31m' 41 | black = '\033[30m' 42 | green = '\033[32m' 43 | orange = '\033[33m' 44 | blue = '\033[34m' 45 | purple = '\033[35m' 46 | cyan = '\033[36m' 47 | lightgrey = '\033[37m' 48 | darkgrey = '\033[90m' 49 | 50 | V2Ray_CONFIG = { 51 | "outboundDetour": [ 52 | { 53 | "protocol": "freedom", 54 | "tag": "direct", 55 | "settings": { 56 | } 57 | } 58 | ], 59 | "inbound": { 60 | "listen": "127.0.0.1", 61 | "port": LOCAL_PORT, 62 | "protocol": "socks", 63 | "settings": { 64 | "ip": "127.0.0.1", 65 | "auth": "noauth" 66 | }, 67 | "allowPassive": False 68 | }, 69 | "routing": { 70 | "strategy": "rules", 71 | "settings": { 72 | "domainStrategy": "IPIfNonMatch", 73 | "rules": [ 74 | { 75 | "port": "1-52", 76 | "type": "field", 77 | "outboundTag": "direct" 78 | }, 79 | { 80 | "port": "54-79", 81 | "type": "field", 82 | "outboundTag": "direct" 83 | }, 84 | { 85 | "port": "81-442", 86 | "type": "field", 87 | "outboundTag": "direct" 88 | }, 89 | { 90 | "port": "444-65535", 91 | "type": "field", 92 | "outboundTag": "direct" 93 | }, 94 | { 95 | "type": "field", 96 | "ip": [ 97 | "0.0.0.0/8", 98 | "10.0.0.0/8", 99 | "100.64.0.0/10", 100 | "127.0.0.0/8", 101 | "169.254.0.0/16", 102 | "172.16.0.0/12", 103 | "192.0.0.0/24", 104 | "192.0.2.0/24", 105 | "192.168.0.0/16", 106 | "198.18.0.0/15", 107 | "198.51.100.0/24", 108 | "203.0.113.0/24", 109 | "::1/128", 110 | "fc00::/7", 111 | "fe80::/10" 112 | ], 113 | "outboundTag": "direct" 114 | } 115 | ] 116 | } 117 | }, 118 | "log": { 119 | "loglevel": "warning", 120 | "access": "/dev/stdout", 121 | "error": "/dev/stderr" 122 | }, 123 | "outbound": { 124 | "protocol": "vmess", 125 | "streamSettings": { 126 | "network": "tcp", 127 | "tcpSettings": { 128 | "header": { 129 | "type": "http" 130 | }, 131 | "connectionReuse": True 132 | }, 133 | # "kcpSettings": { 134 | # "header": { 135 | # "type": "none" 136 | # }, 137 | # }, 138 | # "security": "tls", 139 | "security": "none", 140 | # "tlsSettings": { 141 | # "allowInsecure": false 142 | # }, 143 | "wsSettings": { 144 | "path": "", 145 | "connectionReuse": False 146 | } 147 | }, 148 | "settings": { 149 | "vnext": [ 150 | { 151 | "address": "", 152 | "port": 0, 153 | "users": [ 154 | { 155 | "id": "", 156 | "alterId": 64, 157 | "security": "aes-128-cfb" 158 | } 159 | ] 160 | } 161 | ] 162 | } 163 | } 164 | } 165 | 166 | 167 | suggested_plans = OrderedDict([ 168 | ('shadowsocksr', [ 169 | { 170 | 'name': 'ShadowsocksR auth_chain_a + none * OBFS', 171 | 'protocol': 'auth_chain_a', 172 | 'encrypt': 'none', 173 | 'obfs': ['plain', 'http_simple', 'tls1.2_ticket_auth'] 174 | }, 175 | { 176 | 'name': 'ShadowsocksR [chacha20, rc4-md5] + auth_aes128_md5 * OBFS', 177 | 'protocol': 'auth_aes128_md5', 178 | 'encrypt': ['chacha20', 'rc4-md5'], 179 | 'obfs': ['plain', 'http_simple', 'tls1.2_ticket_auth'] 180 | } 181 | ]), 182 | ('v2ray', [ 183 | { 184 | 'name': 'V2Ray VMess tcp * [none, http] obfs', 185 | 'uuid': str(uuid.uuid4()), 186 | 'network': 'tcp', 187 | 'obfs': ['none', 'http'] 188 | }, 189 | { 190 | 'name': 'V2Ray VMess kcp', 191 | 'uuid': str(uuid.uuid4()), 192 | 'network': 'kcp', 193 | 'obfs': 'none', 194 | } 195 | ]) 196 | ]) 197 | 198 | 199 | def start_socks(server, port, socks_port, username, password, socks_server, params): 200 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 201 | global LATEST_PROCESS 202 | query = { 203 | 'port': socks_port, 204 | 'server': socks_server, 205 | 'password': password, 206 | } 207 | query.update(params) 208 | request = urllib2.Request('http://%s:%s/socks' % (server, port), json.dumps(query)) 209 | if username and password: 210 | auth = base64.encodestring('%s:%s' % (username, password)) 211 | request.add_header('Authorization', 'Basic %s' % auth.strip()) 212 | 213 | print(darkgrey + '[SERVER]', query, reset) 214 | request.add_header('Content-Type', 'application/json') 215 | response = urllib2.urlopen(request) 216 | response.read() 217 | 218 | if LATEST_PROCESS: 219 | try: 220 | os.killpg(os.getpgid(LATEST_PROCESS.pid), signal.SIGTERM) 221 | except OSError: 222 | pass 223 | 224 | print(darkgrey + "Starting %s client:" % socks_server) 225 | if socks_server == 'shadowsocksr': 226 | path = os.path.join(BASE_DIR, 'shadowsocksr/shadowsocks/local.py') 227 | command = 'python %s -s %s -p %s -k %s -m %s -O %s -o %s -l %s' % ( 228 | path, server, socks_port, password, params['encrypt'], params['protocol'], params['obfs'], LOCAL_PORT 229 | ) 230 | 231 | elif socks_server == 'v2ray': 232 | config_path = tempfile.mktemp('.json') 233 | config_file = open(config_path, 'w') 234 | config = copy.deepcopy(V2Ray_CONFIG) 235 | config['outbound']['settings']['vnext'][0]['address'] = server 236 | config['outbound']['settings']['vnext'][0]['port'] = int(socks_port) 237 | config['outbound']['settings']['vnext'][0]['users'][0]['id'] = params['uuid'] 238 | config['outbound']['streamSettings']['network'] = params['network'] 239 | config['outbound']['streamSettings']['tcpSettings']['header']['type'] = params['obfs'] 240 | 241 | config_file.write(json.dumps(config, indent=2)) 242 | command = os.path.join(BASE_DIR, 'v2ray') + ' -config ' + config_path 243 | print(darkgrey + "[CLIENT]", command, reset) 244 | else: 245 | raise ValueError('') 246 | 247 | print(darkgrey + command + reset) 248 | process = subprocess.Popen( 249 | command, 250 | stdout=subprocess.PIPE, 251 | # stderr=subprocess.PIPE, 252 | shell=True, 253 | preexec_fn=os.setsid 254 | ) 255 | 256 | atexit.register(process.terminate) 257 | 258 | LATEST_PROCESS = process 259 | 260 | 261 | def main(): 262 | args = parse_args() 263 | if args.version: 264 | print(__VERSION__) 265 | return 266 | 267 | if not args.server or not args.port: 268 | print(red + "server and port is required", reset) 269 | return 270 | 271 | speed = None 272 | speedtest_servers = [] 273 | 274 | for socks_server, plans in suggested_plans.items(): 275 | print("代理软件:", green, socks_server, reset) 276 | for plan in plans: 277 | print("测试配置:", green, plan['name'], reset) 278 | print(darkgrey + '=' * 80, reset) 279 | data = dict(plan.items()) 280 | data.pop('name') 281 | 282 | params = [] 283 | choices = [] 284 | for param, values in data.items(): 285 | params.append(param) 286 | choices.append(values if isinstance(values, (tuple, list)) else [values]) 287 | 288 | for choice in itertools.product(*choices): 289 | payload = dict(zip(params, choice)) 290 | print("子配置:", orange, payload, reset) 291 | 292 | try: 293 | socks.setdefaultproxy() 294 | start_socks( 295 | args.server, 296 | args.port, 297 | args.socks_port, 298 | args.user, 299 | args.password, 300 | socks_server, 301 | payload, 302 | ) 303 | except urllib2.HTTPError as e: 304 | if e.code == 401: 305 | print(red + 'ERROR: Invalid username or password', reset) 306 | return 307 | 308 | print(red + 'ERROR: Failed to start socks on server', reset) 309 | print(darkgrey + '-' * 80, reset) 310 | # print(e.code, e.reason) 311 | # print(dir(e)) 312 | return 313 | 314 | for x in range(20): 315 | sock = raw_socket() 316 | try: 317 | sock.connect(('localhost', LOCAL_PORT)) 318 | sock.close() 319 | print(green + 'Local port started ...', reset) 320 | socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, "127.0.0.1", LOCAL_PORT) 321 | time.sleep(0.2) 322 | break 323 | except socket.error: 324 | time.sleep(0.1) 325 | continue 326 | else: 327 | print(red + 'Client no response, skip ...', reset) 328 | continue 329 | 330 | try: 331 | if not speed: 332 | speed = speedtest.Speedtest() 333 | print(darkgrey + "[SPEEDTEST] Fetch servers ...", reset) 334 | speed.get_servers(speedtest_servers) 335 | print(darkgrey + "[SPEEDTEST] Choose closest server ...", reset) 336 | speed.get_best_server() 337 | print(darkgrey + '[SPEEDTEST] Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: %(latency)s ms' % speed.results.server, reset) 338 | 339 | print(darkgrey + "[SPEEDTEST] Downloading ...", reset) 340 | speed.download() 341 | results = speed.results 342 | print(green + '[SPEEDTEST] Ping: %s ms\tDownload: %0.2f Mbps\tUpload: %0.2f Mbps' % 343 | (results.ping, 344 | (results.download / 1000.0 / 1000.0), 345 | (results.upload / 1000.0 / 1000.0), 346 | ), reset) 347 | 348 | except (speedtest.SpeedtestException, httplib.HTTPException): 349 | print(darkgrey + "[SPEEDTEST]", red + 'ERROR: Failed to connect', reset) 350 | 351 | print(darkgrey + '-' * 80, reset) 352 | 353 | return 354 | 355 | 356 | def parse_args(): 357 | description = "SmartSocks" 358 | parser = ArgParser(description=description) 359 | try: 360 | parser.add_argument = parser.add_option 361 | except AttributeError: 362 | pass 363 | 364 | parser.add_argument('-s', '--server', help='SmartSocks server') 365 | parser.add_argument('-p', '--port', help='SmartSocks port') 366 | parser.add_argument('-u', '--user', help='SmartSocks user', 367 | default='admin') 368 | parser.add_argument('-P', '--password', help='SmartSocks password', 369 | default='smartsocks') 370 | parser.add_argument('-S', '--socks-port', help='SmartSocks port', default=8848) 371 | parser.add_argument('-V', '--version', action='store_true', 372 | help='Show the version number and exit') 373 | 374 | options = parser.parse_args() 375 | if isinstance(options, tuple): 376 | args = options[0] 377 | else: 378 | args = options 379 | return args 380 | 381 | 382 | if __name__ == '__main__': 383 | main() 384 | -------------------------------------------------------------------------------- /install_client.sh: -------------------------------------------------------------------------------- 1 | V2RAY_VER="2.33.1" 2 | DIRECTORY="smartsocks" 3 | 4 | case "$(uname -s)" in 5 | Darwin) 6 | V2RAY_PLATFORM="macos" 7 | ;; 8 | 9 | Linux) 10 | V2RAY_PLATFORM="linux-64" 11 | ;; 12 | esac 13 | 14 | if ! [ -d $DIRECTORY ]; then 15 | echo "[SmartSocks] Make directory ./$DIRECTORY" 16 | mkdir $DIRECTORY 17 | fi 18 | 19 | cd $DIRECTORY 20 | 21 | if ! [ -d shadowsocksr ]; then 22 | echo "[SmartSocks] Downloading ShadowsocksR ..." 23 | git clone https://github.com/shadowsocksr/shadowsocksr.git || exit 1 24 | fi 25 | 26 | if ! [ -f v2ray ]; then 27 | echo "[SmartSocks] Downloading V2Ray ${V2RAY_VER} ..." 28 | curl -o v2ray.zip -L https://github.com/v2ray/v2ray-core/releases/download/v${V2RAY_VER}/v2ray-${V2RAY_PLATFORM}.zip || exit 1 29 | unzip v2ray.zip -d tmp 30 | mv tmp/v2ray-v${V2RAY_VER}-${V2RAY_PLATFORM}/v2ray v2ray 31 | chmod +x v2ray 32 | rm -rf tmp 33 | rm v2ray.zip 34 | fi 35 | 36 | echo "[SmartSocks] Downloading SmartSocks client ..." 37 | curl -s https://raw.githubusercontent.com/waylybaye/SmartSocks/master/client.py > client.py 38 | curl -s https://raw.githubusercontent.com/waylybaye/SmartSocks/master/socks.py > socks.py 39 | curl -s https://raw.githubusercontent.com/waylybaye/SmartSocks/master/speedtest.py > speedtest.py 40 | 41 | echo "========================" 42 | echo "Usage: python client.py" 43 | python client.py -h 44 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import unicode_literals, print_function 3 | import os 4 | import copy 5 | import json 6 | import atexit 7 | import signal 8 | import base64 9 | import subprocess 10 | import tempfile 11 | 12 | try: 13 | from argparse import ArgumentParser as ArgParser 14 | except ImportError: 15 | from optparse import OptionParser as ArgParser 16 | 17 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 18 | 19 | __VERSION__ = '0.0.1' 20 | reset = '\033[0m' 21 | red = '\033[31m' 22 | black = '\033[30m' 23 | green = '\033[32m' 24 | orange = '\033[33m' 25 | blue = '\033[34m' 26 | purple = '\033[35m' 27 | cyan = '\033[36m' 28 | lightgrey = '\033[37m' 29 | darkgrey = '\033[90m' 30 | 31 | V2Ray_CONFIG = { 32 | "log": { 33 | "access": "/dev/stdout", 34 | "error": "/dev/stderr", 35 | "loglevel": "warning" 36 | }, 37 | "inbound": { 38 | "port": '', 39 | "protocol": "vmess", 40 | "settings": { 41 | "clients": [ 42 | { 43 | "id": '', 44 | "level": 1, 45 | "alterId": 64, 46 | } 47 | ], 48 | "default": { 49 | "level": 1, 50 | "alterId": 32 51 | }, 52 | }, 53 | "streamSettings": { 54 | "network": '', 55 | "security": "none", 56 | } 57 | }, 58 | "outbound": { 59 | "protocol": "freedom", 60 | "settings": {} 61 | } 62 | } 63 | 64 | V2Ray_TCP = { 65 | "connectionReuse": True, 66 | "header": { 67 | "type": "http", 68 | "request": { 69 | "version": "1.1", 70 | "method": "GET", 71 | "path": [ 72 | "/" 73 | ], 74 | "headers": { 75 | "Host": [ 76 | "www.bing.com" 77 | ], 78 | "User-Agent": [ 79 | "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36", 80 | "Mozilla/5.0 (iPhone; CPU iPhone OS 10_0_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/53.0.2785.109 Mobile/14A456 Safari/601.1.46" 81 | ], 82 | "Accept-Encoding": [ 83 | "gzip, deflate" 84 | ], 85 | "Connection": [ 86 | "keep-alive" 87 | ], 88 | "Pragma": "no-cache" 89 | } 90 | }, 91 | "response": { 92 | "version": "1.1", 93 | "status": "200", 94 | "reason": "OK", 95 | "headers": { 96 | "Content-Type": [ 97 | "application/octet-stream", 98 | "video/mpeg" 99 | ], 100 | "Transfer-Encoding": [ 101 | "chunked" 102 | ], 103 | "Connection": [ 104 | "keep-alive" 105 | ], 106 | "Pragma": "no-cache" 107 | } 108 | } 109 | } 110 | } 111 | 112 | 113 | def socks_command(payload): 114 | server = payload['server'] 115 | port = payload['port'] 116 | 117 | if server == 'shadowsocksr': 118 | path = "python /root/shadowsocksr-manyuser/shadowsocks/server.py " 119 | options = "-k '%(password)s' -m '%(encrypt)s' -p %(port)s -o %(obfs)s -O %(protocol)s" % payload 120 | command = path + options 121 | 122 | elif server == 'v2ray': 123 | config_path = tempfile.mktemp('.json') 124 | config_file = open(config_path, 'w') 125 | config = copy.deepcopy(V2Ray_CONFIG) 126 | config['inbound']['port'] = int(payload['port']) 127 | config['inbound']['settings']['clients'][0]['id'] = payload['uuid'] 128 | config['inbound']['streamSettings']['network'] = payload['network'] 129 | 130 | if payload['obfs'] == 'http': 131 | config['inbound']['streamSettings']['tcpSettings'] = copy.deepcopy(V2Ray_TCP) 132 | 133 | config_file.write(json.dumps(config, indent=2)) 134 | config_file.close() 135 | command = "v2ray -config " + config_path 136 | 137 | else: 138 | raise ValueError("Unsupported socks server") 139 | 140 | print(orange + "COMMAND:", command, reset) 141 | return command 142 | 143 | 144 | class Server(BaseHTTPRequestHandler): 145 | username = "" 146 | password = "" 147 | latest_process = None 148 | 149 | def _set_headers(self): 150 | self.send_response(200) 151 | self.send_header('Content-type', 'text/html') 152 | self.end_headers() 153 | 154 | def do_GET(self): 155 | self.send_response(405) 156 | 157 | def do_HEAD(self): 158 | self._set_headers() 159 | 160 | def response(self, status, content): 161 | self.send_response(status) 162 | self.end_headers() 163 | self.wfile.write(content) 164 | 165 | def do_POST(self): 166 | if self.path != '/socks': 167 | self.send_response(404) 168 | return 169 | 170 | authentication = self.headers.get('authorization') 171 | if not authentication: 172 | self.response(401, "Authentication required.") 173 | return 174 | 175 | if not authentication.startswith('Basic '): 176 | self.response(401, "Unsupported Authentication.") 177 | return 178 | 179 | try: 180 | auth = base64.decodestring(authentication.split(' ')[1]) 181 | tuples = auth.split(':') 182 | user = tuples[0] 183 | password = tuples[1] if len(tuples) > 1 else '' 184 | 185 | if user != self.username or password != self.password: 186 | self.response(401, "Authentication Failed.") 187 | return 188 | 189 | except ValueError: 190 | self.response(401, "Invalid Authentication.") 191 | return 192 | 193 | content_length = int(self.headers.get('content-length', '0')) 194 | payload = self.rfile.read(content_length) 195 | if not payload: 196 | print(red + "No Payload", reset) 197 | self.response(406, "Payload required.") 198 | return 199 | 200 | try: 201 | payload = json.loads(payload) 202 | except ValueError: 203 | print(red + "Payload invalid json", reset) 204 | self.response(406, "Payload invalid.") 205 | return 206 | 207 | if Server.latest_process: 208 | print(orange + "[SmartSocks] closing previous socks", reset) 209 | try: 210 | os.killpg(os.getpgid(Server.latest_process.pid), signal.SIGTERM) 211 | except OSError: 212 | pass 213 | 214 | Server.latest_process = None 215 | 216 | try: 217 | command = socks_command(payload) 218 | except ValueError: 219 | self.response(406, "Unsupported socks") 220 | return 221 | 222 | print(green + "[SmartSocks] starting socks server", reset) 223 | popen = subprocess.Popen( 224 | command, 225 | stdout=subprocess.PIPE, 226 | shell=True, preexec_fn=os.setsid) 227 | 228 | atexit.register(popen.terminate) 229 | 230 | Server.latest_process = popen 231 | self.response(200, "Started") 232 | 233 | # self._set_headers() 234 | # self.wfile.write("

POST!

") 235 | 236 | 237 | def run(server_class=HTTPServer, handler_class=Server, port=80, username="", password=""): 238 | server_address = ('', int(port)) 239 | handler_class.username = username 240 | handler_class.password = password 241 | httpd = server_class(server_address, handler_class) 242 | # print 'Starting SmartSocks ...' 243 | print(green + '[SmarkSocks] Waiting for clients ...', reset) 244 | httpd.serve_forever() 245 | 246 | 247 | def parse_args(): 248 | description = "SmartSocks Server" 249 | parser = ArgParser(description=description) 250 | try: 251 | parser.add_argument = parser.add_option 252 | except AttributeError: 253 | pass 254 | 255 | parser.add_argument('-p', '--port', help='SmartSocks server port') 256 | parser.add_argument('-u', '--user', help='SmartSocks user', 257 | default="admin") 258 | parser.add_argument('-P', '--password', help='SmartSocks password', 259 | default="smartsocks") 260 | parser.add_argument('-V', '--version', action='store_true', 261 | help='Show the version number and exit') 262 | 263 | options = parser.parse_args() 264 | if isinstance(options, tuple): 265 | args = options[0] 266 | else: 267 | args = options 268 | return args 269 | 270 | 271 | def main(): 272 | args = parse_args() 273 | run(port=args.port, username=args.user, password=args.password) 274 | 275 | 276 | if __name__ == "__main__": 277 | main() 278 | -------------------------------------------------------------------------------- /socks.py: -------------------------------------------------------------------------------- 1 | """SocksiPy - Python SOCKS module. 2 | Version 1.00 3 | 4 | Copyright 2006 Dan-Haim. All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 3. Neither the name of Dan Haim nor the names of his contributors may be used 14 | to endorse or promote products derived from this software without specific 15 | prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED 18 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 20 | EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA 23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 25 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE. 26 | 27 | 28 | This module provides a standard socket-like interface for Python 29 | for tunneling connections through SOCKS proxies. 30 | 31 | """ 32 | 33 | import socket 34 | import struct 35 | 36 | PROXY_TYPE_SOCKS4 = 1 37 | PROXY_TYPE_SOCKS5 = 2 38 | PROXY_TYPE_HTTP = 3 39 | 40 | _defaultproxy = None 41 | _orgsocket = socket.socket 42 | 43 | class ProxyError(Exception): 44 | def __init__(self, value): 45 | self.value = value 46 | def __str__(self): 47 | return repr(self.value) 48 | 49 | class GeneralProxyError(ProxyError): 50 | def __init__(self, value): 51 | self.value = value 52 | def __str__(self): 53 | return repr(self.value) 54 | 55 | class Socks5AuthError(ProxyError): 56 | def __init__(self, value): 57 | self.value = value 58 | def __str__(self): 59 | return repr(self.value) 60 | 61 | class Socks5Error(ProxyError): 62 | def __init__(self, value): 63 | self.value = value 64 | def __str__(self): 65 | return repr(self.value) 66 | 67 | class Socks4Error(ProxyError): 68 | def __init__(self, value): 69 | self.value = value 70 | def __str__(self): 71 | return repr(self.value) 72 | 73 | class HTTPError(ProxyError): 74 | def __init__(self, value): 75 | self.value = value 76 | def __str__(self): 77 | return repr(self.value) 78 | 79 | _generalerrors = ("success", 80 | "invalid data", 81 | "not connected", 82 | "not available", 83 | "bad proxy type", 84 | "bad input") 85 | 86 | _socks5errors = ("succeeded", 87 | "general SOCKS server failure", 88 | "connection not allowed by ruleset", 89 | "Network unreachable", 90 | "Host unreachable", 91 | "Connection refused", 92 | "TTL expired", 93 | "Command not supported", 94 | "Address type not supported", 95 | "Unknown error") 96 | 97 | _socks5autherrors = ("succeeded", 98 | "authentication is required", 99 | "all offered authentication methods were rejected", 100 | "unknown username or invalid password", 101 | "unknown error") 102 | 103 | _socks4errors = ("request granted", 104 | "request rejected or failed", 105 | "request rejected because SOCKS server cannot connect to identd on the client", 106 | "request rejected because the client program and identd report different user-ids", 107 | "unknown error") 108 | 109 | def setdefaultproxy(proxytype=None,addr=None,port=None,rdns=True,username=None,password=None): 110 | """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) 111 | Sets a default proxy which all further socksocket objects will use, 112 | unless explicitly changed. 113 | """ 114 | global _defaultproxy 115 | _defaultproxy = (proxytype,addr,port,rdns,username,password) 116 | 117 | class socksocket(socket.socket): 118 | """socksocket([family[, type[, proto]]]) -> socket object 119 | 120 | Open a SOCKS enabled socket. The parameters are the same as 121 | those of the standard socket init. In order for SOCKS to work, 122 | you must specify family=AF_INET, type=SOCK_STREAM and proto=0. 123 | """ 124 | 125 | def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None): 126 | _orgsocket.__init__(self,family,type,proto,_sock) 127 | if _defaultproxy != None: 128 | self.__proxy = _defaultproxy 129 | else: 130 | self.__proxy = (None, None, None, None, None, None) 131 | self.__proxysockname = None 132 | self.__proxypeername = None 133 | 134 | def __recvall(self, bytes): 135 | """__recvall(bytes) -> data 136 | Receive EXACTLY the number of bytes requested from the socket. 137 | Blocks until the required number of bytes have been received. 138 | """ 139 | data = "" 140 | while len(data) < bytes: 141 | data = data + self.recv(bytes-len(data)) 142 | return data 143 | 144 | def setproxy(self,proxytype=None,addr=None,port=None,rdns=True,username=None,password=None): 145 | """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]]) 146 | Sets the proxy to be used. 147 | proxytype - The type of the proxy to be used. Three types 148 | are supported: PROXY_TYPE_SOCKS4 (including socks4a), 149 | PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP 150 | addr - The address of the server (IP or DNS). 151 | port - The port of the server. Defaults to 1080 for SOCKS 152 | servers and 8080 for HTTP proxy servers. 153 | rdns - Should DNS queries be preformed on the remote side 154 | (rather than the local side). The default is True. 155 | Note: This has no effect with SOCKS4 servers. 156 | username - Username to authenticate with to the server. 157 | The default is no authentication. 158 | password - Password to authenticate with to the server. 159 | Only relevant when username is also provided. 160 | """ 161 | self.__proxy = (proxytype,addr,port,rdns,username,password) 162 | 163 | def __negotiatesocks5(self,destaddr,destport): 164 | """__negotiatesocks5(self,destaddr,destport) 165 | Negotiates a connection through a SOCKS5 server. 166 | """ 167 | # First we'll send the authentication packages we support. 168 | if (self.__proxy[4]!=None) and (self.__proxy[5]!=None): 169 | # The username/password details were supplied to the 170 | # setproxy method so we support the USERNAME/PASSWORD 171 | # authentication (in addition to the standard none). 172 | self.sendall("\x05\x02\x00\x02") 173 | else: 174 | # No username/password were entered, therefore we 175 | # only support connections with no authentication. 176 | self.sendall("\x05\x01\x00") 177 | # We'll receive the server's response to determine which 178 | # method was selected 179 | chosenauth = self.__recvall(2) 180 | if chosenauth[0] != "\x05": 181 | self.close() 182 | raise GeneralProxyError((1,_generalerrors[1])) 183 | # Check the chosen authentication method 184 | if chosenauth[1] == "\x00": 185 | # No authentication is required 186 | pass 187 | elif chosenauth[1] == "\x02": 188 | # Okay, we need to perform a basic username/password 189 | # authentication. 190 | self.sendall("\x01" + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.proxy[5])) + self.__proxy[5]) 191 | authstat = self.__recvall(2) 192 | if authstat[0] != "\x01": 193 | # Bad response 194 | self.close() 195 | raise GeneralProxyError((1,_generalerrors[1])) 196 | if authstat[1] != "\x00": 197 | # Authentication failed 198 | self.close() 199 | raise Socks5AuthError,((3,_socks5autherrors[3])) 200 | # Authentication succeeded 201 | else: 202 | # Reaching here is always bad 203 | self.close() 204 | if chosenauth[1] == "\xFF": 205 | raise Socks5AuthError((2,_socks5autherrors[2])) 206 | else: 207 | raise GeneralProxyError((1,_generalerrors[1])) 208 | # Now we can request the actual connection 209 | req = "\x05\x01\x00" 210 | # If the given destination address is an IP address, we'll 211 | # use the IPv4 address request even if remote resolving was specified. 212 | try: 213 | ipaddr = socket.inet_aton(destaddr) 214 | req = req + "\x01" + ipaddr 215 | except socket.error: 216 | # Well it's not an IP number, so it's probably a DNS name. 217 | if self.__proxy[3]==True: 218 | # Resolve remotely 219 | ipaddr = None 220 | req = req + "\x03" + chr(len(destaddr)) + destaddr 221 | else: 222 | # Resolve locally 223 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) 224 | req = req + "\x01" + ipaddr 225 | req = req + struct.pack(">H",destport) 226 | self.sendall(req) 227 | # Get the response 228 | resp = self.__recvall(4) 229 | if resp[0] != "\x05": 230 | self.close() 231 | raise GeneralProxyError((1,_generalerrors[1])) 232 | elif resp[1] != "\x00": 233 | # Connection failed 234 | self.close() 235 | if ord(resp[1])<=8: 236 | raise Socks5Error(ord(resp[1]),_generalerrors[ord(resp[1])]) 237 | else: 238 | raise Socks5Error(9,_generalerrors[9]) 239 | # Get the bound address/port 240 | elif resp[3] == "\x01": 241 | boundaddr = self.__recvall(4) 242 | elif resp[3] == "\x03": 243 | resp = resp + self.recv(1) 244 | boundaddr = self.__recvall(resp[4]) 245 | else: 246 | self.close() 247 | raise GeneralProxyError((1,_generalerrors[1])) 248 | boundport = struct.unpack(">H",self.__recvall(2))[0] 249 | self.__proxysockname = (boundaddr,boundport) 250 | if ipaddr != None: 251 | self.__proxypeername = (socket.inet_ntoa(ipaddr),destport) 252 | else: 253 | self.__proxypeername = (destaddr,destport) 254 | 255 | def getproxysockname(self): 256 | """getsockname() -> address info 257 | Returns the bound IP address and port number at the proxy. 258 | """ 259 | return self.__proxysockname 260 | 261 | def getproxypeername(self): 262 | """getproxypeername() -> address info 263 | Returns the IP and port number of the proxy. 264 | """ 265 | return _orgsocket.getpeername(self) 266 | 267 | def getpeername(self): 268 | """getpeername() -> address info 269 | Returns the IP address and port number of the destination 270 | machine (note: getproxypeername returns the proxy) 271 | """ 272 | return self.__proxypeername 273 | 274 | def __negotiatesocks4(self,destaddr,destport): 275 | """__negotiatesocks4(self,destaddr,destport) 276 | Negotiates a connection through a SOCKS4 server. 277 | """ 278 | # Check if the destination address provided is an IP address 279 | rmtrslv = False 280 | try: 281 | ipaddr = socket.inet_aton(destaddr) 282 | except socket.error: 283 | # It's a DNS name. Check where it should be resolved. 284 | if self.__proxy[3]==True: 285 | ipaddr = "\x00\x00\x00\x01" 286 | rmtrslv = True 287 | else: 288 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) 289 | # Construct the request packet 290 | req = "\x04\x01" + struct.pack(">H",destport) + ipaddr 291 | # The username parameter is considered userid for SOCKS4 292 | if self.__proxy[4] != None: 293 | req = req + self.__proxy[4] 294 | req = req + "\x00" 295 | # DNS name if remote resolving is required 296 | # NOTE: This is actually an extension to the SOCKS4 protocol 297 | # called SOCKS4A and may not be supported in all cases. 298 | if rmtrslv==True: 299 | req = req + destaddr + "\x00" 300 | self.sendall(req) 301 | # Get the response from the server 302 | resp = self.__recvall(8) 303 | if resp[0] != "\x00": 304 | # Bad data 305 | self.close() 306 | raise GeneralProxyError((1,_generalerrors[1])) 307 | if resp[1] != "\x5A": 308 | # Server returned an error 309 | self.close() 310 | if ord(resp[1]) in (91,92,93): 311 | self.close() 312 | raise Socks4Error((ord(resp[1]),_socks4errors[ord(resp[1])-90])) 313 | else: 314 | raise Socks4Error((94,_socks4errors[4])) 315 | # Get the bound address/port 316 | self.__proxysockname = (socket.inet_ntoa(resp[4:]),struct.unpack(">H",resp[2:4])[0]) 317 | if rmtrslv != None: 318 | self.__proxypeername = (socket.inet_ntoa(ipaddr),destport) 319 | else: 320 | self.__proxypeername = (destaddr,destport) 321 | 322 | def __negotiatehttp(self,destaddr,destport): 323 | """__negotiatehttp(self,destaddr,destport) 324 | Negotiates a connection through an HTTP server. 325 | """ 326 | # If we need to resolve locally, we do this now 327 | if self.__proxy[3] == False: 328 | addr = socket.gethostbyname(destaddr) 329 | else: 330 | addr = destaddr 331 | self.sendall("CONNECT " + addr + ":" + str(destport) + " HTTP/1.1\r\n" + "Host: " + destaddr + "\r\n\r\n") 332 | # We read the response until we get the string "\r\n\r\n" 333 | resp = self.recv(1) 334 | while resp.find("\r\n\r\n")==-1: 335 | resp = resp + self.recv(1) 336 | # We just need the first line to check if the connection 337 | # was successful 338 | statusline = resp.splitlines()[0].split(" ",2) 339 | if statusline[0] not in ("HTTP/1.0","HTTP/1.1"): 340 | self.close() 341 | raise GeneralProxyError((1,_generalerrors[1])) 342 | try: 343 | statuscode = int(statusline[1]) 344 | except ValueError: 345 | self.close() 346 | raise GeneralProxyError((1,_generalerrors[1])) 347 | if statuscode != 200: 348 | self.close() 349 | raise HTTPError((statuscode,statusline[2])) 350 | self.__proxysockname = ("0.0.0.0",0) 351 | self.__proxypeername = (addr,destport) 352 | 353 | def connect(self,destpair): 354 | """connect(self,despair) 355 | Connects to the specified destination through a proxy. 356 | destpar - A tuple of the IP/DNS address and the port number. 357 | (identical to socket's connect). 358 | To select the proxy server use setproxy(). 359 | """ 360 | # Do a minimal input check first 361 | if (type(destpair) in (list,tuple)==False) or (len(destpair)<2) or (type(destpair[0])!=str) or (type(destpair[1])!=int): 362 | raise GeneralProxyError((5,_generalerrors[5])) 363 | if self.__proxy[0] == PROXY_TYPE_SOCKS5: 364 | if self.__proxy[2] != None: 365 | portnum = self.__proxy[2] 366 | else: 367 | portnum = 1080 368 | _orgsocket.connect(self,(self.__proxy[1],portnum)) 369 | self.__negotiatesocks5(destpair[0],destpair[1]) 370 | elif self.__proxy[0] == PROXY_TYPE_SOCKS4: 371 | if self.__proxy[2] != None: 372 | portnum = self.__proxy[2] 373 | else: 374 | portnum = 1080 375 | _orgsocket.connect(self,(self.__proxy[1],portnum)) 376 | self.__negotiatesocks4(destpair[0],destpair[1]) 377 | elif self.__proxy[0] == PROXY_TYPE_HTTP: 378 | if self.__proxy[2] != None: 379 | portnum = self.__proxy[2] 380 | else: 381 | portnum = 8080 382 | _orgsocket.connect(self,(self.__proxy[1],portnum)) 383 | self.__negotiatehttp(destpair[0],destpair[1]) 384 | elif self.__proxy[0] == None: 385 | _orgsocket.connect(self,(destpair[0],destpair[1])) 386 | else: 387 | raise GeneralProxyError((4,_generalerrors[4])) 388 | -------------------------------------------------------------------------------- /speedtest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2012-2016 Matt Martz 4 | # All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | # not use this file except in compliance with the License. You may obtain 8 | # a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | # License for the specific language governing permissions and limitations 16 | # under the License. 17 | 18 | import os 19 | import re 20 | import csv 21 | import sys 22 | import math 23 | import errno 24 | import signal 25 | import socket 26 | import timeit 27 | import datetime 28 | import platform 29 | import threading 30 | import xml.parsers.expat 31 | 32 | try: 33 | import gzip 34 | GZIP_BASE = gzip.GzipFile 35 | except ImportError: 36 | gzip = None 37 | GZIP_BASE = object 38 | 39 | __version__ = '1.0.6' 40 | 41 | 42 | class FakeShutdownEvent(object): 43 | """Class to fake a threading.Event.isSet so that users of this module 44 | are not required to register their own threading.Event() 45 | """ 46 | 47 | @staticmethod 48 | def isSet(): 49 | "Dummy method to always return false""" 50 | return False 51 | 52 | 53 | # Some global variables we use 54 | USER_AGENT = None 55 | SOURCE = None 56 | SHUTDOWN_EVENT = FakeShutdownEvent() 57 | SCHEME = 'http' 58 | DEBUG = False 59 | 60 | # Used for bound_interface 61 | SOCKET_SOCKET = socket.socket 62 | 63 | # Begin import game to handle Python 2 and Python 3 64 | try: 65 | import json 66 | except ImportError: 67 | try: 68 | import simplejson as json 69 | except ImportError: 70 | json = None 71 | 72 | try: 73 | import xml.etree.cElementTree as ET 74 | except ImportError: 75 | try: 76 | import xml.etree.ElementTree as ET 77 | except ImportError: 78 | from xml.dom import minidom as DOM 79 | ET = None 80 | 81 | try: 82 | from urllib2 import urlopen, Request, HTTPError, URLError 83 | except ImportError: 84 | from urllib.request import urlopen, Request, HTTPError, URLError 85 | 86 | try: 87 | from httplib import HTTPConnection 88 | except ImportError: 89 | from http.client import HTTPConnection 90 | 91 | try: 92 | from httplib import HTTPSConnection 93 | except ImportError: 94 | try: 95 | from http.client import HTTPSConnection 96 | except ImportError: 97 | HTTPSConnection = None 98 | 99 | try: 100 | from Queue import Queue 101 | except ImportError: 102 | from queue import Queue 103 | 104 | try: 105 | from urlparse import urlparse 106 | except ImportError: 107 | from urllib.parse import urlparse 108 | 109 | try: 110 | from urlparse import parse_qs 111 | except ImportError: 112 | try: 113 | from urllib.parse import parse_qs 114 | except ImportError: 115 | from cgi import parse_qs 116 | 117 | try: 118 | from hashlib import md5 119 | except ImportError: 120 | from md5 import md5 121 | 122 | try: 123 | from argparse import ArgumentParser as ArgParser 124 | from argparse import SUPPRESS as ARG_SUPPRESS 125 | PARSER_TYPE_INT = int 126 | PARSER_TYPE_STR = str 127 | except ImportError: 128 | from optparse import OptionParser as ArgParser 129 | from optparse import SUPPRESS_HELP as ARG_SUPPRESS 130 | PARSER_TYPE_INT = 'int' 131 | PARSER_TYPE_STR = 'string' 132 | 133 | try: 134 | from cStringIO import StringIO 135 | BytesIO = None 136 | except ImportError: 137 | try: 138 | from StringIO import StringIO 139 | BytesIO = None 140 | except ImportError: 141 | from io import StringIO, BytesIO 142 | 143 | try: 144 | import __builtin__ 145 | except ImportError: 146 | import builtins 147 | from io import TextIOWrapper, FileIO 148 | 149 | class _Py3Utf8Stdout(TextIOWrapper): 150 | """UTF-8 encoded wrapper around stdout for py3, to override 151 | ASCII stdout 152 | """ 153 | def __init__(self, **kwargs): 154 | buf = FileIO(sys.stdout.fileno(), 'w') 155 | super(_Py3Utf8Stdout, self).__init__( 156 | buf, 157 | encoding='utf8', 158 | errors='strict' 159 | ) 160 | 161 | def write(self, s): 162 | super(_Py3Utf8Stdout, self).write(s) 163 | self.flush() 164 | 165 | _py3_print = getattr(builtins, 'print') 166 | _py3_utf8_stdout = _Py3Utf8Stdout() 167 | 168 | def to_utf8(v): 169 | """No-op encode to utf-8 for py3""" 170 | return v 171 | 172 | def print_(*args, **kwargs): 173 | """Wrapper function for py3 to print, with a utf-8 encoded stdout""" 174 | kwargs['file'] = _py3_utf8_stdout 175 | _py3_print(*args, **kwargs) 176 | else: 177 | del __builtin__ 178 | 179 | def to_utf8(v): 180 | """Encode value to utf-8 if possible for py2""" 181 | try: 182 | return v.encode('utf8', 'strict') 183 | except AttributeError: 184 | return v 185 | 186 | def print_(*args, **kwargs): 187 | """The new-style print function for Python 2.4 and 2.5. 188 | 189 | Taken from https://pypi.python.org/pypi/six/ 190 | 191 | Modified to set encoding to UTF-8 always 192 | """ 193 | fp = kwargs.pop("file", sys.stdout) 194 | if fp is None: 195 | return 196 | 197 | def write(data): 198 | if not isinstance(data, basestring): 199 | data = str(data) 200 | # If the file has an encoding, encode unicode with it. 201 | encoding = 'utf8' # Always trust UTF-8 for output 202 | if (isinstance(fp, file) and 203 | isinstance(data, unicode) and 204 | encoding is not None): 205 | errors = getattr(fp, "errors", None) 206 | if errors is None: 207 | errors = "strict" 208 | data = data.encode(encoding, errors) 209 | fp.write(data) 210 | want_unicode = False 211 | sep = kwargs.pop("sep", None) 212 | if sep is not None: 213 | if isinstance(sep, unicode): 214 | want_unicode = True 215 | elif not isinstance(sep, str): 216 | raise TypeError("sep must be None or a string") 217 | end = kwargs.pop("end", None) 218 | if end is not None: 219 | if isinstance(end, unicode): 220 | want_unicode = True 221 | elif not isinstance(end, str): 222 | raise TypeError("end must be None or a string") 223 | if kwargs: 224 | raise TypeError("invalid keyword arguments to print()") 225 | if not want_unicode: 226 | for arg in args: 227 | if isinstance(arg, unicode): 228 | want_unicode = True 229 | break 230 | if want_unicode: 231 | newline = unicode("\n") 232 | space = unicode(" ") 233 | else: 234 | newline = "\n" 235 | space = " " 236 | if sep is None: 237 | sep = space 238 | if end is None: 239 | end = newline 240 | for i, arg in enumerate(args): 241 | if i: 242 | write(sep) 243 | write(arg) 244 | write(end) 245 | 246 | 247 | # Exception "constants" to support Python 2 through Python 3 248 | try: 249 | import ssl 250 | try: 251 | CERT_ERROR = (ssl.CertificateError,) 252 | except AttributeError: 253 | CERT_ERROR = tuple() 254 | 255 | HTTP_ERRORS = ((HTTPError, URLError, socket.error, ssl.SSLError) + 256 | CERT_ERROR) 257 | except ImportError: 258 | HTTP_ERRORS = (HTTPError, URLError, socket.error) 259 | 260 | 261 | class SpeedtestException(Exception): 262 | """Base exception for this module""" 263 | 264 | 265 | class SpeedtestCLIError(SpeedtestException): 266 | """Generic exception for raising errors during CLI operation""" 267 | 268 | 269 | class SpeedtestHTTPError(SpeedtestException): 270 | """Base HTTP exception for this module""" 271 | 272 | 273 | class SpeedtestConfigError(SpeedtestException): 274 | """Configuration provided is invalid""" 275 | 276 | 277 | class ConfigRetrievalError(SpeedtestHTTPError): 278 | """Could not retrieve config.php""" 279 | 280 | 281 | class ServersRetrievalError(SpeedtestHTTPError): 282 | """Could not retrieve speedtest-servers.php""" 283 | 284 | 285 | class InvalidServerIDType(SpeedtestException): 286 | """Server ID used for filtering was not an integer""" 287 | 288 | 289 | class NoMatchedServers(SpeedtestException): 290 | """No servers matched when filtering""" 291 | 292 | 293 | class SpeedtestMiniConnectFailure(SpeedtestException): 294 | """Could not connect to the provided speedtest mini server""" 295 | 296 | 297 | class InvalidSpeedtestMiniServer(SpeedtestException): 298 | """Server provided as a speedtest mini server does not actually appear 299 | to be a speedtest mini server 300 | """ 301 | 302 | 303 | class ShareResultsConnectFailure(SpeedtestException): 304 | """Could not connect to speedtest.net API to POST results""" 305 | 306 | 307 | class ShareResultsSubmitFailure(SpeedtestException): 308 | """Unable to successfully POST results to speedtest.net API after 309 | connection 310 | """ 311 | 312 | 313 | class SpeedtestUploadTimeout(SpeedtestException): 314 | """testlength configuration reached during upload 315 | Used to ensure the upload halts when no additional data should be sent 316 | """ 317 | 318 | 319 | class SpeedtestBestServerFailure(SpeedtestException): 320 | """Unable to determine best server""" 321 | 322 | 323 | class GzipDecodedResponse(GZIP_BASE): 324 | """A file-like object to decode a response encoded with the gzip 325 | method, as described in RFC 1952. 326 | 327 | Largely copied from ``xmlrpclib``/``xmlrpc.client`` and modified 328 | to work for py2.4-py3 329 | """ 330 | def __init__(self, response): 331 | # response doesn't support tell() and read(), required by 332 | # GzipFile 333 | if not gzip: 334 | raise SpeedtestHTTPError('HTTP response body is gzip encoded, ' 335 | 'but gzip support is not available') 336 | IO = BytesIO or StringIO 337 | self.io = IO() 338 | while 1: 339 | chunk = response.read(1024) 340 | if len(chunk) == 0: 341 | break 342 | self.io.write(chunk) 343 | self.io.seek(0) 344 | gzip.GzipFile.__init__(self, mode='rb', fileobj=self.io) 345 | 346 | def close(self): 347 | try: 348 | gzip.GzipFile.close(self) 349 | finally: 350 | self.io.close() 351 | 352 | 353 | def get_exception(): 354 | """Helper function to work with py2.4-py3 for getting the current 355 | exception in a try/except block 356 | """ 357 | return sys.exc_info()[1] 358 | 359 | 360 | def bound_socket(*args, **kwargs): 361 | """Bind socket to a specified source IP address""" 362 | 363 | sock = SOCKET_SOCKET(*args, **kwargs) 364 | sock.bind((SOURCE, 0)) 365 | return sock 366 | 367 | 368 | def distance(origin, destination): 369 | """Determine distance between 2 sets of [lat,lon] in km""" 370 | 371 | lat1, lon1 = origin 372 | lat2, lon2 = destination 373 | radius = 6371 # km 374 | 375 | dlat = math.radians(lat2 - lat1) 376 | dlon = math.radians(lon2 - lon1) 377 | a = (math.sin(dlat / 2) * math.sin(dlat / 2) + 378 | math.cos(math.radians(lat1)) * 379 | math.cos(math.radians(lat2)) * math.sin(dlon / 2) * 380 | math.sin(dlon / 2)) 381 | c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)) 382 | d = radius * c 383 | 384 | return d 385 | 386 | 387 | def build_user_agent(): 388 | """Build a Mozilla/5.0 compatible User-Agent string""" 389 | 390 | global USER_AGENT 391 | if USER_AGENT: 392 | return USER_AGENT 393 | 394 | ua_tuple = ( 395 | 'Mozilla/5.0', 396 | '(%s; U; %s; en-us)' % (platform.system(), platform.architecture()[0]), 397 | 'Python/%s' % platform.python_version(), 398 | '(KHTML, like Gecko)', 399 | 'speedtest-cli/%s' % __version__ 400 | ) 401 | USER_AGENT = ' '.join(ua_tuple) 402 | printer(USER_AGENT, debug=True) 403 | return USER_AGENT 404 | 405 | 406 | def build_request(url, data=None, headers=None, bump=''): 407 | """Build a urllib2 request object 408 | 409 | This function automatically adds a User-Agent header to all requests 410 | 411 | """ 412 | 413 | if not USER_AGENT: 414 | build_user_agent() 415 | 416 | if not headers: 417 | headers = {} 418 | 419 | if url[0] == ':': 420 | schemed_url = '%s%s' % (SCHEME, url) 421 | else: 422 | schemed_url = url 423 | 424 | if '?' in url: 425 | delim = '&' 426 | else: 427 | delim = '?' 428 | 429 | # WHO YOU GONNA CALL? CACHE BUSTERS! 430 | final_url = '%s%sx=%s.%s' % (schemed_url, delim, 431 | int(timeit.time.time() * 1000), 432 | bump) 433 | 434 | headers.update({ 435 | 'User-Agent': USER_AGENT, 436 | 'Cache-Control': 'no-cache', 437 | }) 438 | 439 | printer('%s %s' % (('GET', 'POST')[bool(data)], final_url), 440 | debug=True) 441 | 442 | return Request(final_url, data=data, headers=headers) 443 | 444 | 445 | def catch_request(request): 446 | """Helper function to catch common exceptions encountered when 447 | establishing a connection with a HTTP/HTTPS request 448 | 449 | """ 450 | 451 | try: 452 | uh = urlopen(request) 453 | return uh, False 454 | except HTTP_ERRORS: 455 | e = get_exception() 456 | return None, e 457 | 458 | 459 | def get_response_stream(response): 460 | """Helper function to return either a Gzip reader if 461 | ``Content-Encoding`` is ``gzip`` otherwise the response itself 462 | 463 | """ 464 | 465 | try: 466 | getheader = response.headers.getheader 467 | except AttributeError: 468 | getheader = response.getheader 469 | 470 | if getheader('content-encoding') == 'gzip': 471 | return GzipDecodedResponse(response) 472 | 473 | return response 474 | 475 | 476 | def get_attributes_by_tag_name(dom, tag_name): 477 | """Retrieve an attribute from an XML document and return it in a 478 | consistent format 479 | 480 | Only used with xml.dom.minidom, which is likely only to be used 481 | with python versions older than 2.5 482 | """ 483 | elem = dom.getElementsByTagName(tag_name)[0] 484 | return dict(list(elem.attributes.items())) 485 | 486 | 487 | def print_dots(current, total, start=False, end=False): 488 | """Built in callback function used by Thread classes for printing 489 | status 490 | """ 491 | 492 | if SHUTDOWN_EVENT.isSet(): 493 | return 494 | 495 | sys.stdout.write('.') 496 | if current + 1 == total and end is True: 497 | sys.stdout.write('\n') 498 | sys.stdout.flush() 499 | 500 | 501 | def do_nothing(*args, **kwargs): 502 | pass 503 | 504 | 505 | class HTTPDownloader(threading.Thread): 506 | """Thread class for retrieving a URL""" 507 | 508 | def __init__(self, i, request, start, timeout): 509 | threading.Thread.__init__(self) 510 | self.request = request 511 | self.result = [0] 512 | self.starttime = start 513 | self.timeout = timeout 514 | self.i = i 515 | 516 | def run(self): 517 | try: 518 | if (timeit.default_timer() - self.starttime) <= self.timeout: 519 | f = urlopen(self.request) 520 | while (not SHUTDOWN_EVENT.isSet() and 521 | (timeit.default_timer() - self.starttime) <= 522 | self.timeout): 523 | self.result.append(len(f.read(10240))) 524 | if self.result[-1] == 0: 525 | break 526 | f.close() 527 | except IOError: 528 | pass 529 | 530 | 531 | class HTTPUploaderData(object): 532 | """File like object to improve cutting off the upload once the timeout 533 | has been reached 534 | """ 535 | 536 | def __init__(self, length, start, timeout): 537 | self.length = length 538 | self.start = start 539 | self.timeout = timeout 540 | 541 | self._data = None 542 | 543 | self.total = [0] 544 | 545 | def pre_allocate(self): 546 | chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' 547 | multiplier = int(round(int(self.length) / 36.0)) 548 | IO = BytesIO or StringIO 549 | self._data = IO( 550 | ('content1=%s' % 551 | (chars * multiplier)[0:int(self.length) - 9] 552 | ).encode() 553 | ) 554 | 555 | @property 556 | def data(self): 557 | if not self._data: 558 | self.pre_allocate() 559 | return self._data 560 | 561 | def read(self, n=10240): 562 | if ((timeit.default_timer() - self.start) <= self.timeout and 563 | not SHUTDOWN_EVENT.isSet()): 564 | chunk = self.data.read(n) 565 | self.total.append(len(chunk)) 566 | return chunk 567 | else: 568 | raise SpeedtestUploadTimeout() 569 | 570 | def __len__(self): 571 | return self.length 572 | 573 | 574 | class HTTPUploader(threading.Thread): 575 | """Thread class for putting a URL""" 576 | 577 | def __init__(self, i, request, start, size, timeout): 578 | threading.Thread.__init__(self) 579 | self.request = request 580 | self.request.data.start = self.starttime = start 581 | self.size = size 582 | self.result = None 583 | self.timeout = timeout 584 | self.i = i 585 | 586 | def run(self): 587 | request = self.request 588 | try: 589 | if ((timeit.default_timer() - self.starttime) <= self.timeout and 590 | not SHUTDOWN_EVENT.isSet()): 591 | try: 592 | f = urlopen(request) 593 | except TypeError: 594 | # PY24 expects a string or buffer 595 | # This also causes issues with Ctrl-C, but we will concede 596 | # for the moment that Ctrl-C on PY24 isn't immediate 597 | request = build_request(self.request.get_full_url(), 598 | data=request.data.read(self.size)) 599 | f = urlopen(request) 600 | f.read(11) 601 | f.close() 602 | self.result = sum(self.request.data.total) 603 | else: 604 | self.result = 0 605 | except (IOError, SpeedtestUploadTimeout): 606 | self.result = sum(self.request.data.total) 607 | 608 | 609 | class SpeedtestResults(object): 610 | """Class for holding the results of a speedtest, including: 611 | 612 | Download speed 613 | Upload speed 614 | Ping/Latency to test server 615 | Data about server that the test was run against 616 | 617 | Additionally this class can return a result data as a dictionary or CSV, 618 | as well as submit a POST of the result data to the speedtest.net API 619 | to get a share results image link. 620 | """ 621 | 622 | def __init__(self, download=0, upload=0, ping=0, server=None): 623 | self.download = download 624 | self.upload = upload 625 | self.ping = ping 626 | if server is None: 627 | self.server = {} 628 | else: 629 | self.server = server 630 | self._share = None 631 | self.timestamp = '%sZ' % datetime.datetime.utcnow().isoformat() 632 | self.bytes_received = 0 633 | self.bytes_sent = 0 634 | 635 | def __repr__(self): 636 | return repr(self.dict()) 637 | 638 | def share(self): 639 | """POST data to the speedtest.net API to obtain a share results 640 | link 641 | """ 642 | 643 | if self._share: 644 | return self._share 645 | 646 | download = int(round(self.download / 1000.0, 0)) 647 | ping = int(round(self.ping, 0)) 648 | upload = int(round(self.upload / 1000.0, 0)) 649 | 650 | # Build the request to send results back to speedtest.net 651 | # We use a list instead of a dict because the API expects parameters 652 | # in a certain order 653 | api_data = [ 654 | 'recommendedserverid=%s' % self.server['id'], 655 | 'ping=%s' % ping, 656 | 'screenresolution=', 657 | 'promo=', 658 | 'download=%s' % download, 659 | 'screendpi=', 660 | 'upload=%s' % upload, 661 | 'testmethod=http', 662 | 'hash=%s' % md5(('%s-%s-%s-%s' % 663 | (ping, upload, download, '297aae72')) 664 | .encode()).hexdigest(), 665 | 'touchscreen=none', 666 | 'startmode=pingselect', 667 | 'accuracy=1', 668 | 'bytesreceived=%s' % self.bytes_received, 669 | 'bytessent=%s' % self.bytes_sent, 670 | 'serverid=%s' % self.server['id'], 671 | ] 672 | 673 | headers = {'Referer': 'http://c.speedtest.net/flash/speedtest.swf'} 674 | request = build_request('://www.speedtest.net/api/api.php', 675 | data='&'.join(api_data).encode(), 676 | headers=headers) 677 | f, e = catch_request(request) 678 | if e: 679 | raise ShareResultsConnectFailure(e) 680 | 681 | response = f.read() 682 | code = f.code 683 | f.close() 684 | 685 | if int(code) != 200: 686 | raise ShareResultsSubmitFailure('Could not submit results to ' 687 | 'speedtest.net') 688 | 689 | qsargs = parse_qs(response.decode()) 690 | resultid = qsargs.get('resultid') 691 | if not resultid or len(resultid) != 1: 692 | raise ShareResultsSubmitFailure('Could not submit results to ' 693 | 'speedtest.net') 694 | 695 | self._share = 'http://www.speedtest.net/result/%s.png' % resultid[0] 696 | 697 | return self._share 698 | 699 | def dict(self): 700 | """Return dictionary of result data""" 701 | 702 | return { 703 | 'download': self.download, 704 | 'upload': self.upload, 705 | 'ping': self.ping, 706 | 'server': self.server, 707 | 'timestamp': self.timestamp, 708 | 'bytes_sent': self.bytes_sent, 709 | 'bytes_received': self.bytes_received, 710 | 'share': self._share, 711 | } 712 | 713 | def csv(self, delimiter=','): 714 | """Return data in CSV format""" 715 | 716 | data = self.dict() 717 | out = StringIO() 718 | writer = csv.writer(out, delimiter=delimiter, lineterminator='') 719 | row = [data['server']['id'], data['server']['sponsor'], 720 | data['server']['name'], data['timestamp'], 721 | data['server']['d'], data['ping'], data['download'], 722 | data['upload']] 723 | writer.writerow([to_utf8(v) for v in row]) 724 | return out.getvalue() 725 | 726 | def json(self, pretty=False): 727 | """Return data in JSON format""" 728 | 729 | kwargs = {} 730 | if pretty: 731 | kwargs.update({ 732 | 'indent': 4, 733 | 'sort_keys': True 734 | }) 735 | return json.dumps(self.dict(), **kwargs) 736 | 737 | 738 | class Speedtest(object): 739 | """Class for performing standard speedtest.net testing operations""" 740 | 741 | def __init__(self, config=None): 742 | self.config = {} 743 | self.get_config() 744 | if config is not None: 745 | self.config.update(config) 746 | 747 | self.servers = {} 748 | self.closest = [] 749 | self.best = {} 750 | 751 | self.results = SpeedtestResults() 752 | 753 | def get_config(self): 754 | """Download the speedtest.net configuration and return only the data 755 | we are interested in 756 | """ 757 | 758 | headers = {} 759 | if gzip: 760 | headers['Accept-Encoding'] = 'gzip' 761 | request = build_request('://www.speedtest.net/speedtest-config.php', 762 | headers=headers) 763 | uh, e = catch_request(request) 764 | if e: 765 | raise ConfigRetrievalError(e) 766 | configxml = [] 767 | 768 | stream = get_response_stream(uh) 769 | 770 | while 1: 771 | configxml.append(stream.read(1024)) 772 | if len(configxml[-1]) == 0: 773 | break 774 | stream.close() 775 | uh.close() 776 | 777 | if int(uh.code) != 200: 778 | return None 779 | 780 | printer(''.encode().join(configxml), debug=True) 781 | 782 | try: 783 | root = ET.fromstring(''.encode().join(configxml)) 784 | server_config = root.find('server-config').attrib 785 | download = root.find('download').attrib 786 | upload = root.find('upload').attrib 787 | # times = root.find('times').attrib 788 | client = root.find('client').attrib 789 | 790 | except AttributeError: 791 | root = DOM.parseString(''.join(configxml)) 792 | server_config = get_attributes_by_tag_name(root, 'server-config') 793 | download = get_attributes_by_tag_name(root, 'download') 794 | upload = get_attributes_by_tag_name(root, 'upload') 795 | # times = get_attributes_by_tag_name(root, 'times') 796 | client = get_attributes_by_tag_name(root, 'client') 797 | 798 | ignore_servers = list( 799 | map(int, server_config['ignoreids'].split(',')) 800 | ) 801 | 802 | ratio = int(upload['ratio']) 803 | upload_max = int(upload['maxchunkcount']) 804 | up_sizes = [32768, 65536, 131072, 262144, 524288, 1048576, 7340032] 805 | sizes = { 806 | 'upload': up_sizes[ratio - 1:], 807 | 'download': [350, 500, 750, 1000, 1500, 2000, 2500, 808 | 3000, 3500, 4000] 809 | } 810 | 811 | size_count = len(sizes['upload']) 812 | 813 | upload_count = int(math.ceil(upload_max / size_count)) 814 | 815 | counts = { 816 | 'upload': upload_count, 817 | 'download': int(download['threadsperurl']) 818 | } 819 | 820 | threads = { 821 | 'upload': int(upload['threads']), 822 | 'download': int(server_config['threadcount']) * 2 823 | } 824 | 825 | length = { 826 | 'upload': int(upload['testlength']), 827 | 'download': int(download['testlength']) 828 | } 829 | 830 | self.config.update({ 831 | 'client': client, 832 | 'ignore_servers': ignore_servers, 833 | 'sizes': sizes, 834 | 'counts': counts, 835 | 'threads': threads, 836 | 'length': length, 837 | 'upload_max': upload_count * size_count 838 | }) 839 | 840 | self.lat_lon = (float(client['lat']), float(client['lon'])) 841 | 842 | printer(self.config, debug=True) 843 | 844 | return self.config 845 | 846 | def get_servers(self, servers=None): 847 | """Retrieve a the list of speedtest.net servers, optionally filtered 848 | to servers matching those specified in the ``servers`` argument 849 | """ 850 | if servers is None: 851 | servers = [] 852 | 853 | self.servers.clear() 854 | 855 | for i, s in enumerate(servers): 856 | try: 857 | servers[i] = int(s) 858 | except ValueError: 859 | raise InvalidServerIDType('%s is an invalid server type, must ' 860 | 'be int' % s) 861 | 862 | urls = [ 863 | '://www.speedtest.net/speedtest-servers-static.php', 864 | 'http://c.speedtest.net/speedtest-servers-static.php', 865 | '://www.speedtest.net/speedtest-servers.php', 866 | 'http://c.speedtest.net/speedtest-servers.php', 867 | ] 868 | 869 | headers = {} 870 | if gzip: 871 | headers['Accept-Encoding'] = 'gzip' 872 | 873 | errors = [] 874 | for url in urls: 875 | try: 876 | request = build_request('%s?threads=%s' % 877 | (url, 878 | self.config['threads']['download']), 879 | headers=headers) 880 | uh, e = catch_request(request) 881 | if e: 882 | errors.append('%s' % e) 883 | raise ServersRetrievalError() 884 | 885 | stream = get_response_stream(uh) 886 | 887 | serversxml = [] 888 | while 1: 889 | serversxml.append(stream.read(1024)) 890 | if len(serversxml[-1]) == 0: 891 | break 892 | 893 | stream.close() 894 | uh.close() 895 | 896 | if int(uh.code) != 200: 897 | raise ServersRetrievalError() 898 | 899 | printer(''.encode().join(serversxml), debug=True) 900 | 901 | try: 902 | try: 903 | root = ET.fromstring(''.encode().join(serversxml)) 904 | elements = root.getiterator('server') 905 | except AttributeError: 906 | root = DOM.parseString(''.join(serversxml)) 907 | elements = root.getElementsByTagName('server') 908 | except (SyntaxError, xml.parsers.expat.ExpatError): 909 | raise ServersRetrievalError() 910 | 911 | for server in elements: 912 | try: 913 | attrib = server.attrib 914 | except AttributeError: 915 | attrib = dict(list(server.attributes.items())) 916 | 917 | if servers and int(attrib.get('id')) not in servers: 918 | continue 919 | 920 | if int(attrib.get('id')) in self.config['ignore_servers']: 921 | continue 922 | 923 | try: 924 | d = distance(self.lat_lon, 925 | (float(attrib.get('lat')), 926 | float(attrib.get('lon')))) 927 | except: 928 | continue 929 | 930 | attrib['d'] = d 931 | 932 | try: 933 | self.servers[d].append(attrib) 934 | except KeyError: 935 | self.servers[d] = [attrib] 936 | 937 | printer(''.encode().join(serversxml), debug=True) 938 | 939 | break 940 | 941 | except ServersRetrievalError: 942 | continue 943 | 944 | if servers and not self.servers: 945 | raise NoMatchedServers() 946 | 947 | return self.servers 948 | 949 | def set_mini_server(self, server): 950 | """Instead of querying for a list of servers, set a link to a 951 | speedtest mini server 952 | """ 953 | 954 | urlparts = urlparse(server) 955 | 956 | name, ext = os.path.splitext(urlparts[2]) 957 | if ext: 958 | url = os.path.dirname(server) 959 | else: 960 | url = server 961 | 962 | request = build_request(url) 963 | uh, e = catch_request(request) 964 | if e: 965 | raise SpeedtestMiniConnectFailure('Failed to connect to %s' % 966 | server) 967 | else: 968 | text = uh.read() 969 | uh.close() 970 | 971 | extension = re.findall('upload_?[Ee]xtension: "([^"]+)"', 972 | text.decode()) 973 | if not extension: 974 | for ext in ['php', 'asp', 'aspx', 'jsp']: 975 | try: 976 | f = urlopen('%s/speedtest/upload.%s' % (url, ext)) 977 | except: 978 | pass 979 | else: 980 | data = f.read().strip().decode() 981 | if (f.code == 200 and 982 | len(data.splitlines()) == 1 and 983 | re.match('size=[0-9]', data)): 984 | extension = [ext] 985 | break 986 | if not urlparts or not extension: 987 | raise InvalidSpeedtestMiniServer('Invalid Speedtest Mini Server: ' 988 | '%s' % server) 989 | 990 | self.servers = [{ 991 | 'sponsor': 'Speedtest Mini', 992 | 'name': urlparts[1], 993 | 'd': 0, 994 | 'url': '%s/speedtest/upload.%s' % (url.rstrip('/'), extension[0]), 995 | 'latency': 0, 996 | 'id': 0 997 | }] 998 | 999 | return self.servers 1000 | 1001 | def get_closest_servers(self, limit=5): 1002 | """Limit servers to the closest speedtest.net servers based on 1003 | geographic distance 1004 | """ 1005 | 1006 | if not self.servers: 1007 | self.get_servers() 1008 | 1009 | for d in sorted(self.servers.keys()): 1010 | for s in self.servers[d]: 1011 | self.closest.append(s) 1012 | if len(self.closest) == limit: 1013 | break 1014 | else: 1015 | continue 1016 | break 1017 | 1018 | printer(self.closest, debug=True) 1019 | return self.closest 1020 | 1021 | def get_best_server(self, servers=None): 1022 | """Perform a speedtest.net "ping" to determine which speedtest.net 1023 | server has the lowest latency 1024 | """ 1025 | 1026 | if not servers: 1027 | if not self.closest: 1028 | servers = self.get_closest_servers() 1029 | servers = self.closest 1030 | 1031 | results = {} 1032 | for server in servers: 1033 | cum = [] 1034 | url = os.path.dirname(server['url']) 1035 | urlparts = urlparse('%s/latency.txt' % url) 1036 | printer('%s %s/latency.txt' % ('GET', url), debug=True) 1037 | for _ in range(0, 3): 1038 | try: 1039 | if urlparts[0] == 'https': 1040 | h = HTTPSConnection(urlparts[1]) 1041 | else: 1042 | h = HTTPConnection(urlparts[1]) 1043 | headers = {'User-Agent': USER_AGENT} 1044 | start = timeit.default_timer() 1045 | h.request("GET", urlparts[2], headers=headers) 1046 | r = h.getresponse() 1047 | total = (timeit.default_timer() - start) 1048 | except HTTP_ERRORS: 1049 | e = get_exception() 1050 | printer('%r' % e, debug=True) 1051 | cum.append(3600) 1052 | continue 1053 | 1054 | text = r.read(9) 1055 | if int(r.status) == 200 and text == 'test=test'.encode(): 1056 | cum.append(total) 1057 | else: 1058 | cum.append(3600) 1059 | h.close() 1060 | 1061 | avg = round((sum(cum) / 6) * 1000.0, 3) 1062 | results[avg] = server 1063 | 1064 | try: 1065 | fastest = sorted(results.keys())[0] 1066 | except IndexError: 1067 | raise SpeedtestBestServerFailure('Unable to connect to servers to ' 1068 | 'test latency.') 1069 | best = results[fastest] 1070 | best['latency'] = fastest 1071 | 1072 | self.results.ping = fastest 1073 | self.results.server = best 1074 | 1075 | self.best.update(best) 1076 | printer(best, debug=True) 1077 | return best 1078 | 1079 | def download(self, callback=do_nothing): 1080 | """Test download speed against speedtest.net""" 1081 | 1082 | urls = [] 1083 | for size in self.config['sizes']['download']: 1084 | for _ in range(0, self.config['counts']['download']): 1085 | urls.append('%s/random%sx%s.jpg' % 1086 | (os.path.dirname(self.best['url']), size, size)) 1087 | 1088 | request_count = len(urls) 1089 | requests = [] 1090 | for i, url in enumerate(urls): 1091 | requests.append(build_request(url, bump=i)) 1092 | 1093 | def producer(q, requests, request_count): 1094 | for i, request in enumerate(requests): 1095 | thread = HTTPDownloader(i, request, start, 1096 | self.config['length']['download']) 1097 | thread.start() 1098 | q.put(thread, True) 1099 | callback(i, request_count, start=True) 1100 | 1101 | finished = [] 1102 | 1103 | def consumer(q, request_count): 1104 | while len(finished) < request_count: 1105 | thread = q.get(True) 1106 | while thread.isAlive(): 1107 | thread.join(timeout=0.1) 1108 | finished.append(sum(thread.result)) 1109 | callback(thread.i, request_count, end=True) 1110 | 1111 | q = Queue(self.config['threads']['download']) 1112 | prod_thread = threading.Thread(target=producer, 1113 | args=(q, requests, request_count)) 1114 | cons_thread = threading.Thread(target=consumer, 1115 | args=(q, request_count)) 1116 | start = timeit.default_timer() 1117 | prod_thread.start() 1118 | cons_thread.start() 1119 | while prod_thread.isAlive(): 1120 | prod_thread.join(timeout=0.1) 1121 | while cons_thread.isAlive(): 1122 | cons_thread.join(timeout=0.1) 1123 | 1124 | stop = timeit.default_timer() 1125 | self.results.bytes_received = sum(finished) 1126 | self.results.download = ( 1127 | (self.results.bytes_received / (stop - start)) * 8.0 1128 | ) 1129 | if self.results.download > 100000: 1130 | self.config['threads']['upload'] = 8 1131 | return self.results.download 1132 | 1133 | def upload(self, callback=do_nothing, pre_allocate=True): 1134 | """Test upload speed against speedtest.net""" 1135 | 1136 | sizes = [] 1137 | 1138 | for size in self.config['sizes']['upload']: 1139 | for _ in range(0, self.config['counts']['upload']): 1140 | sizes.append(size) 1141 | 1142 | # request_count = len(sizes) 1143 | request_count = self.config['upload_max'] 1144 | 1145 | requests = [] 1146 | for i, size in enumerate(sizes): 1147 | # We set ``0`` for ``start`` and handle setting the actual 1148 | # ``start`` in ``HTTPUploader`` to get better measurements 1149 | data = HTTPUploaderData(size, 0, self.config['length']['upload']) 1150 | if pre_allocate: 1151 | data.pre_allocate() 1152 | requests.append( 1153 | ( 1154 | build_request(self.best['url'], data), 1155 | size 1156 | ) 1157 | ) 1158 | 1159 | def producer(q, requests, request_count): 1160 | for i, request in enumerate(requests[:request_count]): 1161 | thread = HTTPUploader(i, request[0], start, request[1], 1162 | self.config['length']['upload']) 1163 | thread.start() 1164 | q.put(thread, True) 1165 | callback(i, request_count, start=True) 1166 | 1167 | finished = [] 1168 | 1169 | def consumer(q, request_count): 1170 | while len(finished) < request_count: 1171 | thread = q.get(True) 1172 | while thread.isAlive(): 1173 | thread.join(timeout=0.1) 1174 | finished.append(thread.result) 1175 | callback(thread.i, request_count, end=True) 1176 | 1177 | q = Queue(self.config['threads']['upload']) 1178 | prod_thread = threading.Thread(target=producer, 1179 | args=(q, requests, request_count)) 1180 | cons_thread = threading.Thread(target=consumer, 1181 | args=(q, request_count)) 1182 | start = timeit.default_timer() 1183 | prod_thread.start() 1184 | cons_thread.start() 1185 | while prod_thread.isAlive(): 1186 | prod_thread.join(timeout=0.1) 1187 | while cons_thread.isAlive(): 1188 | cons_thread.join(timeout=0.1) 1189 | 1190 | stop = timeit.default_timer() 1191 | self.results.bytes_sent = sum(finished) 1192 | self.results.upload = ( 1193 | (self.results.bytes_sent / (stop - start)) * 8.0 1194 | ) 1195 | return self.results.upload 1196 | 1197 | 1198 | def ctrl_c(signum, frame): 1199 | """Catch Ctrl-C key sequence and set a SHUTDOWN_EVENT for our threaded 1200 | operations 1201 | """ 1202 | 1203 | SHUTDOWN_EVENT.set() 1204 | print_('\nCancelling...') 1205 | sys.exit(0) 1206 | 1207 | 1208 | def version(): 1209 | """Print the version""" 1210 | 1211 | print_(__version__) 1212 | sys.exit(0) 1213 | 1214 | 1215 | def csv_header(): 1216 | """Print the CSV Headers""" 1217 | 1218 | print_('Server ID,Sponsor,Server Name,Timestamp,Distance,Ping,Download,' 1219 | 'Upload') 1220 | sys.exit(0) 1221 | 1222 | 1223 | def parse_args(): 1224 | """Function to handle building and parsing of command line arguments""" 1225 | description = ( 1226 | 'Command line interface for testing internet bandwidth using ' 1227 | 'speedtest.net.\n' 1228 | '------------------------------------------------------------' 1229 | '--------------\n' 1230 | 'https://github.com/sivel/speedtest-cli') 1231 | 1232 | parser = ArgParser(description=description) 1233 | # Give optparse.OptionParser an `add_argument` method for 1234 | # compatibility with argparse.ArgumentParser 1235 | try: 1236 | parser.add_argument = parser.add_option 1237 | except AttributeError: 1238 | pass 1239 | parser.add_argument('--no-download', dest='download', default=True, 1240 | action='store_const', const=False, 1241 | help='Do not perform download test') 1242 | parser.add_argument('--no-upload', dest='upload', default=True, 1243 | action='store_const', const=False, 1244 | help='Do not perform upload test') 1245 | parser.add_argument('--bytes', dest='units', action='store_const', 1246 | const=('byte', 8), default=('bit', 1), 1247 | help='Display values in bytes instead of bits. Does ' 1248 | 'not affect the image generated by --share, nor ' 1249 | 'output from --json or --csv') 1250 | parser.add_argument('--share', action='store_true', 1251 | help='Generate and provide a URL to the speedtest.net ' 1252 | 'share results image, not displayed with --csv') 1253 | parser.add_argument('--simple', action='store_true', default=False, 1254 | help='Suppress verbose output, only show basic ' 1255 | 'information') 1256 | parser.add_argument('--csv', action='store_true', default=False, 1257 | help='Suppress verbose output, only show basic ' 1258 | 'information in CSV format. Speeds listed in ' 1259 | 'bit/s and not affected by --bytes') 1260 | parser.add_argument('--csv-delimiter', default=',', type=PARSER_TYPE_STR, 1261 | help='Single character delimiter to use in CSV ' 1262 | 'output. Default ","') 1263 | parser.add_argument('--csv-header', action='store_true', default=False, 1264 | help='Print CSV headers') 1265 | parser.add_argument('--json', action='store_true', default=False, 1266 | help='Suppress verbose output, only show basic ' 1267 | 'information in JSON format. Speeds listed in ' 1268 | 'bit/s and not affected by --bytes') 1269 | parser.add_argument('--list', action='store_true', 1270 | help='Display a list of speedtest.net servers ' 1271 | 'sorted by distance') 1272 | parser.add_argument('--server', help='Specify a server ID to test against', 1273 | type=PARSER_TYPE_INT) 1274 | parser.add_argument('--mini', help='URL of the Speedtest Mini server') 1275 | parser.add_argument('--source', help='Source IP address to bind to') 1276 | parser.add_argument('--timeout', default=10, type=PARSER_TYPE_INT, 1277 | help='HTTP timeout in seconds. Default 10') 1278 | parser.add_argument('--secure', action='store_true', 1279 | help='Use HTTPS instead of HTTP when communicating ' 1280 | 'with speedtest.net operated servers') 1281 | parser.add_argument('--no-pre-allocate', dest='pre_allocate', 1282 | action='store_const', default=True, const=False, 1283 | help='Do not pre allocate upload data. Pre allocation ' 1284 | 'is enabled by default to improve upload ' 1285 | 'performance. To support systems with ' 1286 | 'insufficient memory, use this option to avoid a ' 1287 | 'MemoryError') 1288 | parser.add_argument('--version', action='store_true', 1289 | help='Show the version number and exit') 1290 | parser.add_argument('--debug', action='store_true', 1291 | help=ARG_SUPPRESS, default=ARG_SUPPRESS) 1292 | 1293 | options = parser.parse_args() 1294 | if isinstance(options, tuple): 1295 | args = options[0] 1296 | else: 1297 | args = options 1298 | return args 1299 | 1300 | 1301 | def validate_optional_args(args): 1302 | """Check if an argument was provided that depends on a module that may 1303 | not be part of the Python standard library. 1304 | 1305 | If such an argument is supplied, and the module does not exist, exit 1306 | with an error stating which module is missing. 1307 | """ 1308 | optional_args = { 1309 | 'json': ('json/simplejson python module', json), 1310 | 'secure': ('SSL support', HTTPSConnection), 1311 | } 1312 | 1313 | for arg, info in optional_args.items(): 1314 | if getattr(args, arg, False) and info[1] is None: 1315 | raise SystemExit('%s is not installed. --%s is ' 1316 | 'unavailable' % (info[0], arg)) 1317 | 1318 | 1319 | def printer(string, quiet=False, debug=False, **kwargs): 1320 | """Helper function to print a string only when not quiet""" 1321 | 1322 | if debug and not DEBUG: 1323 | return 1324 | 1325 | if debug: 1326 | out = '\033[1;30mDEBUG: %s\033[0m' % string 1327 | else: 1328 | out = string 1329 | 1330 | if not quiet: 1331 | print_(out, **kwargs) 1332 | 1333 | 1334 | def shell(): 1335 | """Run the full speedtest.net test""" 1336 | 1337 | global SHUTDOWN_EVENT, SOURCE, SCHEME, DEBUG 1338 | SHUTDOWN_EVENT = threading.Event() 1339 | 1340 | signal.signal(signal.SIGINT, ctrl_c) 1341 | 1342 | args = parse_args() 1343 | 1344 | # Print the version and exit 1345 | if args.version: 1346 | version() 1347 | 1348 | if not args.download and not args.upload: 1349 | raise SpeedtestCLIError('Cannot supply both --no-download and ' 1350 | '--no-upload') 1351 | 1352 | if args.csv_header: 1353 | csv_header() 1354 | 1355 | if len(args.csv_delimiter) != 1: 1356 | raise SpeedtestCLIError('--csv-delimiter must be a single character') 1357 | 1358 | validate_optional_args(args) 1359 | 1360 | socket.setdefaulttimeout(args.timeout) 1361 | 1362 | # If specified bind to a specific IP address 1363 | if args.source: 1364 | SOURCE = args.source 1365 | socket.socket = bound_socket 1366 | 1367 | if args.secure: 1368 | SCHEME = 'https' 1369 | 1370 | debug = getattr(args, 'debug', False) 1371 | if debug == 'SUPPRESSHELP': 1372 | debug = False 1373 | if debug: 1374 | DEBUG = True 1375 | 1376 | # Pre-cache the user agent string 1377 | build_user_agent() 1378 | 1379 | if args.simple or args.csv or args.json: 1380 | quiet = True 1381 | else: 1382 | quiet = False 1383 | 1384 | if args.csv or args.json: 1385 | machine_format = True 1386 | else: 1387 | machine_format = False 1388 | 1389 | # Don't set a callback if we are running quietly 1390 | if quiet or debug: 1391 | callback = do_nothing 1392 | else: 1393 | callback = print_dots 1394 | 1395 | printer('Retrieving speedtest.net configuration...', quiet) 1396 | try: 1397 | speedtest = Speedtest() 1398 | except (ConfigRetrievalError, HTTP_ERRORS): 1399 | printer('Cannot retrieve speedtest configuration') 1400 | raise SpeedtestCLIError(get_exception()) 1401 | 1402 | if args.list: 1403 | try: 1404 | speedtest.get_servers() 1405 | except (ServersRetrievalError, HTTP_ERRORS): 1406 | print_('Cannot retrieve speedtest server list') 1407 | raise SpeedtestCLIError(get_exception()) 1408 | 1409 | for _, servers in sorted(speedtest.servers.items()): 1410 | for server in servers: 1411 | line = ('%(id)5s) %(sponsor)s (%(name)s, %(country)s) ' 1412 | '[%(d)0.2f km]' % server) 1413 | try: 1414 | print_(line) 1415 | except IOError: 1416 | e = get_exception() 1417 | if e.errno != errno.EPIPE: 1418 | raise 1419 | sys.exit(0) 1420 | 1421 | # Set a filter of servers to retrieve 1422 | servers = [] 1423 | if args.server: 1424 | servers.append(args.server) 1425 | 1426 | printer('Testing from %(isp)s (%(ip)s)...' % speedtest.config['client'], 1427 | quiet) 1428 | 1429 | if not args.mini: 1430 | printer('Retrieving speedtest.net server list...', quiet) 1431 | try: 1432 | speedtest.get_servers(servers) 1433 | except NoMatchedServers: 1434 | raise SpeedtestCLIError('No matched servers: %s' % args.server) 1435 | except (ServersRetrievalError, HTTP_ERRORS): 1436 | print_('Cannot retrieve speedtest server list') 1437 | raise SpeedtestCLIError(get_exception()) 1438 | except InvalidServerIDType: 1439 | raise SpeedtestCLIError('%s is an invalid server type, must ' 1440 | 'be an int' % args.server) 1441 | 1442 | printer('Selecting best server based on ping...', quiet) 1443 | speedtest.get_best_server() 1444 | elif args.mini: 1445 | speedtest.get_best_server(speedtest.set_mini_server(args.mini)) 1446 | 1447 | results = speedtest.results 1448 | 1449 | printer('Hosted by %(sponsor)s (%(name)s) [%(d)0.2f km]: ' 1450 | '%(latency)s ms' % results.server, quiet) 1451 | 1452 | if args.download: 1453 | printer('Testing download speed', quiet, 1454 | end=('', '\n')[bool(debug)]) 1455 | speedtest.download(callback=callback) 1456 | printer('Download: %0.2f M%s/s' % 1457 | ((results.download / 1000.0 / 1000.0) / args.units[1], 1458 | args.units[0]), 1459 | quiet) 1460 | else: 1461 | printer('Skipping download test') 1462 | 1463 | if args.upload: 1464 | printer('Testing upload speed', quiet, 1465 | end=('', '\n')[bool(debug)]) 1466 | speedtest.upload(callback=callback, pre_allocate=args.pre_allocate) 1467 | printer('Upload: %0.2f M%s/s' % 1468 | ((results.upload / 1000.0 / 1000.0) / args.units[1], 1469 | args.units[0]), 1470 | quiet) 1471 | else: 1472 | printer('Skipping upload test') 1473 | 1474 | if args.simple: 1475 | print_('Ping: %s ms\nDownload: %0.2f M%s/s\nUpload: %0.2f M%s/s' % 1476 | (results.ping, 1477 | (results.download / 1000.0 / 1000.0) / args.units[1], 1478 | args.units[0], 1479 | (results.upload / 1000.0 / 1000.0) / args.units[1], 1480 | args.units[0])) 1481 | elif args.csv: 1482 | print_(results.csv(delimiter=args.csv_delimiter)) 1483 | elif args.json: 1484 | if args.share: 1485 | results.share() 1486 | print_(results.json()) 1487 | 1488 | if args.share and not machine_format: 1489 | printer('Share results: %s' % results.share()) 1490 | 1491 | 1492 | def main(): 1493 | try: 1494 | shell() 1495 | except KeyboardInterrupt: 1496 | print_('\nCancelling...') 1497 | except (SpeedtestException, SystemExit): 1498 | e = get_exception() 1499 | if getattr(e, 'code', 1) != 0: 1500 | raise SystemExit('ERROR: %s' % e) 1501 | 1502 | 1503 | if __name__ == '__main__': 1504 | main() 1505 | -------------------------------------------------------------------------------- /ss-server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | cd /root/shadowsocksr-manyuser/shadowsocks/ 4 | python server.py "$@" 5 | --------------------------------------------------------------------------------