diff --git a/Config.py b/Config.py new file mode 100644 index 0000000..7a444e6 --- /dev/null +++ b/Config.py @@ -0,0 +1,12 @@ +#Config +MYSQL_HOST = 'mdss.mengsky.net' +MYSQL_PORT = 3306 +MYSQL_USER = 'ss' +MYSQL_PASS = 'ss' +MYSQL_DB = 'shadowsocks' + +MANAGE_PASS = 'ss233333333' +#if you want manage in other server you should set this value to global ip +MANAGE_BIND_IP = '127.0.0.1' +#make sure this port is idle +MANAGE_PORT = 23333 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..013e4b7 --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/python diff --git a/asyncmgr.py b/asyncmgr.py new file mode 100644 index 0000000..8e24b42 --- /dev/null +++ b/asyncmgr.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import time +import os +import socket +import struct +import re +import logging +from shadowsocks import common +from shadowsocks import lru_cache +from shadowsocks import eventloop +import server_pool +import Config + +class ServerMgr(object): + + def __init__(self): + self._loop = None + self._request_id = 1 + self._hosts = {} + self._hostname_status = {} + self._hostname_to_cb = {} + self._cb_to_hostname = {} + self._last_time = time.time() + self._sock = None + self._servers = None + + def add_to_loop(self, loop): + if self._loop: + raise Exception('already add to loop') + self._loop = loop + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.bind((Config.MANAGE_BIND_IP, Config.MANAGE_PORT)) + self._sock.setblocking(False) + loop.add(self._sock, eventloop.POLL_IN) + loop.add_handler(self.handle_events) + + def _handle_data(self, sock): + data, addr = sock.recvfrom(128) + #manage pwd:port:passwd:action + args = data.split(':') + if len(args) < 4: + return + if args[0] == Config.MANAGE_PASS: + if args[3] == '0': + server_pool.ServerPool.get_instance().cb_del_server(args[1]) + elif args[3] == '1': + server_pool.ServerPool.get_instance().new_server(args[1], args[2]) + + def handle_events(self, events): + for sock, fd, event in events: + if sock != self._sock: + continue + if event & eventloop.POLL_ERR: + logging.error('mgr socket err') + self._loop.remove(self._sock) + self._sock.close() + # TODO when dns server is IPv6 + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.SOL_UDP) + self._sock.setblocking(False) + self._loop.add(self._sock, eventloop.POLL_IN) + else: + self._handle_data(sock) + break + + def close(self): + if self._sock: + self._sock.close() + self._sock = None + + +def test(): + pass + +if __name__ == '__main__': + test() diff --git a/config.json b/config.json new file mode 100644 index 0000000..57e9cc1 --- /dev/null +++ b/config.json @@ -0,0 +1,12 @@ +{ + "server":"0.0.0.0", + "server_ipv6": "::", + "server_port":8388, + "local_address": "127.0.0.1", + "local_port":1080, + "password":"m", + "timeout":300, + "method":"aes-256-cfb", + "fast_open": false, + "workers": 1 +} diff --git a/db_transfer.py b/db_transfer.py new file mode 100644 index 0000000..8126d0a --- /dev/null +++ b/db_transfer.py @@ -0,0 +1,148 @@ +#!/usr/bin/python +# -*- coding: UTF-8 -*- + +import logging +import cymysql +import time +import sys +from server_pool import ServerPool +import Config + +class DbTransfer(object): + + instance = None + + def __init__(self): + self.last_get_transfer = {} + + @staticmethod + def get_instance(): + if DbTransfer.instance is None: + DbTransfer.instance = DbTransfer() + return DbTransfer.instance + + def push_db_all_user(self): + #更新用户流量到数据库 + last_transfer = self.last_get_transfer + curr_transfer = ServerPool.get_instance().get_servers_transfer() + #上次和本次的增量 + dt_transfer = {} + for id in curr_transfer.keys(): + if id in last_transfer: + if last_transfer[id][0] == curr_transfer[id][0] and last_transfer[id][1] == curr_transfer[id][1]: + continue + elif curr_transfer[id][0] == 0 and curr_transfer[id][1] == 0: + continue + elif last_transfer[id][0] <= curr_transfer[id][0] and \ + last_transfer[id][1] <= curr_transfer[id][1]: + dt_transfer[id] = [curr_transfer[id][0] - last_transfer[id][0], + curr_transfer[id][1] - last_transfer[id][1]] + else: + dt_transfer[id] = [curr_transfer[id][0], curr_transfer[id][1]] + else: + if curr_transfer[id][0] == 0 and curr_transfer[id][1] == 0: + continue + dt_transfer[id] = [curr_transfer[id][0], curr_transfer[id][1]] + + self.last_get_transfer = curr_transfer + query_head = 'UPDATE user' + query_sub_when = '' + query_sub_when2 = '' + query_sub_in = None + last_time = time.time() + for id in dt_transfer.keys(): + query_sub_when += ' WHEN %s THEN u+%s' % (id, dt_transfer[id][0]) + query_sub_when2 += ' WHEN %s THEN d+%s' % (id, dt_transfer[id][1]) + if query_sub_in is not None: + query_sub_in += ',%s' % id + else: + query_sub_in = '%s' % id + if query_sub_when == '': + return + query_sql = query_head + ' SET u = CASE port' + query_sub_when + \ + ' END, d = CASE port' + query_sub_when2 + \ + ' END, t = ' + str(int(last_time)) + \ + ' WHERE port IN (%s)' % query_sub_in + #print query_sql + conn = cymysql.connect(host=Config.MYSQL_HOST, port=Config.MYSQL_PORT, user=Config.MYSQL_USER, + passwd=Config.MYSQL_PASS, db=Config.MYSQL_DB, charset='utf8') + cur = conn.cursor() + cur.execute(query_sql) + cur.close() + conn.commit() + conn.close() + + @staticmethod + def pull_db_all_user(): + #数据库所有用户信息 + conn = cymysql.connect(host=Config.MYSQL_HOST, port=Config.MYSQL_PORT, user=Config.MYSQL_USER, + passwd=Config.MYSQL_PASS, db=Config.MYSQL_DB, charset='utf8') + cur = conn.cursor() + cur.execute("SELECT port, u, d, transfer_enable, passwd, switch, enable, plan FROM user") + rows = [] + for r in cur.fetchall(): + rows.append(list(r)) + cur.close() + conn.close() + return rows + + @staticmethod + def del_server_out_of_bound_safe(last_rows, rows): + #停止超流量的服务 + #启动没超流量的服务 + #需要动态载入switchrule,以便实时修改规则 + cur_servers = {} + for row in rows: + try: + import switchrule + allow = switchrule.isTurnOn(row[7], row[5]) and row[6] == 1 and row[1] + row[2] < row[3] + except Exception, e: + allow = False + + cur_servers[row[0]] = row[4] + + if ServerPool.get_instance().server_is_run(row[0]) > 0: + if not allow: + logging.info('db stop server at port [%s]' % (row[0])) + ServerPool.get_instance().del_server(row[0]) + elif (row[0] in ServerPool.get_instance().tcp_servers_pool and ServerPool.get_instance().tcp_servers_pool[row[0]]._config['password'] != row[4]) \ + or (row[0] in ServerPool.get_instance().tcp_ipv6_servers_pool and ServerPool.get_instance().tcp_ipv6_servers_pool[row[0]]._config['password'] != row[4]): + #password changed + logging.info('db stop server at port [%s] reason: password changed' % (row[0])) + ServerPool.get_instance().del_server(row[0]) + elif ServerPool.get_instance().server_run_status(row[0]) is False: + if allow: + logging.info('db start server at port [%s] pass [%s]' % (row[0], row[4])) + ServerPool.get_instance().new_server(row[0], row[4]) + + for row in last_rows: + if row[0] in cur_servers: + if row[4] == cur_servers[row[0]]: + pass + else: + logging.info('db stop server at port [%s] reason: port not exist' % (row[0])) + ServerPool.get_instance().del_server(row[0]) + + @staticmethod + def thread_db(): + import socket + import time + timeout = 60 + socket.setdefaulttimeout(timeout) + last_rows = [] + while True: + #logging.warn('db loop') + + try: + DbTransfer.get_instance().push_db_all_user() + rows = DbTransfer.get_instance().pull_db_all_user() + DbTransfer.del_server_out_of_bound_safe(last_rows, rows) + last_rows = rows + except Exception as e: + logging.warn('db thread except:%s' % e) + finally: + time.sleep(15) + + +#SQLData.pull_db_all_user() +#print DbTransfer.get_instance().test() diff --git a/server.py b/server.py new file mode 100644 index 0000000..ad1e558 --- /dev/null +++ b/server.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import time +import sys +import thread +import os +os.chdir(os.path.split(os.path.realpath(__file__))[0]) + +import server_pool +import db_transfer + +#def test(): +# thread.start_new_thread(DbTransfer.thread_db, ()) +# Api.web_server() + +if __name__ == '__main__': + #server_pool.ServerPool.get_instance() + #server_pool.ServerPool.get_instance().new_server(2333, '2333') + thread.start_new_thread(db_transfer.DbTransfer.thread_db, ()) + while True: + time.sleep(99999) diff --git a/server_pool.py b/server_pool.py new file mode 100644 index 0000000..c123271 --- /dev/null +++ b/server_pool.py @@ -0,0 +1,211 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import logging +import time +from shadowsocks import utils +from shadowsocks import eventloop +from shadowsocks import tcprelay +from shadowsocks import udprelay +from shadowsocks import asyncdns +import thread +import threading +import sys +import asyncmgr +import Config +from socket import * + +class ServerPool(object): + + instance = None + + def __init__(self): + utils.check_python() + self.config = utils.get_config(False) + utils.print_shadowsocks() + self.dns_resolver = asyncdns.DNSResolver() + self.mgr = asyncmgr.ServerMgr() + self.udp_on = True ### UDP switch ===================================== + + self.tcp_servers_pool = {} + self.tcp_ipv6_servers_pool = {} + self.udp_servers_pool = {} + self.udp_ipv6_servers_pool = {} + + self.loop = eventloop.EventLoop() + thread.start_new_thread(ServerPool._loop, (self.loop, self.dns_resolver, self.mgr)) + + @staticmethod + def get_instance(): + if ServerPool.instance is None: + ServerPool.instance = ServerPool() + return ServerPool.instance + + @staticmethod + def _loop(loop, dns_resolver, mgr): + try: + mgr.add_to_loop(loop) + dns_resolver.add_to_loop(loop) + loop.run() + except (KeyboardInterrupt, IOError, OSError) as e: + logging.error(e) + import traceback + traceback.print_exc() + os.exit(0) + + def server_is_run(self, port): + port = int(port) + ret = 0 + if port in self.tcp_servers_pool: + ret = 1 + if port in self.tcp_ipv6_servers_pool: + ret |= 2 + return ret + + def server_run_status(self, port): + if 'server' in self.config: + if port not in self.tcp_servers_pool: + return False + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + return False + return True + + def new_server(self, port, password): + ret = True + port = int(port) + + if 'server_ipv6' in self.config: + if port in self.tcp_ipv6_servers_pool: + logging.info("server already at %s:%d" % (self.config['server_ipv6'], port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config['server'] = a_config['server_ipv6'] + a_config['server_port'] = port + a_config['password'] = password + try: + logging.info("starting server at %s:%d" % (a_config['server'], port)) + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False) + tcp_server.add_to_loop(self.loop) + self.tcp_ipv6_servers_pool.update({port: tcp_server}) + if self.udp_on: + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False) + udp_server.add_to_loop(self.loop) + self.udp_ipv6_servers_pool.update({port: udp_server}) + except Exception, e: + logging.warn("IPV6 exception") + logging.warn(e) + + if 'server' in self.config: + if port in self.tcp_servers_pool: + logging.info("server already at %s:%d" % (self.config['server'], port)) + return 'this port server is already running' + else: + a_config = self.config.copy() + a_config['server_port'] = port + a_config['password'] = password + try: + logging.info("starting server at %s:%d" % (a_config['server'], port)) + tcp_server = tcprelay.TCPRelay(a_config, self.dns_resolver, False) + tcp_server.add_to_loop(self.loop) + self.tcp_servers_pool.update({port: tcp_server}) + if self.udp_on: + udp_server = udprelay.UDPRelay(a_config, self.dns_resolver, False) + udp_server.add_to_loop(self.loop) + self.udp_servers_pool.update({port: udp_server}) + except Exception, e: + logging.warn("IPV4 exception") + logging.warn(e) + + return True + + def del_server(self, port): + port = int(port) + logging.info("del server at %d" % port) + try: + udpsock = socket(AF_INET, SOCK_DGRAM) + udpsock.sendto('%s:%s:0:0' % (Config.MANAGE_PASS, port), (Config.MANAGE_BIND_IP, Config.MANAGE_PORT)) + udpsock.close() + except Exception, e: + logging.warn(e) + return True + + def cb_del_server(self, port): + port = int(port) + + if port not in self.tcp_servers_pool: + logging.info("stopped server at %s:%d already stop" % (self.config['server'], port)) + else: + logging.info("stopped server at %s:%d" % (self.config['server'], port)) + try: + self.tcp_servers_pool[port].destroy() + del self.tcp_servers_pool[port] + except Exception, e: + logging.warn(e) + if self.udp_on: + try: + self.udp_servers_pool[port].destroy() + del self.udp_servers_pool[port] + except Exception, e: + logging.warn(e) + + if 'server_ipv6' in self.config: + if port not in self.tcp_ipv6_servers_pool: + logging.info("stopped server at %s:%d already stop" % (self.config['server_ipv6'], port)) + else: + logging.info("stopped server at %s:%d" % (self.config['server_ipv6'], port)) + try: + self.tcp_ipv6_servers_pool[port].destroy() + del self.tcp_ipv6_servers_pool[port] + except Exception, e: + logging.warn(e) + if self.udp_on: + try: + self.udp_ipv6_servers_pool[port].destroy() + del self.udp_ipv6_servers_pool[port] + except Exception, e: + logging.warn(e) + + return True + + def get_server_transfer(self, port): + port = int(port) + ret = [0, 0] + if port in self.tcp_servers_pool: + ret[0] = self.tcp_servers_pool[port].server_transfer_ul + ret[1] = self.tcp_servers_pool[port].server_transfer_dl + if port in self.tcp_ipv6_servers_pool: + ret[0] += self.tcp_ipv6_servers_pool[port].server_transfer_ul + ret[1] += self.tcp_ipv6_servers_pool[port].server_transfer_dl + return ret + + def get_servers_transfer(self): + servers = self.tcp_servers_pool.copy() + servers.update(self.tcp_ipv6_servers_pool) + ret = {} + for port in servers.keys(): + ret[port] = self.get_server_transfer(port) + return ret + diff --git a/setup.py b/setup.py deleted file mode 100644 index 07ea2db..0000000 --- a/setup.py +++ /dev/null @@ -1,39 +0,0 @@ -import codecs -from setuptools import setup - - -with codecs.open('README.rst', encoding='utf-8') as f: - long_description = f.read() - -setup( - name="shadowsocks", - version="2.6.11", - license='http://www.apache.org/licenses/LICENSE-2.0', - description="A fast tunnel proxy that help you get through firewalls", - author='clowwindy', - author_email='clowwindy42@gmail.com', - url='https://github.com/shadowsocks/shadowsocks', - packages=['shadowsocks', 'shadowsocks.crypto'], - package_data={ - 'shadowsocks': ['README.rst', 'LICENSE'] - }, - install_requires=[], - entry_points=""" - [console_scripts] - sslocal = shadowsocks.local:main - ssserver = shadowsocks.server:main - """, - classifiers=[ - 'License :: OSI Approved :: Apache Software License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Topic :: Internet :: Proxy Servers', - ], - long_description=long_description, -) diff --git a/shadowsocks.sql b/shadowsocks.sql new file mode 100644 index 0000000..8d84992 --- /dev/null +++ b/shadowsocks.sql @@ -0,0 +1,24 @@ +SET FOREIGN_KEY_CHECKS=0; + +CREATE TABLE `user` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `email` varchar(32) NOT NULL, + `pass` varchar(16) NOT NULL, + `passwd` varchar(16) NOT NULL, + `t` int(11) NOT NULL DEFAULT '0', + `u` bigint(20) NOT NULL, + `d` bigint(20) NOT NULL, + `transfer_enable` bigint(20) NOT NULL, + `port` int(11) NOT NULL, + `switch` tinyint(4) NOT NULL DEFAULT '1', + `enable` tinyint(4) NOT NULL DEFAULT '1', + `type` tinyint(4) NOT NULL DEFAULT '1', + `last_get_gift_time` int(11) NOT NULL DEFAULT '0', + `last_rest_pass_time` int(11) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`,`port`) +) ENGINE=InnoDB AUTO_INCREMENT=415 DEFAULT CHARSET=utf8; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES ('7', 'test@test.com', '123456', '0000000', '1410609560', '0', '0', '9320666234', '50000', '1', '1', '7', '0', '0'); \ No newline at end of file diff --git a/shadowsocks/common.py b/shadowsocks/common.py index fc03d55..d740160 100644 --- a/shadowsocks/common.py +++ b/shadowsocks/common.py @@ -144,6 +144,8 @@ def parse_header(data): dest_addr = None dest_port = None header_length = 0 + connecttype = (addrtype & 8) and 1 or 0 + addrtype &= ~8 if addrtype == ADDRTYPE_IPV4: if len(data) >= 7: dest_addr = socket.inet_ntoa(data[1:5]) @@ -175,7 +177,7 @@ def parse_header(data): 'encryption method' % addrtype) if dest_addr is None: return None - return addrtype, to_bytes(dest_addr), dest_port, header_length + return connecttype, to_bytes(dest_addr), dest_port, header_length class IPNetwork(object): diff --git a/shadowsocks/crypto/ctypes_libsodium.py b/shadowsocks/crypto/ctypes_libsodium.py new file mode 100644 index 0000000..efecfd4 --- /dev/null +++ b/shadowsocks/crypto/ctypes_libsodium.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_ulonglong, byref, \ + create_string_buffer, c_void_p + +__all__ = ['ciphers'] + +libsodium = None +loaded = False + +buf_size = 2048 + +# for salsa20 and chacha20 +BLOCK_SIZE = 64 + + +def load_libsodium(): + global loaded, libsodium, buf + + from ctypes.util import find_library + for p in ('sodium',): + libsodium_path = find_library(p) + if libsodium_path: + break + else: + raise Exception('libsodium not found') + logging.info('loading libsodium from %s', libsodium_path) + libsodium = CDLL(libsodium_path) + libsodium.sodium_init.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.restype = c_int + libsodium.crypto_stream_salsa20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + libsodium.crypto_stream_chacha20_xor_ic.restype = c_int + libsodium.crypto_stream_chacha20_xor_ic.argtypes = (c_void_p, c_char_p, + c_ulonglong, + c_char_p, c_ulonglong, + c_char_p) + + libsodium.sodium_init() + + buf = create_string_buffer(buf_size) + loaded = True + + +class Salsa20Crypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_libsodium() + self.key = key + self.iv = iv + self.key_ptr = c_char_p(key) + self.iv_ptr = c_char_p(iv) + if cipher_name == b'salsa20': + self.cipher = libsodium.crypto_stream_salsa20_xor_ic + elif cipher_name == b'chacha20': + self.cipher = libsodium.crypto_stream_chacha20_xor_ic + else: + raise Exception('Unknown cipher') + # byte counter, not block counter + self.counter = 0 + + def update(self, data): + global buf_size, buf + l = len(data) + + # we can only prepend some padding to make the encryption align to + # blocks + padding = self.counter % BLOCK_SIZE + if buf_size < padding + l: + buf_size = (padding + l) * 2 + buf = create_string_buffer(buf_size) + + if padding: + data = (b'\0' * padding) + data + self.cipher(byref(buf), c_char_p(data), padding + l, + self.iv_ptr, int(self.counter / BLOCK_SIZE), self.key_ptr) + self.counter += l + # buf is copied to a str object when we access buf.raw + # strip off the padding + return buf.raw[padding:padding + l] + + +ciphers = { + b'salsa20': (32, 8, Salsa20Crypto), + b'chacha20': (32, 8, Salsa20Crypto), +} + + +def test_salsa20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'salsa20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_chacha20(): + from shadowsocks.crypto import util + + cipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 1) + decipher = Salsa20Crypto(b'chacha20', b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +if __name__ == '__main__': + test_chacha20() + test_salsa20() diff --git a/shadowsocks/crypto/ctypes_openssl.py b/shadowsocks/crypto/ctypes_openssl.py new file mode 100644 index 0000000..0ef8ce0 --- /dev/null +++ b/shadowsocks/crypto/ctypes_openssl.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import logging +from ctypes import CDLL, c_char_p, c_int, c_long, byref,\ + create_string_buffer, c_void_p + +__all__ = ['ciphers'] + +libcrypto = None +loaded = False + +buf_size = 2048 + + +def load_openssl(): + global loaded, libcrypto, buf + + from ctypes.util import find_library + for p in ('crypto', 'eay32', 'libeay32'): + libcrypto_path = find_library(p) + if libcrypto_path: + break + else: + raise Exception('libcrypto(OpenSSL) not found') + logging.info('loading libcrypto from %s', libcrypto_path) + libcrypto = CDLL(libcrypto_path) + libcrypto.EVP_get_cipherbyname.restype = c_void_p + libcrypto.EVP_CIPHER_CTX_new.restype = c_void_p + + libcrypto.EVP_CipherInit_ex.argtypes = (c_void_p, c_void_p, c_char_p, + c_char_p, c_char_p, c_int) + + libcrypto.EVP_CipherUpdate.argtypes = (c_void_p, c_void_p, c_void_p, + c_char_p, c_int) + + libcrypto.EVP_CIPHER_CTX_cleanup.argtypes = (c_void_p,) + libcrypto.EVP_CIPHER_CTX_free.argtypes = (c_void_p,) + if hasattr(libcrypto, 'OpenSSL_add_all_ciphers'): + libcrypto.OpenSSL_add_all_ciphers() + + buf = create_string_buffer(buf_size) + loaded = True + + +def load_cipher(cipher_name): + func_name = b'EVP_' + cipher_name.replace(b'-', b'_') + if bytes != str: + func_name = str(func_name, 'utf-8') + cipher = getattr(libcrypto, func_name, None) + if cipher: + cipher.restype = c_void_p + return cipher() + return None + + +class CtypesCrypto(object): + def __init__(self, cipher_name, key, iv, op): + if not loaded: + load_openssl() + self._ctx = None + cipher = libcrypto.EVP_get_cipherbyname(cipher_name) + if not cipher: + cipher = load_cipher(cipher_name) + if not cipher: + raise Exception('cipher %s not found in libcrypto' % cipher_name) + key_ptr = c_char_p(key) + iv_ptr = c_char_p(iv) + self._ctx = libcrypto.EVP_CIPHER_CTX_new() + if not self._ctx: + raise Exception('can not create cipher context') + r = libcrypto.EVP_CipherInit_ex(self._ctx, cipher, None, + key_ptr, iv_ptr, c_int(op)) + if not r: + self.clean() + raise Exception('can not initialize cipher context') + + def update(self, data): + global buf_size, buf + cipher_out_len = c_long(0) + l = len(data) + if buf_size < l: + buf_size = l * 2 + buf = create_string_buffer(buf_size) + libcrypto.EVP_CipherUpdate(self._ctx, byref(buf), + byref(cipher_out_len), c_char_p(data), l) + # buf is copied to a str object when we access buf.raw + return buf.raw[:cipher_out_len.value] + + def __del__(self): + self.clean() + + def clean(self): + if self._ctx: + libcrypto.EVP_CIPHER_CTX_cleanup(self._ctx) + libcrypto.EVP_CIPHER_CTX_free(self._ctx) + + +ciphers = { + b'aes-128-cfb': (16, 16, CtypesCrypto), + b'aes-192-cfb': (24, 16, CtypesCrypto), + b'aes-256-cfb': (32, 16, CtypesCrypto), + b'aes-128-ofb': (16, 16, CtypesCrypto), + b'aes-192-ofb': (24, 16, CtypesCrypto), + b'aes-256-ofb': (32, 16, CtypesCrypto), + b'aes-128-ctr': (16, 16, CtypesCrypto), + b'aes-192-ctr': (24, 16, CtypesCrypto), + b'aes-256-ctr': (32, 16, CtypesCrypto), + b'aes-128-cfb8': (16, 16, CtypesCrypto), + b'aes-192-cfb8': (24, 16, CtypesCrypto), + b'aes-256-cfb8': (32, 16, CtypesCrypto), + b'aes-128-cfb1': (16, 16, CtypesCrypto), + b'aes-192-cfb1': (24, 16, CtypesCrypto), + b'aes-256-cfb1': (32, 16, CtypesCrypto), + b'bf-cfb': (16, 8, CtypesCrypto), + b'camellia-128-cfb': (16, 16, CtypesCrypto), + b'camellia-192-cfb': (24, 16, CtypesCrypto), + b'camellia-256-cfb': (32, 16, CtypesCrypto), + b'cast5-cfb': (16, 8, CtypesCrypto), + b'des-cfb': (8, 8, CtypesCrypto), + b'idea-cfb': (16, 8, CtypesCrypto), + b'rc2-cfb': (16, 8, CtypesCrypto), + b'rc4': (16, 0, CtypesCrypto), + b'seed-cfb': (16, 16, CtypesCrypto), +} + + +def run_method(method): + from shadowsocks.crypto import util + + cipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 1) + decipher = CtypesCrypto(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def test_aes_128_cfb(): + run_method(b'aes-128-cfb') + + +def test_aes_256_cfb(): + run_method(b'aes-256-cfb') + + +def test_aes_128_cfb8(): + run_method(b'aes-128-cfb8') + + +def test_aes_256_ofb(): + run_method(b'aes-256-ofb') + + +def test_aes_256_ctr(): + run_method(b'aes-256-ctr') + + +def test_bf_cfb(): + run_method(b'bf-cfb') + + +def test_rc4(): + run_method(b'rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/crypto/m2.py b/shadowsocks/crypto/m2.py new file mode 100644 index 0000000..4c7e148 --- /dev/null +++ b/shadowsocks/crypto/m2.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import sys +import logging + +__all__ = ['ciphers'] + +has_m2 = True +try: + __import__('M2Crypto') +except ImportError: + has_m2 = False + + +def create_cipher(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, + padding=1): + + import M2Crypto.EVP + return M2Crypto.EVP.Cipher(alg.replace('-', '_'), key, iv, op, + key_as_bytes=0, d='md5', salt=None, i=1, + padding=1) + + +def err(alg, key, iv, op, key_as_bytes=0, d=None, salt=None, i=1, padding=1): + logging.error(('M2Crypto is required to use %s, please run' + ' `apt-get install python-m2crypto`') % alg) + sys.exit(1) + + +if has_m2: + ciphers = { + b'aes-128-cfb': (16, 16, create_cipher), + b'aes-192-cfb': (24, 16, create_cipher), + b'aes-256-cfb': (32, 16, create_cipher), + b'bf-cfb': (16, 8, create_cipher), + b'camellia-128-cfb': (16, 16, create_cipher), + b'camellia-192-cfb': (24, 16, create_cipher), + b'camellia-256-cfb': (32, 16, create_cipher), + b'cast5-cfb': (16, 8, create_cipher), + b'des-cfb': (8, 8, create_cipher), + b'idea-cfb': (16, 8, create_cipher), + b'rc2-cfb': (16, 8, create_cipher), + b'rc4': (16, 0, create_cipher), + b'seed-cfb': (16, 16, create_cipher), + } +else: + ciphers = {} + + +def run_method(method): + from shadowsocks.crypto import util + + cipher = create_cipher(method, b'k' * 32, b'i' * 16, 1) + decipher = create_cipher(method, b'k' * 32, b'i' * 16, 0) + + util.run_cipher(cipher, decipher) + + +def check_env(): + # skip this test on pypy and Python 3 + try: + import __pypy__ + del __pypy__ + from nose.plugins.skip import SkipTest + raise SkipTest + except ImportError: + pass + if bytes != str: + from nose.plugins.skip import SkipTest + raise SkipTest + + +def test_aes_128_cfb(): + check_env() + run_method(b'aes-128-cfb') + + +def test_aes_256_cfb(): + check_env() + run_method(b'aes-256-cfb') + + +def test_bf_cfb(): + check_env() + run_method(b'bf-cfb') + + +def test_rc4(): + check_env() + run_method(b'rc4') + + +if __name__ == '__main__': + test_aes_128_cfb() diff --git a/shadowsocks/encrypt.py b/shadowsocks/encrypt.py index 4e87f41..834b18c 100644 --- a/shadowsocks/encrypt.py +++ b/shadowsocks/encrypt.py @@ -47,6 +47,8 @@ def try_cipher(key, method=None): def EVP_BytesToKey(password, key_len, iv_len): # equivalent to OpenSSL's EVP_BytesToKey() with count 1 # so that we make the same key and iv as nodejs version + if hasattr(password, 'encode'): + password = password.encode('utf-8') cached_key = '%s-%d-%d' % (password, key_len, iv_len) r = cached_keys.get(cached_key, None) if r: diff --git a/shadowsocks/tcprelay.py b/shadowsocks/tcprelay.py index 4834883..6b14114 100644 --- a/shadowsocks/tcprelay.py +++ b/shadowsocks/tcprelay.py @@ -101,6 +101,7 @@ class TCPRelayHandler(object): self._loop = loop self._local_sock = local_sock self._remote_sock = None + self._remote_udp = False self._config = config self._dns_resolver = dns_resolver @@ -190,6 +191,28 @@ class TCPRelayHandler(object): # and update the stream to wait for writing if not data or not sock: return False + #logging.debug("_write_to_sock %s %s %s" % (self._remote_sock, sock, self._remote_udp)) + if self._remote_sock == sock and self._remote_udp: + try: + addrtype = data[3] + if addrtype == '\x01': + addr = socket.inet_ntoa(data[4:8]) + port = struct.unpack('>H', data[8:10])[0] + logging.info('udp sendto %s:%d %d bytes from %s:%d' % (addr, port, len(data), self._client_address[0], self._client_address[1])) + sock.sendto(data[10:], (addr, port)) + elif addrtype == '\x04': + addr = socket.inet_ntop(data[4:20]) + port = struct.unpack('>H', data[20:22])[0] + logging.info('udp sendto %s:%d %d bytes from %s:%d' % (addr, port, len(data), self._client_address[0], self._client_address[1])) + sock.sendto(data[22:], (addr, port)) + elif addrtype == '\x03': + #unsupport + pass + except Exception as e: + trace = traceback.format_exc() + logging.error(trace) + return True + uncomplete = False try: l = len(data) @@ -203,6 +226,7 @@ class TCPRelayHandler(object): errno.EWOULDBLOCK): uncomplete = True else: + #traceback.print_exc() shell.print_exception(e) self.destroy() return False @@ -291,11 +315,13 @@ class TCPRelayHandler(object): header_result = parse_header(data) if header_result is None: raise Exception('can not parse header') - addrtype, remote_addr, remote_port, header_length = header_result - logging.info('connecting %s:%d from %s:%d' % - (common.to_str(remote_addr), remote_port, - self._client_address[0], self._client_address[1])) + connecttype, remote_addr, remote_port, header_length = header_result + logging.info('%s connecting %s:%d from %s:%d' % + ((connecttype == 0) and 'tcp' or 'udp', + common.to_str(remote_addr), remote_port, + self._client_address[0], self._client_address[1])) self._remote_address = (common.to_str(remote_addr), remote_port) + self._remote_udp = (connecttype != 0) # pause reading self._update_stream(STREAM_UP, WAIT_STATUS_WRITING) self._stage = STAGE_DNS @@ -323,8 +349,10 @@ class TCPRelayHandler(object): self.destroy() def _create_remote_socket(self, ip, port): - addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, - socket.SOL_TCP) + if self._remote_udp: + addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) + else: + addrs = socket.getaddrinfo(ip, port, 0, socket.SOCK_STREAM, socket.SOL_TCP) if len(addrs) == 0: raise Exception("getaddrinfo failed for %s:%d" % (ip, port)) af, socktype, proto, canonname, sa = addrs[0] @@ -336,7 +364,10 @@ class TCPRelayHandler(object): self._remote_sock = remote_sock self._fd_to_handlers[remote_sock.fileno()] = self remote_sock.setblocking(False) - remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) + if self._remote_udp: + pass + else: + remote_sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1) return remote_sock def _handle_dns_resolved(self, result, error): @@ -368,14 +399,18 @@ class TCPRelayHandler(object): # else do connect remote_sock = self._create_remote_socket(remote_addr, remote_port) - try: - remote_sock.connect((remote_addr, remote_port)) - except (OSError, IOError) as e: - if eventloop.errno_from_exception(e) == \ - errno.EINPROGRESS: - pass - self._loop.add(remote_sock, - eventloop.POLL_ERR | eventloop.POLL_OUT) + if self._remote_udp: + self._loop.add(remote_sock, + eventloop.POLL_IN) + else: + try: + remote_sock.connect((remote_addr, remote_port)) + except (OSError, IOError) as e: + if eventloop.errno_from_exception(e) == \ + errno.EINPROGRESS: + pass + self._loop.add(remote_sock, + eventloop.POLL_ERR | eventloop.POLL_OUT) self._stage = STAGE_CONNECTING self._update_stream(STREAM_UP, WAIT_STATUS_READWRITING) self._update_stream(STREAM_DOWN, WAIT_STATUS_READING) @@ -407,6 +442,7 @@ class TCPRelayHandler(object): data = self._encryptor.decrypt(data) if not data: return + self._server.server_transfer_ul += len(data) if self._stage == STAGE_STREAM: if self._is_local: data = self._encryptor.encrypt(data) @@ -428,14 +464,26 @@ class TCPRelayHandler(object): self._update_activity() data = None try: - data = self._remote_sock.recv(BUF_SIZE) + if self._remote_udp: + data, addr = self._remote_sock.recvfrom(BUF_SIZE) + port = struct.pack('>H', addr[1]) + try: + ip = socket.inet_aton(addr[0]) + data = '\x00\x00\x00\x01' + ip + port + data + except Exception as e: + ip = socket.inet_pton(socket.AF_INET6, addr[0]) + data = '\x00\x00\x00\x04' + ip + port + data + logging.info('udp recvfrom %s:%d %d bytes to %s:%d' % (addr[0], addr[1], len(data), self._client_address[0], self._client_address[1])) + else: + data = self._remote_sock.recv(BUF_SIZE) except (OSError, IOError) as e: if eventloop.errno_from_exception(e) in \ - (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK): + (errno.ETIMEDOUT, errno.EAGAIN, errno.EWOULDBLOCK, 10035): #errno.WSAEWOULDBLOCK return if not data: self.destroy() return + self._server.server_transfer_dl += len(data) if self._is_local: data = self._encryptor.decrypt(data) else: @@ -472,12 +520,16 @@ class TCPRelayHandler(object): logging.debug('got local error') if self._local_sock: logging.error(eventloop.get_sock_error(self._local_sock)) + trace = traceback.format_exc() + logging.error(trace) self.destroy() def _on_remote_error(self): logging.debug('got remote error') if self._remote_sock: logging.error(eventloop.get_sock_error(self._remote_sock)) + trace = traceback.format_exc() + logging.error(trace) self.destroy() def handle_event(self, sock, event): @@ -558,6 +610,8 @@ class TCPRelay(object): self._eventloop = None self._fd_to_handlers = {} self._last_time = time.time() + self.server_transfer_ul = 0L + self.server_transfer_dl = 0L self._timeout = config['timeout'] self._timeouts = [] # a list for all the handlers diff --git a/shadowsocks/udprelay.py b/shadowsocks/udprelay.py index 98bfaaa..ba4e9d7 100644 --- a/shadowsocks/udprelay.py +++ b/shadowsocks/udprelay.py @@ -173,6 +173,7 @@ class UDPRelay(object): client = self._cache.get(key, None) if not client: # TODO async getaddrinfo + logging.info('UDP handle_server %s:%d from %s:%d' % (common.to_str(server_addr), server_port, self._listen_addr, self._listen_port)) addrs = socket.getaddrinfo(server_addr, server_port, 0, socket.SOCK_DGRAM, socket.SOL_UDP) if addrs: diff --git a/shadowsocks/utils.py b/shadowsocks/utils.py new file mode 100644 index 0000000..785f18c --- /dev/null +++ b/shadowsocks/utils.py @@ -0,0 +1,278 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import os +import json +import sys +import getopt +import logging + + +VERBOSE_LEVEL = 5 + + +def check_python(): + info = sys.version_info + if not (info[0] == 2 and info[1] >= 6): + print 'Python 2.6 or 2.7 required' + sys.exit(1) + + +def print_shadowsocks(): + version = '' + try: + import pkg_resources + version = pkg_resources.get_distribution('shadowsocks').version + except Exception: + pass + print 'shadowsocks %s' % version + + +def find_config(): + config_path = 'config.json' + if os.path.exists(config_path): + return config_path + config_path = os.path.join(os.path.dirname(__file__), '../', 'config.json') + if os.path.exists(config_path): + return config_path + return None + + +def check_config(config): + if config.get('local_address', '') in ['0.0.0.0']: + logging.warn('warning: local set to listen 0.0.0.0, which is not safe') + if config.get('server', '') in ['127.0.0.1', 'localhost']: + logging.warn('warning: server set to listen %s:%s, are you sure?' % + (config['server'], config['server_port'])) + if (config.get('method', '') or '').lower() == '': + logging.warn('warning: table is not safe; please use a safer cipher, ' + 'like AES-256-CFB') + if (config.get('method', '') or '').lower() == 'rc4': + logging.warn('warning: RC4 is not safe; please use a safer cipher, ' + 'like AES-256-CFB') + if config.get('timeout', 300) < 100: + logging.warn('warning: your timeout %d seems too short' % + int(config.get('timeout'))) + if config.get('timeout', 300) > 600: + logging.warn('warning: your timeout %d seems too long' % + int(config.get('timeout'))) + if config.get('password') in ['mypassword', 'barfoo!']: + logging.error('DON\'T USE DEFAULT PASSWORD! Please change it in your ' + 'config.json!') + exit(1) + + +def get_config(is_local): + logging.basicConfig(level=logging.INFO, + format='%(levelname)-s: %(message)s', filemode='a+') + if is_local: + shortopts = 'hs:b:p:k:l:m:c:t:vq' + longopts = ['fast-open'] + else: + shortopts = 'hs:p:k:m:c:t:vq' + longopts = ['fast-open', 'workers:'] + try: + config_path = find_config() + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + for key, value in optlist: + if key == '-c': + config_path = value + + if config_path: + logging.info('loading config from %s' % config_path) + with open(config_path, 'rb') as f: + try: + config = json.load(f, object_hook=_decode_dict) + except ValueError as e: + logging.error('found an error in config.json: %s', + e.message) + sys.exit(1) + else: + config = {} + + optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + v_count = 0 + for key, value in optlist: + if key == '-p': + config['server_port'] = int(value) + elif key == '-k': + config['password'] = value + elif key == '-l': + config['local_port'] = int(value) + elif key == '-s': + config['server'] = value + elif key == '-m': + config['method'] = value + elif key == '-b': + config['local_address'] = value + elif key == '-v': + v_count += 1 + # '-vv' turns on more verbose mode + config['verbose'] = v_count + elif key == '-t': + config['timeout'] = int(value) + elif key == '--fast-open': + config['fast_open'] = True + elif key == '--workers': + config['workers'] = value + elif key == '-h': + if is_local: + print_local_help() + else: + print_server_help() + sys.exit(0) + elif key == '-q': + v_count -= 1 + config['verbose'] = v_count + except getopt.GetoptError as e: + print >>sys.stderr, e + print_help(is_local) + sys.exit(2) + + if not config: + logging.error('config not specified') + print_help(is_local) + sys.exit(2) + + config['password'] = config.get('password', None) + config['method'] = config.get('method', 'aes-256-cfb') + config['port_password'] = config.get('port_password', None) + config['timeout'] = int(config.get('timeout', 300)) + config['fast_open'] = config.get('fast_open', False) + config['workers'] = config.get('workers', 1) + config['verbose'] = config.get('verbose', False) + config['local_address'] = config.get('local_address', '127.0.0.1') + config['local_port'] = config.get('local_port', 1080) + if is_local: + if config.get('server', None) is None: + logging.error('server addr not specified') + print_local_help() + sys.exit(2) + else: + config['server'] = config.get('server', '0.0.0.0') + config['server_port'] = config.get('server_port', 8388) + + if not ('password' in config and config['password']): + logging.error('password not specified') + print_help(is_local) + sys.exit(2) + + logging.getLogger('').handlers = [] + logging.addLevelName(VERBOSE_LEVEL, 'VERBOSE') + if config['verbose'] >= 2: + level = VERBOSE_LEVEL + elif config['verbose'] == 1: + level = logging.DEBUG + elif config['verbose'] == -1: + level = logging.WARN + elif config['verbose'] <= -2: + level = logging.ERROR + else: + level = logging.INFO + logging.basicConfig(level=level, + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%Y-%m-%d %H:%M:%S', filemode='a+') + + check_config(config) + + return config + + +def print_help(is_local): + if is_local: + print_local_help() + else: + print_server_help() + + +def print_local_help(): + print '''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT] + [-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD] + [-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] [-q] + +optional arguments: + -h, --help show this help message and exit + -s SERVER_ADDR server address + -p SERVER_PORT server port, default: 8388 + -b LOCAL_ADDR local binding address, default: 127.0.0.1 + -l LOCAL_PORT local port, default: 1080 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + -c CONFIG path to config file + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + +Online help: +''' + + +def print_server_help(): + print '''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD + -m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open] + [--workers WORKERS] [-v] [-q] + +optional arguments: + -h, --help show this help message and exit + -s SERVER_ADDR server address, default: 0.0.0.0 + -p SERVER_PORT server port, default: 8388 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + -c CONFIG path to config file + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + --workers WORKERS number of workers, available on Unix/Linux + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors + +Online help: +''' + + +def _decode_list(data): + rv = [] + for item in data: + if isinstance(item, unicode): + item = item.encode('utf-8') + elif isinstance(item, list): + item = _decode_list(item) + elif isinstance(item, dict): + item = _decode_dict(item) + rv.append(item) + return rv + + +def _decode_dict(data): + rv = {} + for key, value in data.iteritems(): + if isinstance(key, unicode): + key = key.encode('utf-8') + if isinstance(value, unicode): + value = value.encode('utf-8') + elif isinstance(value, list): + value = _decode_list(value) + elif isinstance(value, dict): + value = _decode_dict(value) + rv[key] = value + return rv diff --git a/switchrule.py b/switchrule.py new file mode 100644 index 0000000..ce6b76e --- /dev/null +++ b/switchrule.py @@ -0,0 +1,2 @@ +def isTurnOn(plan, switch): + return True