├── flask_app ├── static │ ├── img │ │ ├── favicon.ico │ │ └── topback.gif │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ ├── css │ │ └── style.css │ └── js │ │ ├── html5shiv.min.js │ │ ├── respond.min.js │ │ ├── common.js │ │ └── moment.min.js ├── config.py ├── templates │ ├── login.html │ ├── clients.html │ ├── mappings.html │ ├── index.html │ └── base.html ├── server_views.py ├── __init__.py └── api.py ├── requires.txt ├── README.md ├── common.py ├── utils.py ├── protocol.py ├── client.py └── server.py /flask_app/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/img/favicon.ico -------------------------------------------------------------------------------- /flask_app/static/img/topback.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/img/topback.gif -------------------------------------------------------------------------------- /flask_app/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /flask_app/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /flask_app/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /flask_app/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revoll/AnyProxy/HEAD/flask_app/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /requires.txt: -------------------------------------------------------------------------------- 1 | # python3.7 2 | # python3-setuptools 3 | # python3-pip 4 | # virtualenv 5 | loguru 6 | flask 7 | flask-login 8 | flask-mail 9 | flask-moment 10 | flask-bootstrap -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnyProxy 2 | AnyProxy是一个可穿透NAT的端口映射软件。它能够将局域网内的主机端口,映射到公网主机上,实现类似“花生壳”的功能。 3 | 4 | AnyProxy使用Python语言实现,并基于Flask实现了Web监控。主要特点:支持任意的TCP和UDP端口的代理;支持多台主机同时连接;支持客户端主机的动态连接、暂停、恢复及删除。 5 | 6 | 7 | ### 如何使用 8 | -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | class ProxyError(Exception): 2 | pass 3 | 4 | 5 | class RunningStatus: 6 | """状态转移图: PREPARE ----> RUNNING (<==> PENDING) ----> STOPPED """ 7 | PREPARE = 0 8 | RUNNING = 1 9 | PENDING = 3 10 | STOPPED = 5 11 | -------------------------------------------------------------------------------- /flask_app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | class Config: 5 | """ Application Configurations """ 6 | SECRET_KEY = os.environ.get(u'SECRET_KEY') or os.urandom(24) 7 | SSL_DISABLE = True 8 | 9 | MAIL_SERVER = os.environ.get(u'MAIL_SERVER') 10 | MAIL_PORT = int(os.environ.get(u'MAIL_PORT') or u'25') 11 | MAIL_USERNAME = os.environ.get(u'MAIL_USERNAME') 12 | MAIL_PASSWORD = os.environ.get(u'MAIL_PASSWORD') 13 | MAIL_SUBJECT_PREFIX = u'[PySite]' 14 | MAIL_SENDER = u'PySite Admin <%s>' % os.environ.get(u'MAIL_USERNAME') 15 | MAIL_NOTIFICATION = u'MAIL NOTIFICATION BOYD HERE....' 16 | 17 | @staticmethod 18 | def init_app(app): 19 | pass 20 | -------------------------------------------------------------------------------- /flask_app/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block title %}Login{% endblock title %} 5 | 6 | 7 | {% block main_content %} 8 | 9 |

登录

10 |
11 | 12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 |
29 |
30 | {% endblock main_content %} 31 | -------------------------------------------------------------------------------- /flask_app/server_views.py: -------------------------------------------------------------------------------- 1 | from . import server_blueprint as server 2 | from flask import render_template, current_app, request 3 | from flask_login import login_required 4 | from utils import stringify_bytes_val 5 | 6 | 7 | server.add_app_template_global(len, 'len') 8 | server.add_app_template_global(stringify_bytes_val, 'stringify_bytes_val') 9 | 10 | 11 | @server.route('/') 12 | @login_required 13 | def index(): 14 | server_info = current_app.proxy_server_info() 15 | return render_template('index.html', server_info=server_info) 16 | 17 | 18 | @server.route('/clients') 19 | @login_required 20 | def clients(): 21 | clients_info = current_app.proxy_clients_info() 22 | return render_template('clients.html', clients=clients_info) 23 | 24 | 25 | @server.route('/mappings') 26 | @login_required 27 | def mappings(): 28 | cid = request.args.get('cid', None, type=str) 29 | clients_info = current_app.proxy_clients_info(cid) 30 | pos_index = [] 31 | for cid in clients_info: 32 | for port in clients_info[cid]['tcp_maps']: 33 | pos_index.append((port, cid, 'tcp')) 34 | for port in clients_info[cid]['udp_maps']: 35 | pos_index.append((port, cid, 'udp')) 36 | pos_index.sort(key=lambda m: m[0], reverse=False) 37 | return render_template('mappings.html', clients=clients_info, index=pos_index) 38 | -------------------------------------------------------------------------------- /flask_app/static/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * fix: bootstrap.css 3 | */ 4 | .input-group-addon:not(:first-child):not(:last-child), 5 | .input-group-btn:not(:first-child):not(:last-child) { 6 | border-left: 0; 7 | border-right: 0; 8 | } 9 | 10 | 11 | /** 12 | * General configurations 13 | */ 14 | html { 15 | font-size: 62.5%; 16 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 17 | height: 100%; 18 | } 19 | body { 20 | font-family: "Segoe UI", "Lucida Grande", Helvetica, Arial, "Microsoft YaHei", FreeSans, Arimo, 21 | "Droid Sans", "wenquanyi micro hei", "Hiragino Sans GB", "Hiragino Sans GB W3", Arial, sans-serif; 22 | font-weight: normal; 23 | font-size: 1.6rem; 24 | line-height: 1.6; 25 | color: #333; 26 | background-color: #fff; 27 | /* 设置 body 使得 content 在任何的高度下都可以撑满整个屏幕 */ 28 | display: flex; 29 | flex-direction: column; 30 | height: 100%; 31 | } 32 | .docs-header { 33 | /* 我们希望 header 采用固定的高度,只占用必须的空间 */ 34 | /* 0 flex-grow, 0 flex-shrink, auto flex-basis */ 35 | flex: 0 0 auto; 36 | } 37 | .docs-content { 38 | margin-top: -20px; 39 | margin-bottom: -60px; 40 | padding-top: 60px; 41 | padding-bottom: 90px; 42 | /* 将 flex-grow 设置为1,该元素会占用所有的可使用空间,而其他元素该属性值为0,因此不会得到多余的空间 */ 43 | /* 1 flex-grow, 0 flex-shrink, auto flex-basis */ 44 | flex: 1 0 auto; 45 | } 46 | .docs-footer { 47 | margin-top: 60px; 48 | padding: 30px 0; 49 | color: #99979c; 50 | text-align: center; 51 | background-color: #2a2730; 52 | /* 和 header 一样,footer 也采用固定高度*/ 53 | /* 0 flex-grow, 0 flex-shrink, auto flex-basis */ 54 | flex: 0 0 auto; 55 | } 56 | 57 | .docs-footer a { 58 | color: #fff; 59 | } 60 | .docs-footer .footer-links { 61 | padding-left: 0; 62 | margin-bottom: 20px; 63 | } 64 | .docs-footer .footer-links li { 65 | display: inline-block; 66 | } 67 | .docs-footer .footer-links li + li { 68 | margin-left: 15px; 69 | } 70 | @media (min-width: 768px) { 71 | .docs-footer { 72 | text-align: left; 73 | } 74 | .docs-footer p { 75 | margin-bottom: 0; 76 | } 77 | } 78 | 79 | 80 | .op-widget-box > * { 81 | margin-left: 18px; 82 | cursor: pointer; 83 | } 84 | .op-widget-box > *:first-child { 85 | margin-left: 0; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import socket 3 | 4 | 5 | def check_listening(host, port): 6 | try: 7 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 8 | sock.connect((host, port)) 9 | except socket.error: 10 | return False 11 | else: 12 | sock.close() 13 | return True 14 | 15 | 16 | def tcp_connect(host, port, blocking=True): 17 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | sock.connect((host, port)) 19 | sock.setblocking(blocking) 20 | return sock 21 | 22 | 23 | def tcp_listen(host, port, blocking=True, max_conn=1000): 24 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 26 | sock.bind((host, port)) 27 | sock.listen(max_conn) 28 | sock.setblocking(blocking) 29 | return sock 30 | 31 | 32 | def check_udp_port_available(port): 33 | try: 34 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 35 | sock.bind(('127.0.0.1', port)) 36 | except socket.error: 37 | return False 38 | else: 39 | sock.close() 40 | return True 41 | 42 | 43 | def check_python_version(main, sub): 44 | if sys.version_info < (main, sub): 45 | raise RuntimeError(f'"Python {main}.{sub}" or higher version is required.') 46 | return True 47 | 48 | 49 | def stringify_bytes_val(val): 50 | unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] 51 | level = 0 52 | positive = True 53 | if val < 0: 54 | val = - val 55 | positive = False 56 | while level < len(unit) - 1: 57 | if val < 1000: 58 | break 59 | else: 60 | level += 1 61 | val /= 1000 62 | return f'{"" if positive else "-"}{str(val)[:5]} {unit[level]}' 63 | 64 | 65 | def stringify_speed_val(val): 66 | return stringify_bytes_val(val) + '/s' 67 | 68 | 69 | def validate_id_string(id_str, length): 70 | try: 71 | if not id_str or len(id_str) != length: 72 | raise ValueError 73 | int(id_str, 16) # check if `id_str` is a hex format string or not. 74 | return True 75 | except ValueError: 76 | return False 77 | 78 | 79 | def validate_ip_address(ip_str): 80 | try: 81 | values = ip_str.split('.') 82 | if len(values) != 4: 83 | raise ValueError 84 | for v in values: 85 | int(v) 86 | return True 87 | except ValueError: 88 | return False 89 | -------------------------------------------------------------------------------- /flask_app/static/js/html5shiv.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @preserve HTML5 Shiv 3.7.2 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed 3 | */ 4 | !function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.2",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b)}(this,document); -------------------------------------------------------------------------------- /flask_app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, Flask, request, render_template, redirect, url_for, flash 2 | from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user 3 | from flask_moment import Moment 4 | from flask_mail import Mail 5 | from flask_bootstrap import Bootstrap 6 | from flask_app.config import Config 7 | 8 | 9 | server_blueprint = Blueprint('server', __name__, url_prefix='/manage') 10 | api_blueprint = Blueprint('api', __name__, url_prefix='/manage/api') 11 | 12 | from . import server_views, api 13 | 14 | 15 | bootstrap = Bootstrap() 16 | mail = Mail() 17 | moment = Moment() 18 | login_manager = LoginManager() 19 | login_manager.session_protection = 'strong' 20 | login_manager.login_view = 'login' 21 | 22 | 23 | class User(UserMixin): 24 | pass 25 | 26 | 27 | users = { 28 | 'root': {'username': 'root', 'password': 'root'}, 29 | 'admin': {'username': 'admin', 'password': 'admin'}, 30 | } 31 | 32 | 33 | def create_app(): 34 | app = Flask(__name__) 35 | 36 | app.config.from_object(Config) 37 | Config.init_app(app) 38 | 39 | bootstrap.init_app(app) 40 | mail.init_app(app) 41 | login_manager.init_app(app) 42 | moment.init_app(app) 43 | 44 | @login_manager.user_loader 45 | def user_loader(user_id): 46 | if user_id in users: 47 | current_user = User() 48 | current_user.id = user_id 49 | current_user.username = users[user_id]['username'] 50 | return current_user 51 | else: 52 | return None 53 | 54 | @app.route('/login', methods=['GET', 'POST']) 55 | def login(): 56 | url_next = request.args.get('next', url_for('index'), str) 57 | if request.method == 'POST': 58 | username = request.form.get('username') 59 | password = request.form.get('password') 60 | for key in users: 61 | if users[key]['username'] == username and users[key]['password'] == password: 62 | current_user = User() 63 | current_user.id = key 64 | current_user.username = username 65 | login_user(current_user) 66 | return redirect(url_next) 67 | flash('用户名或密码错误!') 68 | return render_template('login.html', url_next=url_next) 69 | 70 | @app.route('/logout') 71 | @login_required 72 | def logout(): 73 | logout_user() 74 | return redirect(url_for('login')) 75 | 76 | @app.route('/') 77 | def index(): 78 | return redirect(url_for('server.index')) 79 | 80 | app.register_blueprint(server_blueprint) 81 | app.register_blueprint(api_blueprint) 82 | 83 | return app 84 | -------------------------------------------------------------------------------- /flask_app/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import datetime 3 | from . import api_blueprint as api 4 | from flask import current_app, request 5 | from flask_login import login_required 6 | 7 | 8 | class DateEncoder(json.JSONEncoder): 9 | def default(self, obj): 10 | if isinstance(obj, datetime.datetime): 11 | return obj.strftime('%Y-%m-%d %H:%M:%S') 12 | elif isinstance(obj, datetime.date): 13 | return obj.strftime("%Y-%m-%d") 14 | else: 15 | return json.JSONEncoder.default(self, obj) 16 | 17 | 18 | @api.route('/get-clients-info') 19 | def get_clients_info(): 20 | return json.dumps(current_app.proxy_clients_info(), cls=DateEncoder) 21 | 22 | 23 | @api.route('/get-server-info') 24 | def get_server_info(): 25 | return json.dumps(current_app.proxy_server_info(), cls=DateEncoder) 26 | 27 | 28 | @api.route('/start-server') 29 | @login_required 30 | def start_server(): 31 | current_app.proxy_execute(current_app.proxy_api.STARTUP_SERVER) 32 | return str(True) 33 | 34 | 35 | @api.route('/stop-server') 36 | @login_required 37 | def stop_server(): 38 | current_app.proxy_execute(current_app.proxy_api.SHUTDOWN_SERVER) 39 | return str(True) 40 | 41 | 42 | @api.route('/restart-server') 43 | @login_required 44 | def restart_server(): 45 | if current_app.proxy_is_running(): 46 | current_app.proxy_execute(current_app.proxy_api.SHUTDOWN_SERVER) 47 | current_app.proxy_execute(current_app.proxy_api.STARTUP_SERVER) 48 | return str(True) 49 | 50 | 51 | @api.route('/pause-client/') 52 | @login_required 53 | def pause_client(client_id): 54 | current_app.proxy_execute(current_app.proxy_api.PAUSE_CLIENT, client_id) 55 | return str(True) 56 | 57 | 58 | @api.route('/resume-client/') 59 | @login_required 60 | def resume_client(client_id): 61 | current_app.proxy_execute(current_app.proxy_api.RESUME_CLIENT, client_id) 62 | return str(True) 63 | 64 | 65 | @api.route('/remove-client/') 66 | @login_required 67 | def remove_client(client_id): 68 | current_app.proxy_execute(current_app.proxy_api.REMOVE_CLIENT, client_id) 69 | return str(True) 70 | 71 | 72 | @api.route('/pause-tcp/') 73 | @login_required 74 | def pause_tcp_map(server_port): 75 | current_app.proxy_execute(current_app.proxy_api.PAUSE_TCP_MAP, server_port) 76 | return str(True) 77 | 78 | 79 | @api.route('/resume-tcp/') 80 | @login_required 81 | def resume_tcp_map(server_port): 82 | current_app.proxy_execute(current_app.proxy_api.RESUME_TCP_MAP, server_port) 83 | return str(True) 84 | 85 | 86 | @api.route('/remove-tcp/') 87 | @login_required 88 | def remove_tcp_map(server_port): 89 | current_app.proxy_execute(current_app.proxy_api.REMOVE_TCP_MAP, server_port) 90 | return str(True) 91 | 92 | 93 | @api.route('/pause-udp/') 94 | @login_required 95 | def pause_udp_map(server_port): 96 | current_app.proxy_execute(current_app.proxy_api.PAUSE_UDP_MAP, server_port) 97 | return str(True) 98 | 99 | 100 | @api.route('/resume-udp/') 101 | @login_required 102 | def resume_udp_map(server_port): 103 | current_app.proxy_execute(current_app.proxy_api.RESUME_UDP_MAP, server_port) 104 | return str(True) 105 | 106 | 107 | @api.route('/remove-udp/') 108 | @login_required 109 | def remove_udp_map(server_port): 110 | current_app.proxy_execute(current_app.proxy_api.REMOVE_UDP_MAP, server_port) 111 | return str(True) 112 | 113 | 114 | @api.route('/reset-statistic') 115 | @login_required 116 | def reset_statistic(): 117 | current_app.proxy_reset_statistic() 118 | return str(True) 119 | -------------------------------------------------------------------------------- /flask_app/templates/clients.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block title %}客户端管理{% endblock title %} 5 | 6 | 7 | {% block main_content %} 8 | 9 |

共计{{ len(clients) }}个客户端:

10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for c in clients %} 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 43 | 44 | 52 | 53 | {% endfor %} 54 | 55 |
创建时间客户端ID名称公网地址协议流量TCP映射数TCP流量UDP映射数UDP流量相关操作
{{ moment(clients[c]['start_time']).format('YYYY-MM-DD HH:mm:ss') }}{{ clients[c]['client_id'] }}{{ clients[c]['client_name'] }}{{ clients[c]['tcp_e_addr'][0] }}{{ stringify_bytes_val(clients[c]['total_statistic'][0] + clients[c]['total_statistic'][1] - clients[c]['tcp_statistic'][0] - clients[c]['tcp_statistic'][1] - clients[c]['udp_statistic'][0] - clients[c]['udp_statistic'][1]) }} 37 | {{ len(clients[c]['tcp_maps']) }} 38 | {{ stringify_bytes_val(clients[c]['tcp_statistic'][0] + clients[c]['tcp_statistic'][1]) }} 41 | {{ len(clients[c]['udp_maps']) }} 42 | {{ stringify_bytes_val(clients[c]['udp_statistic'][0] + clients[c]['udp_statistic'][1]) }} 45 | {% if clients[c]['is_alive'] %} 46 | 47 | {% else %} 48 | 49 | {% endif %} 50 | 51 |
56 |
57 | {% endblock main_content %} 58 | 59 | 60 | {% block scripts %} 61 | {{ super() }} 62 | 83 | {% endblock scripts %} -------------------------------------------------------------------------------- /flask_app/static/js/respond.min.js: -------------------------------------------------------------------------------- 1 | /*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl 2 | * Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT 3 | * */ 4 | 5 | !function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b关闭自动刷新 9 |

共计{{ len(index) }}个映射端口:

10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% for idx in index %} 27 | {% if idx[2] == 'tcp' %} 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | {% elif idx[2] == 'udp' %} 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | {% endif %} 56 | {% endfor %} 57 | 58 |
创建时间客户端端口公网地址服务端端口上传流量下载流量流量统计相关操作
{{ moment(clients[idx[1]]['tcp_maps'][idx[0]]['create_time']).format('YYYY-MM-DD HH:mm:ss') }}{{ clients[idx[1]]['client_name'] }} ({{ clients[idx[1]]['tcp_maps'][idx[0]]['client_port'] }}){{ clients[idx[1]]['tcp_e_addr'][0] + ':' + clients[idx[1]]['tcp_e_addr'][1] }}TCP: {{ idx[0] }}{{ stringify_bytes_val(clients[idx[1]]['tcp_maps'][idx[0]]['statistic'][0]) }}{{ stringify_bytes_val(clients[idx[1]]['tcp_maps'][idx[0]]['statistic'][1]) }}{{ stringify_bytes_val(clients[idx[1]]['tcp_maps'][idx[0]]['statistic'][0] + clients[idx[1]]['tcp_maps'][idx[0]]['statistic'][1]) }} 37 | 38 | 39 |
{{ moment(clients[idx[1]]['udp_maps'][idx[0]]['create_time']).format('YYYY-MM-DD HH:mm:ss') }}{{ clients[idx[1]]['client_name'] }} ({{ clients[idx[1]]['udp_maps'][idx[0]]['client_port'] }}){{ clients[idx[1]]['udp_e_addr'][0] + ':' + clients[idx[1]]['udp_e_addr'][1] }}UDP: {{ idx[0] }}{{ stringify_bytes_val(clients[idx[1]]['udp_maps'][idx[0]]['statistic'][0]) }}{{ stringify_bytes_val(clients[idx[1]]['udp_maps'][idx[0]]['statistic'][1]) }}{{ stringify_bytes_val(clients[idx[1]]['udp_maps'][idx[0]]['statistic'][0] + clients[idx[1]]['udp_maps'][idx[0]]['statistic'][1]) }} 51 | 52 | 53 |
59 |
60 | {% endblock main_content %} 61 | 62 | 63 | {% block scripts %} 64 | {{ super() }} 65 | 86 | {% endblock scripts %} -------------------------------------------------------------------------------- /flask_app/static/js/common.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 返回顶部控件: 调用 scroll_to_top.init(); 3 | * @type: 4 | */ 5 | var scroll_to_top = { 6 | setting:{ 7 | startline:100, //起始行 8 | scrollto:0, //滚动到指定位置 9 | scrollduration:400, //滚动过渡时间 10 | fadeduration:[500,100] //淡出淡现消失 11 | }, 12 | controlHTML: '', //返回顶部按钮 13 | controlattrs:{offsetx:30,offsety:80},//返回按钮固定位置 14 | anchorkeyword:"#top", 15 | state:{ 16 | isvisible:false, 17 | shouldvisible:false 18 | },scrollup:function(){ 19 | if(!this.cssfixedsupport){ 20 | this.$control.css({opacity:0}); 21 | } 22 | var dest=isNaN(this.setting.scrollto)?this.setting.scrollto:parseInt(this.setting.scrollto); 23 | if(typeof dest=="string"&&jQuery("#"+dest).length==1){ 24 | dest=jQuery("#"+dest).offset().top; 25 | }else{ 26 | dest=0; 27 | } 28 | this.$body.animate({scrollTop:dest},this.setting.scrollduration); 29 | },keepfixed:function(){ 30 | var $window=jQuery(window); 31 | var controlx=$window.scrollLeft()+$window.width()-this.$control.width()-this.controlattrs.offsetx; 32 | var controly=$window.scrollTop()+$window.height()-this.$control.height()-this.controlattrs.offsety; 33 | this.$control.css({left:controlx+"px",top:controly+"px"}); 34 | },togglecontrol:function(){ 35 | var scrolltop=jQuery(window).scrollTop(); 36 | if(!this.cssfixedsupport){ 37 | this.keepfixed(); 38 | } 39 | this.state.shouldvisible=(scrolltop>=this.setting.startline)?true:false; 40 | if(this.state.shouldvisible&&!this.state.isvisible){ 41 | this.$control.stop().animate({opacity:1},this.setting.fadeduration[0]); 42 | this.state.isvisible=true; 43 | }else{ 44 | if(this.state.shouldvisible==false&&this.state.isvisible){ 45 | this.$control.stop().animate({opacity:0},this.setting.fadeduration[1]); 46 | this.state.isvisible=false; 47 | } 48 | } 49 | },init:function(){ 50 | jQuery(document).ready(function($){ 51 | var mainobj=scroll_to_top; 52 | var iebrws=document.all; 53 | mainobj.cssfixedsupport=!iebrws||iebrws&&document.compatMode=="CSS1Compat"&&window.XMLHttpRequest; 54 | mainobj.$body=(window.opera)?(document.compatMode=="CSS1Compat"?$("html"):$("body")):$("html,body"); 55 | mainobj.$control=$('
'+mainobj.controlHTML+"
").css({position:mainobj.cssfixedsupport?"fixed":"absolute",bottom:mainobj.controlattrs.offsety,right:mainobj.controlattrs.offsetx,opacity:0,cursor:"pointer"}).attr({title:"返回顶部"}).click(function(){mainobj.scrollup();return false;}).appendTo("body");if(document.all&&!window.XMLHttpRequest&&mainobj.$control.text()!=""){mainobj.$control.css({width:mainobj.$control.width()});}mainobj.togglecontrol(); 56 | $('a[href="'+mainobj.anchorkeyword+'"]').click(function(){mainobj.scrollup();return false;}); 57 | $(window).bind("scroll resize",function(e){mainobj.togglecontrol();}); 58 | }); 59 | } 60 | }; 61 | scroll_to_top.init(); 62 | 63 | 64 | /** 65 | * 跳转到对应的URL地址,在跳转前给予提示信息警告。 66 | * @param url 67 | * @param msg 68 | */ 69 | function head_url(url, msg) { 70 | 71 | if (confirm(msg)) { 72 | console.log(url); 73 | window.location.href = url; 74 | } 75 | } 76 | 77 | 78 | /** 79 | * flask-moment组件代码 80 | */ 81 | function flask_moment_render(elem) { 82 | $(elem).text(eval('moment("' + $(elem).data('timestamp') + '").' + $(elem).data('format') + ';')); 83 | $(elem).removeClass('flask-moment'); 84 | } 85 | function flask_moment_render_all() { 86 | $('.flask-moment').each(function() { 87 | flask_moment_render(this); 88 | if ($(this).data('refresh')) { 89 | (function(elem, interval) { setInterval(function() { flask_moment_render(elem) }, interval); })(this, $(this).data('refresh')); 90 | } 91 | }) 92 | } 93 | 94 | 95 | /** 96 | * 设置与呼出notification模态对话框 97 | */ 98 | function notification_init(header, body, footer) { 99 | $("#myModalTitle").html(header != null ? header : '提示:'); 100 | $("#myModalBody").html(body != null ? body : '请在此处添加提示信息......'); 101 | $("#myModalFooter").html(footer != null ? footer : ''); 102 | } 103 | function notification_show() { 104 | // 105 | // $('#myModal').modal(options); 106 | // or 107 | // 108 | // 109 | $("#myModal").modal(); 110 | } 111 | 112 | 113 | /** 114 | * AJAX方式调用服务接口成功后,典型的回调函数 115 | */ 116 | function callback_with_display(param) { 117 | notification_init(null, param.detail, null); 118 | notification_show(); 119 | } 120 | function callback_with_reload(param) { 121 | window.location.reload(); 122 | } 123 | function callback_with_reload_or_display(param) { 124 | if (param.status == "success") { 125 | window.location.reload(); 126 | } else { 127 | notification_init(null, param.detail, null); 128 | notification_show(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /flask_app/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | 4 | {% block title %}端口代理服务管理中心{% endblock title %} 5 | 6 | 7 | {% block main_content %} 8 |
9 | 10 |

{{ server_info['server_name'] }}【SID:{{ server_info['server_id'] }}】

11 |
12 | 13 |
14 |
15 |
Server Port
tcp({{ server_info['server_port'] }}), udp({{ server_info['server_port'] }})
16 |
Status
{% if server_info['is_running'] %}
running
{% else %}
stopped
{% endif %} 17 |
Num of Clients
{{ server_info['alive_clients'] }} alive of {{ server_info['total_clients'] }} client(s)
18 |
Up Stream
{{ stringify_bytes_val(server_info['total_statistic'][0]) }}
19 |
Down Stream
{{ stringify_bytes_val(server_info['total_statistic'][1]) }}
20 |
Total Stream
{{ stringify_bytes_val(server_info['total_statistic'][0] + server_info['total_statistic'][1]) }}
21 |
Server Created Time
{{ moment(server_info["start_time"]).format('YYYY-MM-DD HH:mm:ss') }}
22 |
23 |
24 |
25 | {% if server_info["is_running"] %} 26 |

27 |

28 |

29 | {% else %} 30 |

31 | {% endif %} 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | {% endblock main_content %} 40 | 41 | 42 | {% block scripts %} 43 | {{ super() }} 44 | 45 | 197 | {% endblock scripts %} -------------------------------------------------------------------------------- /flask_app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Base - ProxyServer{% endblock title %} 9 | 10 | 11 | {% block styles %} 12 | 13 | 14 | {% endblock styles %} 15 | 19 | 20 | 21 |
22 | 48 |
49 | 50 |
51 |
{% block main_content %}{% endblock main_content %}
52 |
53 | 54 |
55 |
56 | 60 |

Designed and built with all the love in the world by @revoll. Maintained by the the author and contributors.

61 |

©Copyright by Kui Wang. All rights reserved.

62 |
63 |
64 | 65 | 77 | 78 | {% block scripts %} 79 | 80 | 81 | 82 | 83 | 237 | {% endblock scripts %} 238 | 239 | -------------------------------------------------------------------------------- /protocol.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import struct 3 | from datetime import datetime 4 | from uuid import uuid4 5 | from xml.etree import ElementTree 6 | from loguru import logger as log 7 | 8 | 9 | class ProtocolError(Exception): 10 | pass 11 | 12 | 13 | class Protocol: 14 | """ 15 | Data Transfer Protocol for each command: 16 | ---------------------------------------- 17 | | 18 | | [:: Common Commands ::] 19 | | 20 | |-- PING: Request('timestamp') >>> Response('timestamp') 21 | | 22 | | 23 | | [:: Server Side Commands ::] 24 | | 25 | |-- ADD_TCP_CONNECTION: Request('client_port') >>> Response('conn_uuid'/None/'Invalid') 26 | | 27 | | 28 | | [:: Client Side Commands ::] 29 | | 30 | |-- CHECK_TCP_PORT: Request('server_port') >>> Response(True/False) 31 | | 32 | |-- CHECK_UDP_PORT: Request('server_port') >>> Response(True/False) 33 | | 34 | |-- ADD_TCP_MAP: Request('client_port:server_port') >>> Response('Success'/'Error'/'Invalid') 35 | | 36 | |-- PAUSE_TCP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 37 | | 38 | |-- RESUME_TCP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 39 | | 40 | |-- REMOVE_TCP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 41 | | 42 | |-- ADD_UDP_MAP: Request('client_port:server_port') >>> Response('Success'/'Error'/'Invalid') 43 | | 44 | |-- PAUSE_UDP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 45 | | 46 | |-- RESUME_UDP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 47 | | 48 | |-- REMOVE_UDP_MAP: Request('server_port') >>> Response('Success'/'Error'/'Invalid') 49 | | 50 | |-- PAUSE_PROXY: Request() >>> Response('Success'/'Error'/'Invalid') 51 | | 52 | |-- RESUME_PROXY: Request() >>> Response('Success'/'Error'/'Invalid') 53 | | 54 | `-- DISCONNECT: Request() >>> Response('Success'/'Error'/'Invalid') 55 | """ 56 | DEFAULT_SID_LENGTH = 8 # 服务器SID长度 57 | CID_LENGTH = 8 # 客户端CID长度 58 | PARAM_SEPARATOR = '**' # 数据项之间的分隔符 59 | SOCKET_BUFFER_SIZE = 65536 # 1400 # TODO:套接字socket缓存大小。目前的UDP封包设计可能导致性能下降。 60 | TASK_SCHEDULE_PERIOD = 0.001 # 任务挂起等待的时间粒度 61 | UDP_REQUEST_DURATION = 9000 # UDP请求的记录保留时效(2.5h) 62 | CLIENT_TCP_PING_PERIOD = 30 # 客户端TCP保活探测时间 63 | CLIENT_UDP_PING_PERIOD = 5 # 客户端UDP保活时间 64 | UDP_UNREACHABLE_WARNING_PERIOD = 16 # 打印无法接收UDP PING包日志的超时时间 65 | CONNECTION_TIMEOUT = 30 # TCP连接超时时间 66 | MAX_RETRY_PERIOD = 60 # 客户端在断开重连的情况下,最大重连时间间隔 67 | 68 | class ConnectionType: 69 | """Connection type.""" 70 | MANAGER = 'Manager' 71 | PROXY_CLIENT = 'ProxyClient' 72 | PROXY_TCP_DATA = 'ProxyTcpData' 73 | 74 | class Command: 75 | """Common command used by both server and client.""" 76 | PING = 'Ping' 77 | 78 | class ServerCommand: 79 | """Used by server to control client.""" 80 | ADD_TCP_CONNECTION = 'AddTcpConnection' 81 | 82 | class ClientCommand: 83 | """Used by client to communicate with server.""" 84 | CHECK_TCP_PORT = 'CheckTcpPort' # 检查TCP服务是否开启 85 | CHECK_UDP_PORT = 'CheckUdpPort' # 检查UDP端口是否可用 86 | ADD_TCP_MAP = 'AddTcpMap' 87 | PAUSE_TCP_MAP = 'PauseTcpMap' 88 | RESUME_TCP_MAP = 'ResumeTcpMap' 89 | REMOVE_TCP_MAP = 'RemoveTcpMap' 90 | ADD_UDP_MAP = 'AddUdpMap' 91 | PAUSE_UDP_MAP = 'PauseUdpMap' 92 | RESUME_UDP_MAP = 'ResumeUdpMap' 93 | REMOVE_UDP_MAP = 'RemoveUdpMap' 94 | PAUSE_PROXY = 'PauseProxy' 95 | RESUME_PROXY = 'ResumeProxy' 96 | DISCONNECT = 'Disconnect' 97 | 98 | class Result: 99 | """Standard function call result.""" 100 | ERROR = 'Error' 101 | SUCCESS = 'Success' 102 | INVALID = 'Invalid' 103 | UNKNOWN = 'Unknown' 104 | 105 | def __init__(self, tcp_stream=None): 106 | self.__reader = tcp_stream[0] 107 | self.__writer = tcp_stream[1] 108 | 109 | # request queue format: [(uuid, cmd, data), ...] 110 | self.__request_queue = asyncio.Queue() 111 | # response pool format: {uuid: (cmd, data), ...} 112 | self.__response_pool = {} 113 | 114 | self.__status = False 115 | self.__task_recv_msg = asyncio.create_task(self.__recv_msg_task()) 116 | 117 | self.tcp_statistic = [0, 0] # TCP上下行流量 118 | self.udp_statistic = [0, 0] # UDP上下行流量 119 | 120 | @staticmethod 121 | def make_req(uuid, key, val): 122 | return f'{val if val else ""}' 123 | 124 | @staticmethod 125 | def make_resp(uuid, key, val): 126 | return f'{val if val else ""}' 127 | 128 | @staticmethod 129 | def parse_msg(msg): 130 | root = ElementTree.fromstring(msg) 131 | return root.tag, root.attrib['uuid'], root.attrib['type'], root.text 132 | 133 | async def __recv_msg_task(self): 134 | self.__status = True 135 | try: 136 | while True: 137 | msg = await self.__reader.readline() 138 | 139 | if msg: 140 | self.tcp_statistic[1] += len(msg) 141 | else: 142 | remote = self.__writer.get_extra_info('peername') 143 | raise ConnectionError(f'Protocol connection with {remote} is closed or broken.') 144 | 145 | try: 146 | msg = msg.decode().strip() 147 | log.debug(msg) 148 | msg_type, uuid, key, val = Protocol.parse_msg(msg) 149 | except Exception as e: 150 | log.warning(f'protocol error while parsing {msg}: {e}') 151 | continue 152 | 153 | if msg_type == 'request': 154 | await self.__request_queue.put((uuid, key, val)) 155 | elif msg_type == 'response': 156 | self.__response_pool[uuid] = (key, val) 157 | else: 158 | log.warning(f'can not recognise message type of: {msg}') 159 | except Exception as e: 160 | log.error(e) 161 | self.__writer.close() 162 | self.__status = False 163 | 164 | def close(self): 165 | self.__task_recv_msg.cancel() 166 | self.__writer.close() 167 | self.__status = False 168 | 169 | async def wait_closed(self): 170 | # TODO: WARNING: 此函数直接写成 `await self.__task_recv_msg` 会导致调用异常,目前不知道原因。 171 | # try: 172 | # await self.__task_recv_msg 173 | # except asyncio.CancelledError: 174 | # self.__writer.close() 175 | # self.__status = False 176 | while self.__status: 177 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 178 | 179 | def is_healthy(self): 180 | return self.__status 181 | 182 | async def send_request(self, uuid, cmd, data=None): 183 | """ 184 | send request 185 | :param uuid: 186 | :param cmd: 187 | :param data: 188 | :return: 189 | @throws: ConnectionError(IOError) 190 | """ 191 | try: 192 | req = Protocol.make_req(uuid, cmd, data) 193 | b_req = (req + '\n').encode() 194 | self.__writer.write(b_req) 195 | await self.__writer.drain() 196 | self.tcp_statistic[0] += len(b_req) 197 | log.debug(req) 198 | except Exception: 199 | self.close() 200 | raise 201 | 202 | async def send_response(self, uuid, cmd, data=None): 203 | """ 204 | send response 205 | :param uuid: 206 | :param cmd: 207 | :param data: 208 | :return: 209 | @throws: ConnectionError(IOError) 210 | """ 211 | try: 212 | resp = Protocol.make_resp(uuid, cmd, data) 213 | b_resp = (resp + '\n').encode() 214 | self.__writer.write(b_resp) 215 | await self.__writer.drain() 216 | self.tcp_statistic[0] += len(b_resp) 217 | # self.__request_queue.task_done() 218 | log.debug(resp) 219 | except Exception: 220 | self.close() 221 | raise 222 | 223 | async def get_request(self, timeout=None): 224 | """ 225 | get request 226 | :param timeout: 227 | :return: 228 | @throws: BlockingIOError, asyncio.TimeoutError 229 | """ 230 | if not self.__request_queue.empty(): 231 | req = self.__request_queue.get_nowait() 232 | self.__request_queue.task_done() 233 | return req 234 | elif timeout == 0: 235 | raise BlockingIOError(f'Protocol failed to get request as no request arrived.') 236 | wait_time = 0 237 | while True: 238 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 239 | wait_time += Protocol.TASK_SCHEDULE_PERIOD 240 | if not self.__request_queue.empty(): 241 | req = self.__request_queue.get_nowait() 242 | self.__request_queue.task_done() 243 | return req 244 | if timeout is None or timeout < 0: 245 | continue 246 | if wait_time >= timeout: 247 | raise asyncio.TimeoutError(f'Protocol get request timeout after waiting {timeout}s.') 248 | 249 | async def get_response(self, uuid, timeout=None): 250 | """ 251 | get response 252 | :param uuid: 253 | :param timeout: 254 | :return: 255 | @throws: BlockingIOError, asyncio.TimeoutError 256 | """ 257 | if uuid in self.__response_pool: 258 | resp = self.__response_pool[uuid] 259 | del self.__response_pool[uuid] 260 | return resp 261 | elif timeout == 0: 262 | raise BlockingIOError(f'Protocol failed to get response with uuid={uuid}.') 263 | wait_time = 0 264 | while True: 265 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 266 | wait_time += Protocol.TASK_SCHEDULE_PERIOD 267 | if uuid in self.__response_pool: 268 | resp = self.__response_pool[uuid] 269 | del self.__response_pool[uuid] 270 | return resp 271 | if timeout is None or timeout < 0: 272 | continue 273 | if wait_time >= timeout: 274 | raise asyncio.TimeoutError(f'Protocol get response timeout after waiting {timeout}s.') 275 | 276 | async def request(self, cmd, data=None, timeout=None): 277 | """ 278 | make request 279 | :param cmd: 280 | :param data: 281 | :param timeout: 282 | :return: 283 | @throws: IOError, asyncio.TimeoutError, ProtocolError 284 | """ 285 | req_uuid = uuid4().hex 286 | await self.send_request(req_uuid, cmd, data) 287 | cmd2, data2 = await self.get_response(req_uuid, timeout) 288 | if cmd2 == cmd: 289 | return data2 290 | else: 291 | req = Protocol.make_req(req_uuid, cmd, data) 292 | resp = Protocol.make_resp(req_uuid, cmd2, data2) 293 | raise ProtocolError(f'Send: {req}; Receive: {resp}.') 294 | 295 | """ 296 | Frame format of UDP Protocol Data Relay. 297 | 298 | Case of UdpPacketType.SYNC: 299 | 0 8 16 24 32 40 48 56 64 300 | +--------+--------+--------+--------+--------+--------+--------+--------+ 301 | | SYNC_FLAG | PACKET_LENGTH | CRC | 302 | +--------+--------+--------+--------+--------+--------+--------+--------+ 303 | | | 304 | | INFO(CLIENT_ID), TIMESTAMP ... | 305 | | | 306 | +--------+--------+--------+--------+--------+--------+--------+--------+ 307 | 308 | Case of UdpPacketType.DATA: 309 | 0 8 16 24 32 40 48 56 64 310 | +--------+--------+--------+--------+--------+--------+--------+--------+ 311 | | DATA_FLAG | PACKET_LENGTH | CRC | 312 | +--------+--------+--------+--------+--------+--------+--------+--------+ 313 | | PROXY_PORT | USER_HOST | USER_PORT | 314 | +--------+--------+--------+--------+--------+--------+--------+--------+ 315 | | | 316 | | USER DATA ... | 317 | | | 318 | +--------+--------+--------+--------+--------+--------+--------+--------+ 319 | """ 320 | UDP_SYNC_HEADER_LEN = 8 321 | UDP_DATA_HEADER_LEN = 16 322 | __UDP_SYNC_FLAG = b'\x53\x59' # 'SY' 323 | __UDP_DATA_FLAG = b'\x44\x41' # 'DA' 324 | 325 | class UdpPacketType: 326 | SYNC = 'SYNC' 327 | DATA = 'DATA' 328 | UNKNOWN = 'UNKNOWN' 329 | 330 | class UdpPacketInfo: 331 | TYPE = 'type' 332 | TIMESTAMP = 'timestamp' 333 | SERVER_PORT = 'server_port' 334 | USER_ADDRESS = 'user_address' 335 | B_USER_DATA = 'b_user_data' 336 | 337 | @staticmethod 338 | def pack_udp_sync_packet(**kwargs): # raise: struct.error 339 | """ 340 | 合成udp sync二进制数据包。 341 | :param kwargs: 用户需要传递的参数 342 | :return: udp sync packet 343 | @throws: ProtocolError, struct.error 344 | """ 345 | kwargs[Protocol.UdpPacketInfo.TIMESTAMP] = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') 346 | val_list = (f'{k}={kwargs[k]}' for k in kwargs) 347 | b_data = Protocol.PARAM_SEPARATOR.join(val_list).encode() 348 | length = Protocol.UDP_SYNC_HEADER_LEN + len(b_data) 349 | b_len = struct.pack('>I', length) # length.to_bytes(length=4, byteorder='big', signed=False) 350 | b_crc = b'\x55\x55' 351 | return b''.join((Protocol.__UDP_SYNC_FLAG, b_len, b_crc, b_data)) 352 | 353 | @staticmethod 354 | def pack_udp_data_packet(server_port, user_address, b_user_data, pack_data=True): 355 | """ 356 | 合成udp data二进制数据包。 357 | :param server_port: 358 | :param user_address: 359 | :param b_user_data: 360 | :param pack_data: 361 | :return: udp data packet 362 | @throws: ProtocolError, struct.error 363 | """ 364 | user_host = user_address[0] if user_address[0] else '127.0.0.1' 365 | user_port = user_address[1] 366 | if len(user_host.split('.')) != 4: 367 | raise ProtocolError(f'Invalid ip address of user_host({user_host}) is given.') 368 | b_proxy_port = struct.pack('>H', server_port) 369 | b_user_host = bytes(map(int, user_host.split('.'))) 370 | b_user_port = struct.pack('>H', user_port) 371 | b_len = struct.pack('>I', Protocol.UDP_DATA_HEADER_LEN + len(b_user_data)) 372 | b_crc = b'\x55\x55' 373 | return b''.join((Protocol.__UDP_DATA_FLAG, b_len, b_crc, b_proxy_port, b_user_host, b_user_port, b_user_data)) \ 374 | if pack_data else b''.join((Protocol.__UDP_DATA_FLAG, b_len, b_crc, b_proxy_port, b_user_host, b_user_port)) 375 | 376 | @staticmethod 377 | def unpack_udp_packet(packet, unpack_data=False): 378 | """ 379 | 将二进制数据包解析成字典参数返回。 380 | :param packet: 381 | :param unpack_data: 382 | :return: dict result 383 | @throws: ProtocolError, struct.error 384 | """ 385 | result = {Protocol.UdpPacketInfo.TYPE: Protocol.UdpPacketType.UNKNOWN} 386 | 387 | len_packet = len(packet) 388 | if len_packet != struct.unpack('>I', packet[2:6])[0]: 389 | raise ProtocolError('Checking LENGTH of udp packet failed.') 390 | if packet[6:8] != b'\x55\x55': 391 | raise ProtocolError('Checking CRC of udp data packet failed.') 392 | 393 | if packet[0:2] == Protocol.__UDP_SYNC_FLAG: 394 | if len_packet < Protocol.UDP_SYNC_HEADER_LEN: 395 | raise ProtocolError('Checking LENGTH of udp sync packet failed.') 396 | val_list = packet[Protocol.UDP_SYNC_HEADER_LEN:].decode().split(Protocol.PARAM_SEPARATOR) 397 | for val in val_list: 398 | val_pair = val.split('=', 1) 399 | if len(val_pair) != 2: 400 | raise ProtocolError('Invalid parameter in udp sync packet.') 401 | result[val_pair[0]] = val_pair[1] 402 | result[Protocol.UdpPacketInfo.TYPE] = Protocol.UdpPacketType.SYNC 403 | elif packet[0:2] == Protocol.__UDP_DATA_FLAG: 404 | if len_packet < Protocol.UDP_DATA_HEADER_LEN: 405 | raise ProtocolError('Checking LENGTH of udp data packet failed.') 406 | result[Protocol.UdpPacketInfo.SERVER_PORT] = struct.unpack('>H', packet[8:10])[0] 407 | result[Protocol.UdpPacketInfo.USER_ADDRESS] = ( 408 | '.'.join(str(x) for x in list(packet[10:14])), struct.unpack('>H', packet[14:16])[0]) 409 | if unpack_data: 410 | result[Protocol.UdpPacketInfo.B_USER_DATA] = packet[Protocol.UDP_DATA_HEADER_LEN:] 411 | result[Protocol.UdpPacketInfo.TYPE] = Protocol.UdpPacketType.DATA 412 | else: 413 | raise ProtocolError('Unrecognised udp packet.') 414 | return result 415 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | import socket 4 | import time 5 | from datetime import datetime 6 | from uuid import uuid4 7 | from protocol import Protocol 8 | from common import ProxyError, RunningStatus 9 | from utils import check_python_version, check_listening 10 | from loguru import logger as log 11 | 12 | 13 | class TcpMapInfo: 14 | SERVER_PORT = 'server_port' 15 | SWITCH = 'is_running' 16 | STATISTIC = 'statistic' 17 | CREATE_TIME = 'create_time' 18 | 19 | 20 | class UdpMapInfo: 21 | SERVER_PORT = 'server_port' 22 | SWITCH = 'is_running' 23 | STATISTIC = 'statistic' 24 | CREATE_TIME = 'create_time' 25 | 26 | 27 | class ProxyClient: 28 | """端口代理转发类(客户端) 29 | """ 30 | def __init__(self, server_host, server_port=10000, port_map=None, cid=None, name='DefaultClient'): 31 | for maps in (port_map['TCP'], port_map['UDP']): 32 | for p in maps: 33 | if type(p) != int or type(maps[p]) != int or list(maps.values()).count(maps[p]) != 1: 34 | raise ValueError('Invalid argument for `port_map`: %s' % port_map) 35 | server_host = socket.gethostbyname(server_host) 36 | server_port = int(server_port) 37 | 38 | self.client_id = cid # generates by server after connection if not provided. 39 | self.client_name = name # user defined client name. 40 | self.server_host = server_host # server side host address. 41 | self.server_port = server_port # for both tcp and udp service. 42 | 43 | self.ini_tcp_map = port_map['TCP'] # = {80: 10080, 81: 10081, ...} 44 | self.ini_udp_map = port_map['UDP'] # = {80: 10080, ...} 45 | 46 | self.tcp_maps = None # tcp proxy map 47 | self.udp_maps = None # udp proxy map 48 | 49 | self.__udp_socket = None # socket for receiving udp packet from server to local udp service. 50 | self.__udp_req_map = None # socket for feedback udp packet from local udp service to server. 51 | 52 | self.__protocol = None # tcp connection used to communicate with server. 53 | self.__task_serve_req = None # task for processing requests form server. 54 | self.__task_udp_receive = None # task for receiving udp packet form server. 55 | self.__task_udp_feedback = None # task for feedback udp packet to server. 56 | self.__task_daemon = None # task for daemon 57 | 58 | self.status = RunningStatus.PREPARE # PREPARE -> RUNNING (-> PENDING) -> STOPPED 59 | self.ping = time.time() # the period when the last udp ping receives. 60 | self.timestamp = datetime.utcnow() # the period when proxy client creates. 61 | 62 | def tcp_statistic(self, upstream=None, downstream=None): 63 | if upstream is None: 64 | self.__protocol.tcp_statistic[0] = 0 65 | else: 66 | self.__protocol.tcp_statistic[0] += upstream 67 | if downstream is None: 68 | self.__protocol.tcp_statistic[1] = 0 69 | else: 70 | self.__protocol.tcp_statistic[1] += downstream 71 | return tuple(self.__protocol.tcp_statistic) 72 | 73 | def udp_statistic(self, upstream=None, downstream=None): 74 | if upstream is None: 75 | self.__protocol.udp_statistic[0] = 0 76 | else: 77 | self.__protocol.udp_statistic[0] += upstream 78 | if downstream is None: 79 | self.__protocol.udp_statistic[1] = 0 80 | else: 81 | self.__protocol.udp_statistic[1] += downstream 82 | return tuple(self.__protocol.udp_statistic) 83 | 84 | def _map_msg(self, client_port, server_port, status, extra=None): 85 | return f'{ sys._getframe(1).f_code.co_name.upper() }: ' \ 86 | f'Local({client_port}) >>> {self.server_host}({server_port}) ... ' \ 87 | f'[{ status.upper() }]{ " (" + extra + ")" if extra else "" }' 88 | 89 | async def add_tcp_map(self, client_port, server_port): 90 | if client_port in self.tcp_maps: 91 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR', f'already registered.')) 92 | status, detail = (await self.__protocol.request( 93 | Protocol.ClientCommand.ADD_TCP_MAP, f'{client_port}{Protocol.PARAM_SEPARATOR}{server_port}', 94 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 95 | if status != Protocol.Result.SUCCESS: 96 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR', detail)) 97 | self.tcp_maps[client_port] = { 98 | TcpMapInfo.SERVER_PORT: server_port, 99 | TcpMapInfo.SWITCH: True, 100 | TcpMapInfo.STATISTIC: [0, 0], 101 | TcpMapInfo.CREATE_TIME: datetime.utcnow(), 102 | } 103 | log.success(self._map_msg(client_port, server_port, 'OK')) 104 | 105 | async def pause_tcp_map(self, client_port): 106 | if client_port not in self.tcp_maps: 107 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 108 | else: 109 | server_port = self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT] 110 | status, detail = (await self.__protocol.request( 111 | Protocol.ClientCommand.PAUSE_TCP_MAP, f'{self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT]}', 112 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 113 | if status != Protocol.Result.SUCCESS: 114 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 115 | self.tcp_maps[client_port][TcpMapInfo.SWITCH] = False 116 | log.success(self._map_msg(client_port, server_port, 'OK')) 117 | 118 | async def resume_tcp_map(self, client_port): 119 | if client_port not in self.tcp_maps: 120 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 121 | else: 122 | server_port = self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT] 123 | status, detail = (await self.__protocol.request( 124 | Protocol.ClientCommand.RESUME_TCP_MAP, f'{self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT]}', 125 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 126 | if status != Protocol.Result.SUCCESS: 127 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 128 | self.tcp_maps[client_port][TcpMapInfo.SWITCH] = True 129 | log.success(self._map_msg(client_port, server_port, 'OK')) 130 | 131 | async def remove_tcp_map(self, client_port): 132 | if client_port not in self.tcp_maps: 133 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 134 | else: 135 | server_port = self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT] 136 | status, detail = (await self.__protocol.request( 137 | Protocol.ClientCommand.REMOVE_TCP_MAP, f'{self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT]}', 138 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 139 | if status != Protocol.Result.SUCCESS: 140 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 141 | del self.tcp_maps[client_port] 142 | log.success(self._map_msg(client_port, server_port, 'OK')) 143 | 144 | async def add_udp_map(self, client_port, server_port): 145 | if client_port in self.udp_maps: 146 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR', f'already registered.')) 147 | status, detail = (await self.__protocol.request( 148 | Protocol.ClientCommand.ADD_UDP_MAP, f'{client_port}{Protocol.PARAM_SEPARATOR}{server_port}', 149 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 150 | if status != Protocol.Result.SUCCESS: 151 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 152 | self.udp_maps[client_port] = { 153 | UdpMapInfo.SERVER_PORT: server_port, 154 | UdpMapInfo.SWITCH: True, 155 | UdpMapInfo.STATISTIC: [0, 0], 156 | UdpMapInfo.CREATE_TIME: datetime.utcnow(), 157 | } 158 | log.success(self._map_msg(client_port, server_port, 'OK')) 159 | 160 | async def pause_udp_map(self, client_port): 161 | if client_port not in self.udp_maps: 162 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 163 | else: 164 | server_port = self.udp_maps[client_port][UdpMapInfo.SERVER_PORT] 165 | status, detail = (await self.__protocol.request( 166 | Protocol.ClientCommand.PAUSE_UDP_MAP, f'{self.udp_maps[client_port][UdpMapInfo.SERVER_PORT]}', 167 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 168 | if status != Protocol.Result.SUCCESS: 169 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 170 | self.udp_maps[client_port][UdpMapInfo.SWITCH] = False 171 | log.success(self._map_msg(client_port, server_port, 'OK')) 172 | 173 | async def resume_udp_map(self, client_port): 174 | if client_port not in self.udp_maps: 175 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 176 | else: 177 | server_port = self.udp_maps[client_port][UdpMapInfo.SERVER_PORT] 178 | status, detail = (await self.__protocol.request( 179 | Protocol.ClientCommand.RESUME_UDP_MAP, f'{self.udp_maps[client_port][UdpMapInfo.SERVER_PORT]}', 180 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 181 | if status != Protocol.Result.SUCCESS: 182 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 183 | self.udp_maps[client_port][UdpMapInfo.SWITCH] = True 184 | log.success(self._map_msg(client_port, server_port, 'OK')) 185 | 186 | async def remove_udp_map(self, client_port): 187 | if client_port not in self.udp_maps: 188 | raise ProxyError(self._map_msg(client_port, None, 'ERROR', f'not registered.')) 189 | else: 190 | server_port = self.udp_maps[client_port][UdpMapInfo.SERVER_PORT] 191 | status, detail = (await self.__protocol.request( 192 | Protocol.ClientCommand.REMOVE_UDP_MAP, f'{self.udp_maps[client_port][UdpMapInfo.SERVER_PORT]}', 193 | Protocol.CONNECTION_TIMEOUT)).split(Protocol.PARAM_SEPARATOR, 1) 194 | if status != Protocol.Result.SUCCESS: 195 | raise ProxyError(self._map_msg(client_port, server_port, 'ERROR'), detail) 196 | del self.udp_maps[client_port] 197 | log.success(self._map_msg(client_port, server_port, 'OK')) 198 | 199 | async def __tcp_data_relay_task(self, client_port, sock_stream, peer_stream): 200 | 201 | async def data_relay_monitor(): 202 | while True: 203 | if self.status == RunningStatus.RUNNING and client_port in self.tcp_maps \ 204 | and self.tcp_maps[client_port][TcpMapInfo.SWITCH]: 205 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 206 | else: 207 | break 208 | 209 | async def simplex_data_relay(reader, writer, upstream=True): 210 | while True: 211 | try: 212 | data = await reader.read(Protocol.SOCKET_BUFFER_SIZE) 213 | if not data: 214 | break 215 | writer.write(data) 216 | await writer.drain() 217 | self.tcp_maps[client_port][TcpMapInfo.STATISTIC][0 if upstream else 1] += len(data) 218 | except IOError: 219 | break 220 | 221 | server_port = self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT] 222 | rp = peer_stream[1].get_extra_info("sockname")[1] 223 | log.info(f'Server({server_port}) ----> {self.client_id}({client_port}) [{rp}]') 224 | _, pending = await asyncio.wait({ 225 | asyncio.create_task(data_relay_monitor()), 226 | asyncio.create_task(simplex_data_relay(sock_stream[0], peer_stream[1], upstream=True)), 227 | asyncio.create_task(simplex_data_relay(peer_stream[0], sock_stream[1], upstream=False)), 228 | }, return_when=asyncio.FIRST_COMPLETED) 229 | for task in pending: 230 | task.cancel() 231 | sock_stream[1].close() 232 | peer_stream[1].close() 233 | log.info(f'Server({server_port}) --x-> {self.client_id}({client_port}) [{rp}]') 234 | 235 | async def __serve_request_task(self): 236 | while True: 237 | uuid, cmd, data = await self.__protocol.get_request(timeout=None) 238 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.SUCCESS, '')) 239 | try: 240 | if cmd == Protocol.Command.PING: 241 | result += datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') 242 | 243 | elif cmd == Protocol.ServerCommand.ADD_TCP_CONNECTION: 244 | client_port = int(data) 245 | server_port = self.tcp_maps[client_port][TcpMapInfo.SERVER_PORT] 246 | reader1, writer1 = await asyncio.open_connection('127.0.0.1', client_port) 247 | reader2, writer2 = await asyncio.open_connection(self.server_host, self.server_port) 248 | conn_uuid = uuid4().hex 249 | identify = Protocol.PARAM_SEPARATOR.join( 250 | (Protocol.ConnectionType.PROXY_TCP_DATA, self.client_id, str(server_port), conn_uuid)) 251 | writer2.write((identify + '\n').encode()) 252 | await writer2.drain() 253 | resp = (await reader2.readline()).decode().strip() 254 | if not resp or resp[:len(Protocol.Result.SUCCESS)] != Protocol.Result.SUCCESS: 255 | raise ProxyError('Add tcp connection failed while connecting to server.') 256 | asyncio.create_task( 257 | self.__tcp_data_relay_task(client_port, (reader1, writer1), (reader2, writer2))) 258 | result += conn_uuid 259 | 260 | else: 261 | log.warning(f'Unrecognised request from Server: {Protocol.make_req(uuid, cmd, data)}') 262 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.INVALID, 'unrecognised request')) 263 | 264 | except Exception as e: 265 | log.error(f'Error while processing request({Protocol.make_req(uuid, cmd, data)}): {e}') 266 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.ERROR, str(e))) 267 | finally: 268 | try: 269 | await self.__protocol.send_response(uuid, cmd, result) 270 | except Exception as e: 271 | log.error(e) 272 | break 273 | 274 | async def __udp_receive_task(self): 275 | 276 | def udp_send_data(__b_data, __udp_port, __user_address): 277 | key = hash((__udp_port, __user_address)) 278 | if key in self.__udp_req_map: 279 | sock = self.__udp_req_map[key][2] 280 | self.__udp_req_map[key][3] = time.time() 281 | else: 282 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 283 | sock.setblocking(False) 284 | self.__udp_req_map[key] = [__udp_port, user_address, sock, time.time()] 285 | sock.sendto(__b_data, ('127.0.0.1', __udp_port)) 286 | 287 | while True: 288 | try: 289 | data, address = self.__udp_socket.recvfrom(Protocol.SOCKET_BUFFER_SIZE) 290 | except BlockingIOError: 291 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 292 | continue 293 | except IOError as e: 294 | log.error(e) 295 | break 296 | 297 | try: 298 | # if address[0] != self.server_host: 299 | # raise ProxyError(f'Received udp packet from {address} which is not the server.') 300 | try: 301 | packet_info = Protocol.unpack_udp_packet(data, unpack_data=False) 302 | except Exception as e: 303 | raise ProxyError(f'Protocol.unpack_udp_packet(): {e}') 304 | if packet_info[Protocol.UdpPacketInfo.TYPE] == Protocol.UdpPacketType.SYNC: 305 | self.udp_statistic(0, len(data)) 306 | self.__udp_ping = time.time() 307 | log.debug(f'UDP_PING: {packet_info[Protocol.UdpPacketInfo.TIMESTAMP]}') 308 | elif packet_info[Protocol.UdpPacketInfo.TYPE] == Protocol.UdpPacketType.DATA: 309 | self.udp_statistic(0, Protocol.UDP_DATA_HEADER_LEN) 310 | self.udp_maps[UdpMapInfo.STATISTIC][1] += len(data) - Protocol.UDP_DATA_HEADER_LEN 311 | user_address = packet_info[Protocol.UdpPacketInfo.USER_ADDRESS] 312 | server_port = packet_info[Protocol.UdpPacketInfo.SERVER_PORT] 313 | client_port = None 314 | for port in self.udp_maps: 315 | if self.udp_maps[port][UdpMapInfo.SERVER_PORT] == server_port: 316 | client_port = port 317 | break 318 | if not client_port: 319 | log.warning(f'Received udp data packet on server port({server_port}) that not registered.') 320 | continue 321 | try: 322 | udp_send_data(data[Protocol.UDP_DATA_HEADER_LEN:], client_port, user_address) 323 | except IOError as e: 324 | log.error(e) 325 | continue 326 | log.debug(f'Received udp data packet from {user_address} on port({client_port})') 327 | else: 328 | self.udp_statistic(0, len(data)) 329 | raise ProxyError(f'Received udp packet from {address} with unknown type.') 330 | except Exception as e: 331 | log.debug(f'Received udp packet from: {address}, data:\n' + data.hex()) 332 | log.warning(e) 333 | self.__protocol.close() 334 | 335 | async def __udp_feedback_task(self): 336 | while True: 337 | for key in list(self.__udp_req_map): 338 | client_port, user_address, sock, _ = self.__udp_req_map[key] 339 | try: 340 | while True: 341 | try: 342 | data = sock.recv(Protocol.SOCKET_BUFFER_SIZE) 343 | except BlockingIOError: 344 | break 345 | data_packet = Protocol.pack_udp_data_packet( 346 | self.udp_maps[client_port][UdpMapInfo.SERVER_PORT], user_address, data, pack_data=True) 347 | sock.sendto(data_packet, (self.server_host, self.server_port)) 348 | self.udp_statistic(Protocol.UDP_DATA_HEADER_LEN, 0) 349 | self.udp_maps[UdpMapInfo.STATISTIC][0] += len(data) 350 | log.debug(f'Sent feedback to server port({self.server_port})') 351 | except IOError as e: 352 | log.error(e) 353 | val = self.__udp_req_map.pop(key) 354 | val[2].close() 355 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 356 | 357 | async def __daemon_task(self): 358 | cnt = 0 359 | await asyncio.sleep(Protocol.CLIENT_UDP_PING_PERIOD) 360 | while True: 361 | # 362 | # Send tcp keep alive message. 363 | # 364 | if not (cnt % Protocol.CLIENT_TCP_PING_PERIOD): 365 | try: 366 | await self.__protocol.request(Protocol.Command.PING, timeout=Protocol.CONNECTION_TIMEOUT) 367 | except Exception as e: 368 | log.error(e) 369 | break 370 | # 371 | # Send udp keep alive data packet. 372 | # 373 | if not (cnt % Protocol.CLIENT_UDP_PING_PERIOD): 374 | data_packet = Protocol.pack_udp_sync_packet(client_id=self.client_id) 375 | try: 376 | self.__udp_socket.sendto(data_packet, (self.server_host, self.server_port)) 377 | except IOError as e: 378 | log.error(e) 379 | # log.debug(f'Sent UDP ping packet to server({self.server_host}:{self.server_port})') 380 | # 381 | # Check if udp keep alive data packet received in time. 382 | # 383 | if time.time() - self.__udp_ping > Protocol.UDP_UNREACHABLE_WARNING_PERIOD: 384 | log.warning('Too many udp ping packet lost!') 385 | self.__udp_ping += Protocol.CLIENT_UDP_PING_PERIOD 386 | # 387 | # Remove virtual udp connections in `self.__udp_req_map` which is too old. 388 | # 389 | for key in list(self.__udp_req_map.keys()): 390 | if time.time() - self.__udp_req_map[key][3] > Protocol.UDP_REQUEST_DURATION: 391 | val = self.__udp_req_map.pop(key) 392 | val[2].close() 393 | # 394 | # Every loop comes with a rest. 395 | # 396 | cnt += 1 397 | await asyncio.sleep(1) 398 | 399 | self.__protocol.close() # kill itself. 400 | 401 | async def __main_task(self): 402 | if self.status == RunningStatus.RUNNING or self.status == RunningStatus.PENDING: 403 | log.error('Abort starting up the Proxy Client as it is already running.') 404 | return 405 | log.info(f'Connecting with the Proxy Server({self.server_host}:{self.server_port}) ...') 406 | reader, writer = await asyncio.open_connection(self.server_host, self.server_port) 407 | identify = Protocol.PARAM_SEPARATOR.join( 408 | (Protocol.ConnectionType.PROXY_CLIENT, self.client_id if self.client_id else "", self.client_name)) 409 | writer.write((identify + '\n').encode()) 410 | await writer.drain() 411 | resp = (await reader.readline()).decode().strip().split(Protocol.PARAM_SEPARATOR) 412 | if resp and resp[0] == Protocol.Result.SUCCESS: 413 | self.client_id = resp[1] 414 | log.success(f'Connected!') 415 | else: 416 | log.error(f'Failed!') 417 | return 418 | 419 | self.tcp_maps = {} 420 | self.udp_maps = {} 421 | self.__udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 422 | self.__udp_socket.setblocking(False) 423 | self.__udp_req_map = {} 424 | self.__udp_ping = time.time() 425 | 426 | self.__protocol = Protocol((reader, writer)) 427 | self.__task_serve_req = asyncio.create_task(self.__serve_request_task()) 428 | self.__task_udp_receive = asyncio.create_task(self.__udp_receive_task()) 429 | self.__task_udp_feedback = asyncio.create_task(self.__udp_feedback_task()) 430 | self.__task_daemon = asyncio.create_task(self.__daemon_task()) 431 | 432 | self.status = RunningStatus.RUNNING 433 | log.success('>>>>>>>> Proxy Client is now STARTED !!! <<<<<<<<') 434 | 435 | try: 436 | for tcp_port in self.ini_tcp_map: 437 | await self.add_tcp_map(tcp_port, self.ini_tcp_map[tcp_port]) 438 | for udp_port in self.ini_udp_map: 439 | await self.add_udp_map(udp_port, self.ini_udp_map[udp_port]) 440 | except Exception as e: 441 | log.error(e) 442 | self.__protocol.close() 443 | 444 | await self.__protocol.wait_closed() 445 | log.warning('Stopping the Proxy Client ...') 446 | 447 | self.__task_serve_req.cancel() 448 | self.__task_udp_receive.cancel() 449 | self.__task_udp_feedback.cancel() 450 | self.__task_daemon.cancel() 451 | 452 | for key in self.__udp_req_map: 453 | self.__udp_req_map[key][2].close() 454 | self.__udp_socket.close() 455 | 456 | self.status = RunningStatus.STOPPED 457 | log.warning('>>>>>>>> Proxy Client is now STOPPED !!! <<<<<<<<') 458 | 459 | async def startup(self): 460 | asyncio.create_task(self.__main_task()) 461 | 462 | async def shutdown(self): 463 | log.warning(f'Stopping the Proxy Client by calling ...') 464 | self.__protocol.close() 465 | while self.status != RunningStatus.STOPPED: 466 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 467 | 468 | async def pause(self): 469 | if Protocol.Result.SUCCESS == await self.__protocol.request( 470 | Protocol.ClientCommand.PAUSE_PROXY, '', Protocol.CONNECTION_TIMEOUT): 471 | self.status = RunningStatus.PENDING 472 | log.warning('>>>>>>>> Proxy Client is now PENDING ... <<<<<<<<') 473 | else: 474 | log.error('Failed to pause the Proxy Client.') 475 | 476 | async def resume(self): 477 | if Protocol.Result.SUCCESS == await self.__protocol.request( 478 | Protocol.ClientCommand.RESUME_PROXY, '', Protocol.CONNECTION_TIMEOUT): 479 | self.status = RunningStatus.RUNNING 480 | log.success('>>>>>>>> Proxy Client is now RESUMED ... <<<<<<<<') 481 | else: 482 | log.error('Failed to resume the Proxy Client.') 483 | 484 | def run(self, debug=False): 485 | asyncio.run(self.__main_task(), debug=debug) 486 | 487 | 488 | def run_proxy_client(server_host, server_port, port_map, cid=None, name='AnonymousClient'): 489 | """ 运行代理客户端。 490 | :param server_host: 服务器主机 491 | :param server_port: 服务器端口 492 | :param port_map: 客户端的映射表 493 | :param cid: 客户端ID 494 | :param name: 客户端名称(客户端ID由服务端自动生成) 495 | :return: 496 | """ 497 | proxy_client = ProxyClient(server_host, server_port, port_map, cid, name) 498 | proxy_client.run(debug=False) 499 | 500 | 501 | def run_proxy_client_with_reconnect(server_host, server_port, port_map, cid=None, name='AnonymousClient'): 502 | """ 运行代理客户端,并在连接断开的情况下按一定的等待机制(使用相同的客户端ID)自动重连。 503 | :param server_host: 服务器主机 504 | :param server_port: 服务器端口 505 | :param port_map: 客户端的映射表 506 | :param cid: 客户端ID 507 | :param name: 客户端名称(客户端ID由服务端自动生成) 508 | :return: 509 | """ 510 | seed = 1 511 | wait = 2 512 | while True: 513 | if check_listening(server_host, server_port): 514 | proxy_client = ProxyClient(server_host, server_port, port_map, cid, name) 515 | proxy_client.run(debug=False) 516 | time_halt = 10 # halting period in case of disconnected 517 | log.info(f'Proxy client is disconnected, halt {time_halt}s before reconnect ...') 518 | time.sleep(time_halt) 519 | seed = 1 520 | wait = 2 521 | cid = proxy_client.client_id 522 | else: 523 | log.error('Proxy Service is Unreachable.') 524 | if wait < Protocol.MAX_RETRY_PERIOD: 525 | tmp = wait 526 | wait += seed 527 | seed = tmp 528 | if wait > Protocol.MAX_RETRY_PERIOD: 529 | wait = Protocol.MAX_RETRY_PERIOD 530 | log.info(f'Trying to reconnect Server({server_host}:{server_port}) after {wait}s ...') 531 | time.sleep(wait) 532 | 533 | 534 | if __name__ == '__main__': 535 | check_python_version(3, 7) 536 | 537 | log.remove() 538 | log.add(sys.stderr, level="DEBUG") 539 | # log.add("any-proxy-server.log", level="INFO", rotation="1 month") 540 | 541 | _client_id = '80000001' 542 | _client_name = 'DemoClient' 543 | _port_map = {'TCP': {80: 10080}, 'UDP': {}} 544 | 545 | # run_proxy_client('0.0.0.0', 10000, _port_map, _client_id, _client_name) 546 | run_proxy_client_with_reconnect('localhost', 10000, _port_map, _client_id, _client_name) 547 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | import socket 4 | import time 5 | from datetime import datetime 6 | from uuid import uuid4 7 | from operator import methodcaller 8 | from protocol import Protocol, ProtocolError 9 | from common import RunningStatus, ProxyError 10 | from utils import check_python_version, check_listening, check_udp_port_available, validate_id_string 11 | from loguru import logger as log 12 | 13 | 14 | class TcpMapInfo: 15 | CLIENT_PORT = 'client_port' 16 | CONN_POOL = 'connection_pool' 17 | TASK_LISTEN = 'task_listen' 18 | SWITCH = 'is_running' 19 | STATISTIC = 'statistic' 20 | CREATE_TIME = 'create_time' 21 | 22 | 23 | class UdpMapInfo: 24 | CLIENT_PORT = 'client_port' 25 | UDP_SOCKET = 'udp_socket' 26 | TASK_TRANSFER = 'task_transfer' 27 | SWITCH = 'is_running' 28 | STATISTIC = 'statistic' 29 | CREATE_TIME = 'create_time' 30 | 31 | 32 | class _PeerClient: 33 | """端口代理转发客户端连接类:支持TCP和UDP映射 34 | """ 35 | CONN_QUEUE_SIZE = 100 36 | 37 | def __init__(self, server_host, sock_stream, client_id, client_name): 38 | self.server_host = socket.gethostbyname(server_host) 39 | self.client_id = client_id 40 | self.client_name = client_name 41 | self.tcp_e_address = sock_stream[1].get_extra_info('peername') 42 | self.udp_e_address = (None, None) # NAT external address of this client. 43 | 44 | self.tcp_maps = {} # {server_port: {...}, ...} 45 | self.udp_maps = {} # {server_port: {...}, ...} 46 | 47 | self.__conn_queue = asyncio.Queue() # {(server_port, reader, writer), ...} 48 | 49 | self.__protocol = Protocol(sock_stream) 50 | self.__task_serve_req = asyncio.create_task(self.__serve_request_task()) 51 | self.__task_process_conn = asyncio.create_task(self.__process_conn_task()) 52 | self.__task_watchdog = asyncio.create_task(self.__watchdog_task()) 53 | 54 | self.status = RunningStatus.RUNNING # PREPARE -> RUNNING (-> PENDING) -> STOPPED 55 | self.ping = time.time() # the period when the last tcp ping receives. 56 | self.timestamp = datetime.utcnow() # the period when proxy client creates. 57 | 58 | def tcp_statistic(self, upstream=None, downstream=None): 59 | if upstream is None: 60 | self.__protocol.tcp_statistic[0] = 0 61 | else: 62 | self.__protocol.tcp_statistic[0] += upstream 63 | if downstream is None: 64 | self.__protocol.tcp_statistic[1] = 0 65 | else: 66 | self.__protocol.tcp_statistic[1] += downstream 67 | return tuple(self.__protocol.tcp_statistic) 68 | 69 | def udp_statistic(self, upstream=None, downstream=None): 70 | if upstream is None: 71 | self.__protocol.udp_statistic[0] = 0 72 | else: 73 | self.__protocol.udp_statistic[0] += upstream 74 | if downstream is None: 75 | self.__protocol.udp_statistic[1] = 0 76 | else: 77 | self.__protocol.udp_statistic[1] += downstream 78 | return tuple(self.__protocol.udp_statistic) 79 | 80 | async def __tcp_service_task(self, server_port): 81 | 82 | async def tcp_service_handler(reader, writer): 83 | if self.status == RunningStatus.RUNNING and self.tcp_maps[server_port][TcpMapInfo.SWITCH]: 84 | await self.__conn_queue.put((server_port, reader, writer)) 85 | else: 86 | writer.close() 87 | 88 | server = await asyncio.start_server(tcp_service_handler, self.server_host, server_port) 89 | async with server: 90 | await server.wait_closed() 91 | 92 | async def __tcp_data_relay_task(self, server_port, sock_stream, peer_stream): 93 | 94 | async def data_relay_monitor(): 95 | while True: 96 | if self.status == RunningStatus.RUNNING and server_port in self.tcp_maps \ 97 | and self.tcp_maps[server_port][TcpMapInfo.SWITCH]: 98 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 99 | else: 100 | break 101 | 102 | async def simplex_data_relay(reader, writer, upstream=True): 103 | while True: 104 | try: 105 | data = await reader.read(Protocol.SOCKET_BUFFER_SIZE) 106 | if not data: 107 | break 108 | writer.write(data) 109 | await writer.drain() 110 | self.tcp_maps[server_port][TcpMapInfo.STATISTIC][0 if upstream else 1] += len(data) 111 | except IOError: 112 | break 113 | 114 | client_port = self.tcp_maps[server_port][TcpMapInfo.CLIENT_PORT] 115 | rp = peer_stream[1].get_extra_info("peername")[1] 116 | log.info(f'Server({server_port}) ----> {self.client_id}({client_port}) [{rp}]') 117 | _, pending = await asyncio.wait({ 118 | asyncio.create_task(data_relay_monitor()), 119 | asyncio.create_task(simplex_data_relay(sock_stream[0], peer_stream[1], upstream=True)), 120 | asyncio.create_task(simplex_data_relay(peer_stream[0], sock_stream[1], upstream=False)), 121 | }, return_when=asyncio.FIRST_COMPLETED) 122 | for task in pending: 123 | task.cancel() 124 | sock_stream[1].close() 125 | peer_stream[1].close() 126 | log.info(f'Server({server_port}) --x-> {self.client_id}({client_port}) [{rp}]') 127 | 128 | async def __udp_service_task(self, server_port): 129 | udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 130 | udp_sock.setblocking(False) 131 | udp_sock.bind((self.server_host, server_port)) 132 | self.udp_maps[server_port][UdpMapInfo.UDP_SOCKET] = udp_sock 133 | while True: 134 | if self.status != RunningStatus.RUNNING or not self.udp_maps[server_port][TcpMapInfo.SWITCH]: 135 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 136 | continue 137 | try: 138 | data, address = udp_sock.recvfrom(Protocol.SOCKET_BUFFER_SIZE) 139 | if not data: 140 | raise IOError(f'EOF from {address}') 141 | except BlockingIOError: 142 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 143 | continue 144 | except Exception as e: 145 | log.error(e) 146 | break 147 | try: 148 | len_data = len(data) 149 | data_packet = Protocol.pack_udp_data_packet(server_port, address, data, pack_data=True) 150 | if self.udp_e_address: 151 | udp_sock.sendto(data_packet, self.udp_e_address) 152 | self.udp_statistic(Protocol.UDP_DATA_HEADER_LEN, 0) 153 | self.udp_maps[server_port][UdpMapInfo.STATISTIC][0] += len_data 154 | log.debug(f'Received UDP Packet on Port:{server_port}, Client:{address}, Size:{len_data}') 155 | except IOError as e: 156 | log.error(e) 157 | break 158 | except Exception as e: 159 | log.error(e) 160 | self.udp_maps[server_port][UdpMapInfo.SWITCH] = False 161 | self.udp_maps[server_port][UdpMapInfo.UDP_SOCKET].close() 162 | 163 | async def __process_conn_task(self): 164 | while True: 165 | try: 166 | server_port, reader, writer = await self.__conn_queue.get() 167 | except Exception as e: 168 | log.error(e) 169 | break 170 | else: 171 | self.__conn_queue.task_done() 172 | 173 | try: 174 | client_port = self.tcp_maps[server_port][TcpMapInfo.CLIENT_PORT] 175 | 176 | result = await self.__protocol.request( 177 | Protocol.ServerCommand.ADD_TCP_CONNECTION, client_port, timeout=Protocol.CONNECTION_TIMEOUT) 178 | 179 | status, detail = result.split(Protocol.PARAM_SEPARATOR, 1) 180 | if status == Protocol.Result.SUCCESS: 181 | conn_uuid = detail 182 | else: 183 | raise ProxyError(detail) 184 | 185 | wait_time = 0 186 | while wait_time < Protocol.CONNECTION_TIMEOUT: 187 | if conn_uuid in self.tcp_maps[server_port][TcpMapInfo.CONN_POOL]: 188 | break 189 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 190 | wait_time += Protocol.TASK_SCHEDULE_PERIOD 191 | if conn_uuid not in self.tcp_maps[server_port][TcpMapInfo.CONN_POOL]: 192 | raise ProxyError(f'{self.client_id}({client_port}) ----> Server({server_port}) ... [TIMEOUT].') 193 | 194 | reader2, writer2 = self.tcp_maps[server_port][TcpMapInfo.CONN_POOL][conn_uuid] 195 | del self.tcp_maps[server_port][TcpMapInfo.CONN_POOL][conn_uuid] 196 | asyncio.create_task(self.__tcp_data_relay_task(server_port, (reader, writer), (reader2, writer2))) 197 | 198 | except Exception as e: 199 | log.error(e) 200 | writer.close() 201 | 202 | async def __serve_request_task(self): 203 | while True: 204 | try: 205 | uuid, cmd, data = await self.__protocol.get_request(timeout=None) 206 | except Exception as e: 207 | log.error(e) 208 | break 209 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.SUCCESS, '')) 210 | 211 | try: 212 | if self.status != RunningStatus.RUNNING: # reject any client request. 213 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.INVALID, 'server is not running')) 214 | 215 | elif cmd == Protocol.Command.PING: 216 | self.ping = time.time() 217 | result += datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') 218 | 219 | elif cmd == Protocol.ClientCommand.CHECK_TCP_PORT: 220 | port = int(data) 221 | if check_listening('', port): 222 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.ERROR, 'tcp port is in use')) 223 | 224 | elif cmd == Protocol.ClientCommand.CHECK_UDP_PORT: 225 | port = int(data) 226 | if not check_udp_port_available(port): 227 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.ERROR, 'udp port is in use')) 228 | 229 | elif cmd == Protocol.ClientCommand.ADD_TCP_MAP: 230 | str_pair = data.split(Protocol.PARAM_SEPARATOR) 231 | c = int(str_pair[0]) 232 | s = int(str_pair[1]) 233 | await self.add_tcp_map(s, c) 234 | 235 | elif cmd == Protocol.ClientCommand.PAUSE_TCP_MAP: 236 | port = int(data) 237 | await self.pause_tcp_map(port) 238 | 239 | elif cmd == Protocol.ClientCommand.RESUME_TCP_MAP: 240 | port = int(data) 241 | await self.resume_tcp_map(port) 242 | 243 | elif cmd == Protocol.ClientCommand.REMOVE_TCP_MAP: 244 | port = int(data) 245 | await self.remove_tcp_map(port) 246 | 247 | elif cmd == Protocol.ClientCommand.ADD_UDP_MAP: 248 | str_pair = data.split(Protocol.PARAM_SEPARATOR) 249 | c = int(str_pair[0]) 250 | s = int(str_pair[1]) 251 | await self.add_udp_map(s, c) 252 | 253 | elif cmd == Protocol.ClientCommand.PAUSE_UDP_MAP: 254 | port = int(data) 255 | await self.pause_udp_map(port) 256 | 257 | elif cmd == Protocol.ClientCommand.RESUME_UDP_MAP: 258 | port = int(data) 259 | await self.resume_udp_map(port) 260 | 261 | elif cmd == Protocol.ClientCommand.REMOVE_UDP_MAP: 262 | port = int(data) 263 | await self.remove_udp_map(port) 264 | 265 | elif cmd == Protocol.ClientCommand.PAUSE_PROXY: 266 | await self.pause_client() 267 | 268 | elif cmd == Protocol.ClientCommand.RESUME_PROXY: 269 | await self.resume_client() 270 | 271 | elif cmd == Protocol.ClientCommand.DISCONNECT: 272 | await self.close_client() 273 | 274 | else: 275 | log.warning(f'Unrecognised request from {self.client_name}(CID:{self.client_id}): ' 276 | f'{Protocol.make_req(uuid, cmd, data)}') 277 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.INVALID, 'unrecognised request')) 278 | 279 | except Exception as e: 280 | log.error(f'Error while processing request({Protocol.make_req(uuid, cmd, data)}) ' 281 | f'from {self.client_name}(CID:{self.client_id}): {e}') 282 | result = Protocol.PARAM_SEPARATOR.join((Protocol.Result.ERROR, 'error while processing request')) 283 | finally: 284 | try: 285 | await self.__protocol.send_response(uuid, cmd, result) 286 | except Exception as e: 287 | log.error(e) 288 | break 289 | 290 | async def __watchdog_task(self): 291 | await self.__protocol.wait_closed() 292 | log.warning(f'Stopping {self.client_name}(CID:{self.client_id}) by watchdog ...') 293 | 294 | for server_port in list(self.tcp_maps.keys()): 295 | await self.remove_tcp_map(server_port) 296 | for server_port in list(self.udp_maps.keys()): 297 | await self.remove_udp_map(server_port) 298 | while not self.__conn_queue.empty(): 299 | stream = self.__conn_queue.get_nowait() 300 | stream[1].close() 301 | self.__conn_queue.task_done() 302 | self.__conn_queue = None 303 | self.__task_serve_req.cancel() 304 | self.__task_process_conn.cancel() 305 | 306 | self.status = RunningStatus.STOPPED 307 | log.warning(f'{self.client_name}(CID:{self.client_id}) is STOPPED by watchdog !!!') 308 | 309 | def _map_msg(self, server_port, client_port, status, extra=None): 310 | return f'{ sys._getframe(1).f_code.co_name.upper() }: ' \ 311 | f'Server({server_port}) >>> {self.client_id}({client_port}) ... ' \ 312 | f'[{ status.upper() }]{ " (" + extra + ")" if extra else "" }' 313 | 314 | async def add_tcp_map(self, server_port, client_port): 315 | if server_port in self.tcp_maps: 316 | raise ProxyError(self._map_msg(server_port, client_port, 'ERROR', f'already registered.')) 317 | if check_listening(self.server_host, server_port): 318 | raise ProxyError(self._map_msg(server_port, client_port, 'ERROR', f'target port is in use.')) 319 | self.tcp_maps[server_port] = { 320 | TcpMapInfo.CLIENT_PORT: client_port, 321 | TcpMapInfo.SWITCH: True, 322 | TcpMapInfo.STATISTIC: [0, 0], 323 | TcpMapInfo.CREATE_TIME: datetime.utcnow(), 324 | TcpMapInfo.CONN_POOL: {}, 325 | TcpMapInfo.TASK_LISTEN: asyncio.create_task(self.__tcp_service_task(server_port)), 326 | } 327 | log.success(self._map_msg(server_port, client_port, 'OK')) 328 | 329 | async def pause_tcp_map(self, server_port): 330 | if server_port not in self.tcp_maps: 331 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 332 | self.tcp_maps[server_port][TcpMapInfo.SWITCH] = False 333 | log.success(self._map_msg(server_port, self.tcp_maps[server_port][TcpMapInfo.CLIENT_PORT], 'OK')) 334 | 335 | async def resume_tcp_map(self, server_port): 336 | if server_port not in self.tcp_maps: 337 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 338 | self.tcp_maps[server_port][TcpMapInfo.SWITCH] = True 339 | log.success(self._map_msg(server_port, self.tcp_maps[server_port][TcpMapInfo.CLIENT_PORT], 'OK')) 340 | 341 | async def remove_tcp_map(self, server_port): 342 | if server_port not in self.tcp_maps: 343 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 344 | await self.pause_tcp_map(server_port) 345 | for stream in self.tcp_maps[server_port][TcpMapInfo.CONN_POOL]: 346 | stream[1].close() 347 | self.tcp_maps[server_port][TcpMapInfo.TASK_LISTEN].cancel() 348 | log.success(self._map_msg(server_port, self.tcp_maps[server_port][TcpMapInfo.CLIENT_PORT], 'OK')) 349 | del self.tcp_maps[server_port] 350 | 351 | async def add_udp_map(self, server_port, client_port): 352 | if server_port in self.udp_maps: 353 | raise ProxyError(self._map_msg(server_port, client_port, 'ERROR', f'already registered.')) 354 | if not check_udp_port_available(server_port): 355 | raise ProxyError(self._map_msg(server_port, client_port, 'ERROR', f'target port is in use.')) 356 | self.udp_maps[server_port] = { 357 | UdpMapInfo.CLIENT_PORT: client_port, 358 | UdpMapInfo.UDP_SOCKET: None, 359 | UdpMapInfo.SWITCH: True, 360 | UdpMapInfo.STATISTIC: [0, 0], 361 | UdpMapInfo.CREATE_TIME: datetime.utcnow(), 362 | UdpMapInfo.TASK_TRANSFER: asyncio.create_task(self.__udp_service_task(server_port)), 363 | } 364 | log.success(self._map_msg(server_port, client_port, 'OK')) 365 | 366 | async def pause_udp_map(self, server_port): 367 | if server_port not in self.udp_maps: 368 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 369 | self.udp_maps[server_port][UdpMapInfo.SWITCH] = False 370 | log.success(self._map_msg(server_port, self.udp_maps[server_port][UdpMapInfo.CLIENT_PORT], 'OK')) 371 | 372 | async def resume_udp_map(self, server_port): 373 | if server_port not in self.udp_maps: 374 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 375 | self.udp_maps[server_port][UdpMapInfo.SWITCH] = True 376 | log.success(self._map_msg(server_port, self.udp_maps[server_port][UdpMapInfo.CLIENT_PORT], 'OK')) 377 | 378 | async def remove_udp_map(self, server_port): 379 | if server_port not in self.udp_maps: 380 | raise ProxyError(self._map_msg(server_port, None, 'ERROR', f'not registered.')) 381 | await self.pause_udp_map(server_port) 382 | self.udp_maps[server_port][UdpMapInfo.UDP_SOCKET].close() 383 | self.udp_maps[server_port][UdpMapInfo.TASK_TRANSFER].cancel() 384 | log.success(self._map_msg(server_port, self.udp_maps[server_port][UdpMapInfo.CLIENT_PORT], 'OK')) 385 | del self.udp_maps[server_port] 386 | 387 | async def pause_client(self): 388 | msg = f'PAUSE_CLIENT: {self.client_name}(CID:{self.client_id}) ... ' 389 | if self.status == RunningStatus.STOPPED: 390 | raise ProxyError(msg + '[ERROR] (already stopped.)') 391 | self.status = RunningStatus.PENDING 392 | log.success(msg + '[OK]') 393 | 394 | async def resume_client(self): 395 | msg = f'RESUME_CLIENT: {self.client_name}(CID:{self.client_id}) ... ' 396 | if self.status == RunningStatus.STOPPED: 397 | raise ProxyError(msg + '[ERROR] (already stopped.)') 398 | self.status = RunningStatus.RUNNING 399 | log.success(msg + '[OK]') 400 | 401 | async def close_client(self, wait=True): 402 | self.__protocol.close() 403 | if wait: 404 | while self.status != RunningStatus.STOPPED: 405 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 406 | 407 | 408 | class ProxyServer: 409 | """端口代理转发客户端连接管理类 410 | """ 411 | def __init__(self, host='', port=10000, sid=None, name=None): 412 | host = socket.gethostbyname(host) 413 | port = int(port) 414 | if not sid: 415 | sid = uuid4().hex[-Protocol.DEFAULT_SID_LENGTH:].upper() 416 | if not name: 417 | name = 'ProxyServer' 418 | 419 | self.server_id = sid # 420 | self.server_name = name # 421 | self.server_host = host # '0.0.0.0' 422 | self.server_port = port # 423 | 424 | self.__clients = {} # {client_id: {...}, ...} 425 | self.__conn_queue = None # {(reader, writer), ...} 426 | self.__udp_socket = None # udp socket for datagram relaying. 427 | 428 | self.__task_tcp_service = None # 429 | self.__task_process_conn = None # 430 | self.__task_dispatch_datagram = None # 431 | self.__task_daemon = None # 432 | 433 | self.status = RunningStatus.PREPARE # PREPARE -> RUNNING -> STOPPED 434 | self.timestamp = datetime.utcnow() # the period when server creates. 435 | 436 | class AsyncApi: 437 | ADD_TCP_MAP = 'add_tcp_map' # args: server_port, client_port 438 | PAUSE_TCP_MAP = 'pause_tcp_map' # args: server_port 439 | RESUME_TCP_MAP = 'resume_tcp_map' # args: server_port 440 | REMOVE_TCP_MAP = 'remove_tcp_map' # args: server_port 441 | 442 | ADD_UDP_MAP = 'add_udp_port' # args: server_port, client_port 443 | PAUSE_UDP_MAP = 'pause_udp_map' # args: server_port 444 | RESUME_UDP_MAP = 'resume_udp_map' # args: server_port 445 | REMOVE_UDP_MAP = 'remove_udp_map' # args: server_port 446 | 447 | PAUSE_CLIENT = 'pause_client' # args: client_id 448 | RESUME_CLIENT = 'resume_client' # args: client_id 449 | CLOSE_CLIENT = 'close_client' # args: client_id 450 | 451 | # ADD_CLIENT = '__add_client' # args: client_id, client_name, sock_stream 452 | REMOVE_CLIENT = 'remove_client' # args: client_id 453 | 454 | STARTUP_SERVER = 'startup' # args: 455 | SHUTDOWN_SERVER = 'shutdown' # args: 456 | 457 | async def __add_client(self, client_id, client_name, sock_stream): 458 | msg = f'ADD_CLIENT: {client_name}(CID:{client_id}) ... ' 459 | if not validate_id_string(client_id, Protocol.CID_LENGTH): 460 | raise ProxyError(msg + f'[ERROR] (invalid client id provided.)') 461 | if client_id in self.__clients: 462 | if self.__clients[client_id].status == RunningStatus.STOPPED: 463 | del self.__clients[client_id] 464 | else: 465 | raise ProxyError(msg + f'[ERROR] (client id already registered.)') 466 | self.__clients[client_id] = _PeerClient(self.server_host, sock_stream, client_id, client_name) 467 | log.success(msg + '[OK]') 468 | 469 | async def remove_client(self, client_id): 470 | if client_id not in self.__clients: 471 | raise ProxyError(f'REMOVE_CLIENT: (None)(CID:{client_id}) ... [ERROR] (not registered.)') 472 | if self.__clients[client_id].status != RunningStatus.STOPPED: 473 | await self.__clients[client_id].close_client(wait=True) 474 | log.success(f'REMOVE_CLIENT: {self.__clients[client_id].client_name}(CID:{client_id}) ... [OK]') 475 | del self.__clients[client_id] 476 | 477 | async def __tcp_service_task(self): 478 | 479 | async def tcp_service_handler(reader, writer): 480 | await self.__conn_queue.put((reader, writer)) 481 | 482 | server = await asyncio.start_server(tcp_service_handler, self.server_host, self.server_port) 483 | async with server: 484 | await server.wait_closed() 485 | 486 | async def __process_conn_task(self): 487 | 488 | async def stream_readline(stream_reader): 489 | return await stream_reader.readline() 490 | 491 | while True: 492 | try: 493 | reader, writer = await self.__conn_queue.get() 494 | except Exception as e: 495 | log.error(e) 496 | break 497 | else: 498 | self.__conn_queue.task_done() 499 | 500 | try: 501 | task = asyncio.create_task(stream_readline(reader)) 502 | await asyncio.wait_for(task, timeout=Protocol.CONNECTION_TIMEOUT) 503 | identify = task.result().decode().strip() 504 | if not identify: 505 | peer = writer.get_extra_info('peername') 506 | raise ProxyError(f'Identify string is empty of tcp connection from {peer}') 507 | log.debug(identify) 508 | except Exception as e: 509 | log.error(e) 510 | writer.close() 511 | continue 512 | 513 | reject_reason = 'unknown error' 514 | try: 515 | conn_type, data = identify.split(Protocol.PARAM_SEPARATOR, 1) 516 | val_list = data.split(Protocol.PARAM_SEPARATOR) 517 | 518 | if conn_type == Protocol.ConnectionType.MANAGER: 519 | reject_reason = 'role of MANAGER is not supported yet.' 520 | raise NotImplementedError('Reject manager connection as ' + reject_reason) 521 | elif conn_type == Protocol.ConnectionType.PROXY_CLIENT: 522 | client_id = val_list[0] 523 | client_name = val_list[1] 524 | if not client_id: # generate new client id if it is empty 525 | while True: 526 | client_id = uuid4().hex[-Protocol.CID_LENGTH:].upper() 527 | if client_id not in self.__clients: 528 | break 529 | try: 530 | await self.__add_client(client_id, client_name, (reader, writer)) 531 | except Exception as e: 532 | reject_reason = str(e) 533 | raise 534 | writer.write(f'{Protocol.Result.SUCCESS}{Protocol.PARAM_SEPARATOR}{client_id}\n'.encode()) 535 | await writer.drain() 536 | elif conn_type == Protocol.ConnectionType.PROXY_TCP_DATA: 537 | client_id = val_list[0] 538 | server_port = int(val_list[1]) 539 | conn_id = val_list[2] 540 | if client_id not in self.__clients: 541 | reject_reason = f'provided client_id({client_id}) not found.' 542 | raise ProxyError('Reject reverse-data-connection (tcp) as ' + reject_reason) 543 | else: 544 | client = self.__clients[client_id] 545 | if client.status != RunningStatus.RUNNING: 546 | reject_reason = f'{client.client_name}(CID:{client.client_id}) is paused.' 547 | raise ProxyError('Reject reverse-data-connection (tcp) as ' + reject_reason) 548 | if not client.tcp_maps[server_port][TcpMapInfo.SWITCH]: 549 | reject_reason = f'server_port({server_port}) is paused.' 550 | raise ProxyError('Reject reverse-data-connection (tcp) as ' + reject_reason) 551 | client.tcp_maps[server_port][TcpMapInfo.CONN_POOL][conn_id] = (reader, writer) 552 | writer.write(f'{Protocol.Result.SUCCESS}{Protocol.PARAM_SEPARATOR}\n'.encode()) 553 | await writer.drain() 554 | else: 555 | reject_reason = 'unrecognised connection type' 556 | raise ProxyError(f'Reject connection on port({self.server_port}) with identify: {identify}') 557 | except Exception as e: 558 | log.error(e) 559 | writer.write(f'{Protocol.Result.ERROR}{Protocol.PARAM_SEPARATOR}{reject_reason}\n'.encode()) 560 | await writer.drain() 561 | writer.close() 562 | 563 | async def __dispatch_datagram_task(self): 564 | while True: 565 | try: 566 | data, address = self.__udp_socket.recvfrom(Protocol.SOCKET_BUFFER_SIZE) 567 | except BlockingIOError: 568 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 569 | continue 570 | except Exception as e: 571 | log.error(e) 572 | break 573 | 574 | try: 575 | try: 576 | packet_info = Protocol.unpack_udp_packet(data, unpack_data=False) 577 | except Exception as e: 578 | raise ProxyError(f'Protocol.unpack_udp_packet(): {e}') 579 | 580 | if packet_info[Protocol.UdpPacketInfo.TYPE] == Protocol.UdpPacketType.SYNC: 581 | cid = packet_info['client_id'] 582 | timestamp = packet_info[Protocol.UdpPacketInfo.TIMESTAMP] 583 | if cid not in self.__clients: 584 | raise ProxyError(f'Received udp sync packet from client(CID:{cid}) which is not found.') 585 | client = self.__clients[cid] 586 | log.debug(f'Received udp sync packet from {client.client_name}(CID:{cid}) at {timestamp}') 587 | client.udp_statistic(0, len(data)) 588 | client.udp_e_address = address 589 | if client.status == RunningStatus.RUNNING: 590 | sync_packet = Protocol.pack_udp_sync_packet(server_id=self.server_id) 591 | try: 592 | self.__udp_socket.sendto(sync_packet, address) 593 | except IOError as e: 594 | log.error(e) 595 | break 596 | client.udp_statistic(len(sync_packet), 0) 597 | elif packet_info[Protocol.UdpPacketInfo.TYPE] == Protocol.UdpPacketType.DATA: 598 | server_port = packet_info[Protocol.UdpPacketInfo.SERVER_PORT] 599 | user_address = packet_info[Protocol.UdpPacketInfo.USER_ADDRESS] 600 | client = None 601 | for cid in self.__clients: 602 | if server_port in self.__clients[cid].udp_maps: 603 | client = self.__clients[cid] 604 | client.udp_statistic(0, Protocol.UDP_DATA_HEADER_LEN) 605 | client.udp_maps[server_port][UdpMapInfo.STATISTIC][1] += \ 606 | len(data) - Protocol.UDP_DATA_HEADER_LEN 607 | break 608 | if not client: 609 | raise ProxyError(f'Received udp data packet which is not owned by any client.') 610 | if client.status == RunningStatus.RUNNING and client.udp_maps[server_port][UdpMapInfo.SWITCH]: 611 | udp_socket = client.udp_maps[server_port][UdpMapInfo.UDP_SOCKET] 612 | try: 613 | udp_socket.sendto(data[Protocol.UDP_DATA_HEADER_LEN:], user_address) 614 | except IOError as e: 615 | log.error(e) 616 | continue 617 | log.debug(f'Forwards udp data packet from port({server_port}) to {user_address}') 618 | else: 619 | log.warning(f'Drops udp data packet on port({server_port}) as switch is turned off.') 620 | else: 621 | raise ProxyError(f'Received udp packet from {address} with unknown type.') 622 | except Exception as e: 623 | log.debug(f'Received udp packet from: {address}, data:\n' + data.hex()) 624 | log.warning(e) 625 | 626 | async def __daemon_task(self): 627 | cnt = 0 628 | ping_timeout = Protocol.CLIENT_TCP_PING_PERIOD + Protocol.CONNECTION_TIMEOUT # *2 629 | while True: 630 | # 631 | # Check if client connection is lost. 632 | # 633 | for c in self.__clients: 634 | # 635 | # Attention: As the client ping request may arrives in 636 | # T(Protocol.CLIENT_TCP_PING_PERIOD) + 2 * T(Protocol.CONNECTION_TIMEOUT) seconds, 637 | # and the server should regard a client as LOST and REMOVE it prior to the client 638 | # detected the connection is unreachable (the client may auto reconnect after that). So, 639 | # the definition of threshold `ping_timeout` here is very important !!! 640 | # 641 | # Also see the definition of ProxyClient.__daemon_task in client.py. 642 | # 643 | if self.__clients[c].status != RunningStatus.STOPPED and \ 644 | time.time() - self.__clients[c].ping > ping_timeout: 645 | log.warning(f'{self.__clients[c].client_name}(CID:{self.__clients[c].client_id})' 646 | f' is inactive for {ping_timeout}s, and will be terminated ...') 647 | await self.__clients[c].close_client(wait=False) 648 | # 649 | # Every loop comes with a rest. 650 | # 651 | cnt += 1 652 | await asyncio.sleep(1) 653 | 654 | async def __main_task(self): 655 | if self.status == RunningStatus.RUNNING: 656 | log.error('Abort starting up the Proxy Server as it is already running.') 657 | return 658 | self.__clients = {} 659 | self.__conn_queue = asyncio.Queue() 660 | self.__udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 661 | self.__udp_socket.setblocking(False) 662 | self.__udp_socket.bind((self.server_host, self.server_port)) 663 | 664 | self.__task_tcp_service = asyncio.create_task(self.__tcp_service_task()) 665 | self.__task_process_conn = asyncio.create_task(self.__process_conn_task()) 666 | self.__task_dispatch_datagram = asyncio.create_task(self.__dispatch_datagram_task()) 667 | self.__task_daemon = asyncio.create_task(self.__daemon_task()) 668 | 669 | self.status = RunningStatus.RUNNING 670 | log.success('>>>>>>>> Proxy Service is now STARTED !!! <<<<<<<<') 671 | log.info(f'Serving on {self.server_host}:{self.server_port} (tcp & udp) ...') 672 | 673 | done, pending = await asyncio.wait({ 674 | self.__task_tcp_service, 675 | self.__task_process_conn, 676 | self.__task_dispatch_datagram, 677 | self.__task_daemon, # We terminate `__main_task` by cancel `__task_daemon`. 678 | }, return_when=asyncio.FIRST_COMPLETED) 679 | log.warning('Stopping the Proxy Server ...') 680 | 681 | for task in done: 682 | log.critical(task) 683 | for task in pending: 684 | task.cancel() 685 | 686 | for c in list(self.__clients): 687 | await self.__clients[c].close_client(wait=True) 688 | del self.__clients[c] 689 | while not self.__conn_queue.empty(): 690 | stream = self.__conn_queue.get_nowait() 691 | stream[1].close() 692 | self.__conn_queue.task_done() 693 | self.__conn_queue = None 694 | self.__udp_socket.close() 695 | 696 | self.status = RunningStatus.STOPPED 697 | log.warning('>>>>>>>> Proxy Service is now STOPPED !!! <<<<<<<<') 698 | 699 | async def startup(self): 700 | asyncio.create_task(self.__main_task()) 701 | 702 | async def shutdown(self): 703 | log.warning(f'Stopping the Proxy Server by calling ...') 704 | self.__task_daemon.cancel() 705 | while self.status != RunningStatus.STOPPED: 706 | await asyncio.sleep(Protocol.TASK_SCHEDULE_PERIOD) 707 | 708 | def run(self, debug=False): 709 | asyncio.run(self.__main_task(), debug=debug) 710 | 711 | def is_running(self): 712 | return self.status == RunningStatus.RUNNING 713 | 714 | def reset_statistic(self, client_id=None): 715 | if client_id and client_id not in self.__clients: 716 | log.error(f'Failed to reset statistic of Client(CID:{client_id}) as target client not found.') 717 | return 718 | for c in (client_id,) if client_id else self.__clients: 719 | for p in self.__clients[c].tcp_maps: 720 | self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][0] = 0 721 | self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][1] = 0 722 | for p in self.__clients[c].udp_maps: 723 | self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][0] = 0 724 | self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][1] = 0 725 | self.__clients[c].tcp_statistic(None, None) 726 | self.__clients[c].udp_statistic(None, None) 727 | log.success(f'Client(CID:{client_id})' if client_id else 'All clients' + ' statistic was reset.') 728 | 729 | def execute(self, loop, api_name, *args, timeout=None): 730 | """ 731 | 客户端及其代理端口的操作接口。以一般调用的方式执行协程函数功能。 732 | :param loop: 733 | :param api_name: 734 | :param args: 735 | :param timeout: 736 | :return: 737 | """ 738 | tcp_api_list = (self.AsyncApi.PAUSE_TCP_MAP, self.AsyncApi.RESUME_TCP_MAP, self.AsyncApi.REMOVE_TCP_MAP) 739 | udp_api_list = (self.AsyncApi.PAUSE_UDP_MAP, self.AsyncApi.RESUME_UDP_MAP, self.AsyncApi.REMOVE_UDP_MAP) 740 | client_api_list = (self.AsyncApi.PAUSE_CLIENT, self.AsyncApi.RESUME_CLIENT) 741 | client_api_list2 = (self.AsyncApi.REMOVE_CLIENT,) 742 | server_api_list = (self.AsyncApi.STARTUP_SERVER, self.AsyncApi.SHUTDOWN_SERVER) 743 | 744 | try: 745 | if api_name in tcp_api_list + udp_api_list: 746 | client_object = None 747 | server_port = args[0] 748 | for c in self.__clients: 749 | if api_name in tcp_api_list and server_port in self.__clients[c].tcp_maps or \ 750 | api_name in udp_api_list and server_port in self.__clients[c].udp_maps: 751 | client_object = self.__clients[c] 752 | break 753 | if client_object is None: 754 | raise ProxyError(f'proxy port{server_port} not found.') 755 | asyncio.run_coroutine_threadsafe( 756 | methodcaller(api_name, server_port)(client_object), loop=loop 757 | ).result(timeout=timeout) 758 | elif api_name in client_api_list: 759 | client_id = args[0] 760 | if client_id not in self.__clients: 761 | raise ProxyError(f'client with cid({client_id}) not found.') 762 | asyncio.run_coroutine_threadsafe( 763 | methodcaller(api_name)(self.__clients[client_id]), loop=loop 764 | ).result(timeout=timeout) 765 | elif api_name in client_api_list2: 766 | client_id = args[0] 767 | if client_id not in self.__clients: 768 | raise ProxyError(f'client with cid({client_id}) not found.') 769 | asyncio.run_coroutine_threadsafe( 770 | methodcaller(api_name, client_id)(self), loop=loop 771 | ).result(timeout=timeout) 772 | elif api_name in server_api_list: 773 | asyncio.run_coroutine_threadsafe( 774 | methodcaller(api_name)(self), loop=loop 775 | ).result(timeout=timeout) 776 | else: 777 | raise ProxyError(f'{api_name} not supported.') 778 | except ProxyError as e: 779 | raise ProxyError(f'Call of API({api_name}) failed as {e}') 780 | 781 | def _get_server_info(self): 782 | protocol_tcp_s = [0, 0] 783 | protocol_udp_s = [0, 0] 784 | port_tcp_s = [0, 0] 785 | port_udp_s = [0, 0] 786 | alive_clients = 0 787 | for c in self.__clients: 788 | if self.__clients[c].status != RunningStatus.STOPPED: 789 | alive_clients += 1 790 | pt = self.__clients[c].tcp_statistic(0, 0) 791 | pu = self.__clients[c].udp_statistic(0, 0) 792 | protocol_tcp_s[0] += pt[0] 793 | protocol_tcp_s[1] += pt[1] 794 | protocol_udp_s[0] += pu[0] 795 | protocol_udp_s[1] += pu[1] 796 | for p in self.__clients[c].tcp_maps: 797 | port_tcp_s[0] += self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][0] 798 | port_tcp_s[1] += self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][1] 799 | for p in self.__clients[c].udp_maps: 800 | port_udp_s[0] += self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][0] 801 | port_udp_s[1] += self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][1] 802 | server_info = { 803 | "server_id": self.server_id, 804 | "server_name": self.server_name, 805 | "server_host": self.server_host, 806 | "server_port": self.server_port, 807 | "start_time": self.timestamp, 808 | "is_running": self.status == RunningStatus.RUNNING, 809 | "alive_clients": alive_clients, 810 | "total_clients": len(self.__clients), 811 | "tcp_statistic": (port_tcp_s[0], port_tcp_s[1]), 812 | "udp_statistic": (port_udp_s[0], port_udp_s[1]), 813 | "total_statistic": (protocol_tcp_s[0] + protocol_udp_s[0] + port_tcp_s[0] + port_udp_s[0], 814 | protocol_tcp_s[1] + protocol_udp_s[1] + port_tcp_s[1] + port_udp_s[1]), 815 | } 816 | return server_info 817 | 818 | def _get_clients_info(self, client_id=None): 819 | clients_info = {} 820 | if client_id and client_id not in self.__clients: 821 | return clients_info 822 | for c in (client_id,) if client_id else self.__clients: 823 | protocol_tcp_s = self.__clients[c].tcp_statistic(0, 0) 824 | protocol_udp_s = self.__clients[c].udp_statistic(0, 0) 825 | port_tcp_s = [0, 0] 826 | port_udp_s = [0, 0] 827 | c_switch = self.__clients[c].status == RunningStatus.RUNNING 828 | tcp_maps = {} 829 | udp_maps = {} 830 | for p in sorted(self.__clients[c].tcp_maps): 831 | tcp_maps[p] = { 832 | "client_port": self.__clients[c].tcp_maps[p][TcpMapInfo.CLIENT_PORT], 833 | "is_running": self.__clients[c].tcp_maps[p][TcpMapInfo.SWITCH] and c_switch, 834 | "statistic": tuple(self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC]), 835 | "create_time": self.__clients[c].tcp_maps[p][TcpMapInfo.CREATE_TIME], 836 | } 837 | port_tcp_s[0] += self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][0] 838 | port_tcp_s[1] += self.__clients[c].tcp_maps[p][TcpMapInfo.STATISTIC][1] 839 | for p in sorted(self.__clients[c].udp_maps): 840 | udp_maps[p] = { 841 | "client_port": self.__clients[c].udp_maps[p][UdpMapInfo.CLIENT_PORT], 842 | "is_running": self.__clients[c].udp_maps[p][UdpMapInfo.SWITCH] and c_switch, 843 | "statistic": tuple(self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC]), 844 | "create_time": self.__clients[c].udp_maps[p][UdpMapInfo.CREATE_TIME], 845 | } 846 | port_udp_s[0] += self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][0] 847 | port_udp_s[1] += self.__clients[c].udp_maps[p][UdpMapInfo.STATISTIC][1] 848 | clients_info[c] = { 849 | "client_id": self.__clients[c].client_id, 850 | "client_name": self.__clients[c].client_name, 851 | "tcp_e_addr": self.__clients[c].tcp_e_address, 852 | "udp_e_addr": self.__clients[c].udp_e_address, 853 | "start_time": self.__clients[c].timestamp, 854 | "is_alive": self.__clients[c].status != RunningStatus.STOPPED, 855 | "is_running": self.__clients[c].status == RunningStatus.RUNNING, 856 | "tcp_maps": tcp_maps, 857 | "udp_maps": udp_maps, 858 | "tcp_statistic": port_tcp_s, 859 | "udp_statistic": port_udp_s, 860 | "total_statistic": (protocol_tcp_s[0] + protocol_udp_s[0] + port_tcp_s[0] + port_udp_s[0], 861 | protocol_tcp_s[1] + protocol_udp_s[1] + port_tcp_s[1] + port_udp_s[1]), 862 | } 863 | return clients_info 864 | 865 | 866 | def run_proxy_server(host, port, name): 867 | """ 运行代理服务端。 868 | :param host: 代理服务器地址(本机) 869 | :param port: 代理服务端口(TCP和UDP同时开启,共用同一个端口号) 870 | :param name: 代理服务器名称 871 | :return: 872 | """ 873 | proxy_server = ProxyServer(host, port, None, name) 874 | proxy_server.run(debug=False) 875 | 876 | 877 | def run_proxy_server_with_web(host, port, name, web_port, start_proxy=True): 878 | """ 运行带web管理功能的代理服务端。 879 | :param host: 代理服务器地址(本机) 880 | :param port: 代理服务端口(TCP和UDP同时开启,共用同一个端口号) 881 | :param name: 代理服务器名称 882 | :param web_port: Web管理服务端口 883 | :param start_proxy: 是否直接启动代理服务 884 | :return: 885 | """ 886 | from threading import Thread 887 | from flask_app import create_app 888 | 889 | def thread_proxy_server(loop): 890 | asyncio.set_event_loop(loop) 891 | loop.run_forever() 892 | 893 | def proxy_server_api(api_name, *args, timeout=None): 894 | result = None 895 | try: 896 | result = proxy_server.execute(proxy_loop, api_name, *args, timeout=timeout) 897 | except (asyncio.TimeoutError, ProtocolError, ProxyError, Exception) as e: 898 | log.error(f'Call of Proxy Server: {e}') 899 | finally: 900 | return result 901 | 902 | flask_app = create_app() 903 | proxy_server = ProxyServer(host, port, None, name) 904 | proxy_loop = asyncio.new_event_loop() 905 | thread_proxy = Thread(target=thread_proxy_server, args=(proxy_loop,)) 906 | 907 | # flask_app.proxy_server = proxy_server 908 | flask_app.proxy_api = ProxyServer.AsyncApi 909 | flask_app.proxy_execute = proxy_server_api 910 | flask_app.proxy_is_running = proxy_server.is_running 911 | flask_app.proxy_reset_statistic = proxy_server.reset_statistic 912 | flask_app.proxy_server_info = proxy_server._get_server_info 913 | flask_app.proxy_clients_info = proxy_server._get_clients_info 914 | 915 | thread_proxy.setDaemon(True) 916 | thread_proxy.start() 917 | 918 | if start_proxy: 919 | flask_app.proxy_execute(ProxyServer.AsyncApi.STARTUP_SERVER) 920 | try: 921 | flask_app.run(host=host, port=web_port, debug=False) 922 | except KeyboardInterrupt: 923 | flask_app.execute(ProxyServer.AsyncApi.SHUTDOWN_SERVER) 924 | finally: 925 | proxy_loop.stop() 926 | 927 | 928 | if __name__ == '__main__': 929 | check_python_version(3, 7) 930 | 931 | log.remove() 932 | log.add(sys.stderr, level="DEBUG") 933 | # log.add("any-proxy-server.log", level="INFO", rotation="1 month") 934 | 935 | # run_proxy_server('0.0.0.0', 10000, 'ProxyServer') 936 | run_proxy_server_with_web('0.0.0.0', 10000, 'ProxyServer', web_port=9999) 937 | -------------------------------------------------------------------------------- /flask_app/static/js/moment.min.js: -------------------------------------------------------------------------------- 1 | //! moment.js 2 | //! version : 2.18.1 3 | //! authors : Tim Wood, Iskren Chernev, Moment.js contributors 4 | //! license : MIT 5 | //! momentjs.com 6 | !function(a,b){"object"==typeof exports&&"undefined"!=typeof module?module.exports=b():"function"==typeof define&&define.amd?define(b):a.moment=b()}(this,function(){"use strict";function a(){return sd.apply(null,arguments)}function b(a){sd=a}function c(a){return a instanceof Array||"[object Array]"===Object.prototype.toString.call(a)}function d(a){return null!=a&&"[object Object]"===Object.prototype.toString.call(a)}function e(a){var b;for(b in a)return!1;return!0}function f(a){return void 0===a}function g(a){return"number"==typeof a||"[object Number]"===Object.prototype.toString.call(a)}function h(a){return a instanceof Date||"[object Date]"===Object.prototype.toString.call(a)}function i(a,b){var c,d=[];for(c=0;c0)for(c=0;c0?"future":"past"];return z(c)?c(b):c.replace(/%s/i,b)}function J(a,b){var c=a.toLowerCase();Hd[c]=Hd[c+"s"]=Hd[b]=a}function K(a){return"string"==typeof a?Hd[a]||Hd[a.toLowerCase()]:void 0}function L(a){var b,c,d={};for(c in a)j(a,c)&&(b=K(c),b&&(d[b]=a[c]));return d}function M(a,b){Id[a]=b}function N(a){var b=[];for(var c in a)b.push({unit:c,priority:Id[c]});return b.sort(function(a,b){return a.priority-b.priority}),b}function O(b,c){return function(d){return null!=d?(Q(this,b,d),a.updateOffset(this,c),this):P(this,b)}}function P(a,b){return a.isValid()?a._d["get"+(a._isUTC?"UTC":"")+b]():NaN}function Q(a,b,c){a.isValid()&&a._d["set"+(a._isUTC?"UTC":"")+b](c)}function R(a){return a=K(a),z(this[a])?this[a]():this}function S(a,b){if("object"==typeof a){a=L(a);for(var c=N(a),d=0;d=0;return(f?c?"+":"":"-")+Math.pow(10,Math.max(0,e)).toString().substr(1)+d}function U(a,b,c,d){var e=d;"string"==typeof d&&(e=function(){return this[d]()}),a&&(Md[a]=e),b&&(Md[b[0]]=function(){return T(e.apply(this,arguments),b[1],b[2])}),c&&(Md[c]=function(){return this.localeData().ordinal(e.apply(this,arguments),a)})}function V(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function W(a){var b,c,d=a.match(Jd);for(b=0,c=d.length;b=0&&Kd.test(a);)a=a.replace(Kd,c),Kd.lastIndex=0,d-=1;return a}function Z(a,b,c){ce[a]=z(b)?b:function(a,d){return a&&c?c:b}}function $(a,b){return j(ce,a)?ce[a](b._strict,b._locale):new RegExp(_(a))}function _(a){return aa(a.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e}))}function aa(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function ba(a,b){var c,d=b;for("string"==typeof a&&(a=[a]),g(b)&&(d=function(a,c){c[b]=u(a)}),c=0;c=0&&isFinite(h.getFullYear())&&h.setFullYear(a),h}function ta(a){var b=new Date(Date.UTC.apply(null,arguments));return a<100&&a>=0&&isFinite(b.getUTCFullYear())&&b.setUTCFullYear(a),b}function ua(a,b,c){var d=7+b-c,e=(7+ta(a,0,d).getUTCDay()-b)%7;return-e+d-1}function va(a,b,c,d,e){var f,g,h=(7+c-d)%7,i=ua(a,d,e),j=1+7*(b-1)+h+i;return j<=0?(f=a-1,g=pa(f)+j):j>pa(a)?(f=a+1,g=j-pa(a)):(f=a,g=j),{year:f,dayOfYear:g}}function wa(a,b,c){var d,e,f=ua(a.year(),b,c),g=Math.floor((a.dayOfYear()-f-1)/7)+1;return g<1?(e=a.year()-1,d=g+xa(e,b,c)):g>xa(a.year(),b,c)?(d=g-xa(a.year(),b,c),e=a.year()+1):(e=a.year(),d=g),{week:d,year:e}}function xa(a,b,c){var d=ua(a,b,c),e=ua(a+1,b,c);return(pa(a)-d+e)/7}function ya(a){return wa(a,this._week.dow,this._week.doy).week}function za(){return this._week.dow}function Aa(){return this._week.doy}function Ba(a){var b=this.localeData().week(this);return null==a?b:this.add(7*(a-b),"d")}function Ca(a){var b=wa(this,1,4).week;return null==a?b:this.add(7*(a-b),"d")}function Da(a,b){return"string"!=typeof a?a:isNaN(a)?(a=b.weekdaysParse(a),"number"==typeof a?a:null):parseInt(a,10)}function Ea(a,b){return"string"==typeof a?b.weekdaysParse(a)%7||7:isNaN(a)?null:a}function Fa(a,b){return a?c(this._weekdays)?this._weekdays[a.day()]:this._weekdays[this._weekdays.isFormat.test(b)?"format":"standalone"][a.day()]:c(this._weekdays)?this._weekdays:this._weekdays.standalone}function Ga(a){return a?this._weekdaysShort[a.day()]:this._weekdaysShort}function Ha(a){return a?this._weekdaysMin[a.day()]:this._weekdaysMin}function Ia(a,b,c){var d,e,f,g=a.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],d=0;d<7;++d)f=l([2e3,1]).day(d),this._minWeekdaysParse[d]=this.weekdaysMin(f,"").toLocaleLowerCase(),this._shortWeekdaysParse[d]=this.weekdaysShort(f,"").toLocaleLowerCase(),this._weekdaysParse[d]=this.weekdays(f,"").toLocaleLowerCase();return c?"dddd"===b?(e=ne.call(this._weekdaysParse,g),e!==-1?e:null):"ddd"===b?(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:null):(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null):"dddd"===b?(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null))):"ddd"===b?(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:null))):(e=ne.call(this._minWeekdaysParse,g),e!==-1?e:(e=ne.call(this._weekdaysParse,g),e!==-1?e:(e=ne.call(this._shortWeekdaysParse,g),e!==-1?e:null)))}function Ja(a,b,c){var d,e,f;if(this._weekdaysParseExact)return Ia.call(this,a,b,c);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),d=0;d<7;d++){if(e=l([2e3,1]).day(d),c&&!this._fullWeekdaysParse[d]&&(this._fullWeekdaysParse[d]=new RegExp("^"+this.weekdays(e,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[d]=new RegExp("^"+this.weekdaysShort(e,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[d]=new RegExp("^"+this.weekdaysMin(e,"").replace(".",".?")+"$","i")),this._weekdaysParse[d]||(f="^"+this.weekdays(e,"")+"|^"+this.weekdaysShort(e,"")+"|^"+this.weekdaysMin(e,""),this._weekdaysParse[d]=new RegExp(f.replace(".",""),"i")),c&&"dddd"===b&&this._fullWeekdaysParse[d].test(a))return d;if(c&&"ddd"===b&&this._shortWeekdaysParse[d].test(a))return d;if(c&&"dd"===b&&this._minWeekdaysParse[d].test(a))return d;if(!c&&this._weekdaysParse[d].test(a))return d}}function Ka(a){if(!this.isValid())return null!=a?this:NaN;var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=Da(a,this.localeData()),this.add(a-b,"d")):b}function La(a){if(!this.isValid())return null!=a?this:NaN;var b=(this.day()+7-this.localeData()._week.dow)%7;return null==a?b:this.add(a-b,"d")}function Ma(a){if(!this.isValid())return null!=a?this:NaN;if(null!=a){var b=Ea(a,this.localeData());return this.day(this.day()%7?b:b-7)}return this.day()||7}function Na(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysStrictRegex:this._weekdaysRegex):(j(this,"_weekdaysRegex")||(this._weekdaysRegex=ye),this._weekdaysStrictRegex&&a?this._weekdaysStrictRegex:this._weekdaysRegex)}function Oa(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(j(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ze),this._weekdaysShortStrictRegex&&a?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function Pa(a){return this._weekdaysParseExact?(j(this,"_weekdaysRegex")||Qa.call(this),a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(j(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Ae),this._weekdaysMinStrictRegex&&a?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qa(){function a(a,b){return b.length-a.length}var b,c,d,e,f,g=[],h=[],i=[],j=[];for(b=0;b<7;b++)c=l([2e3,1]).day(b),d=this.weekdaysMin(c,""),e=this.weekdaysShort(c,""),f=this.weekdays(c,""),g.push(d),h.push(e),i.push(f),j.push(d),j.push(e),j.push(f);for(g.sort(a),h.sort(a),i.sort(a),j.sort(a),b=0;b<7;b++)h[b]=aa(h[b]),i[b]=aa(i[b]),j[b]=aa(j[b]);this._weekdaysRegex=new RegExp("^("+j.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+h.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+g.join("|")+")","i")}function Ra(){return this.hours()%12||12}function Sa(){return this.hours()||24}function Ta(a,b){U(a,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),b)})}function Ua(a,b){return b._meridiemParse}function Va(a){return"p"===(a+"").toLowerCase().charAt(0)}function Wa(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"}function Xa(a){return a?a.toLowerCase().replace("_","-"):a}function Ya(a){for(var b,c,d,e,f=0;f0;){if(d=Za(e.slice(0,b).join("-")))return d;if(c&&c.length>=b&&v(e,c,!0)>=b-1)break;b--}f++}return null}function Za(a){var b=null;if(!Fe[a]&&"undefined"!=typeof module&&module&&module.exports)try{b=Be._abbr,require("./locale/"+a),$a(b)}catch(a){}return Fe[a]}function $a(a,b){var c;return a&&(c=f(b)?bb(a):_a(a,b),c&&(Be=c)),Be._abbr}function _a(a,b){if(null!==b){var c=Ee;if(b.abbr=a,null!=Fe[a])y("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),c=Fe[a]._config;else if(null!=b.parentLocale){if(null==Fe[b.parentLocale])return Ge[b.parentLocale]||(Ge[b.parentLocale]=[]),Ge[b.parentLocale].push({name:a,config:b}),null;c=Fe[b.parentLocale]._config}return Fe[a]=new C(B(c,b)),Ge[a]&&Ge[a].forEach(function(a){_a(a.name,a.config)}),$a(a),Fe[a]}return delete Fe[a],null}function ab(a,b){if(null!=b){var c,d=Ee;null!=Fe[a]&&(d=Fe[a]._config),b=B(d,b),c=new C(b),c.parentLocale=Fe[a],Fe[a]=c,$a(a)}else null!=Fe[a]&&(null!=Fe[a].parentLocale?Fe[a]=Fe[a].parentLocale:null!=Fe[a]&&delete Fe[a]);return Fe[a]}function bb(a){var b;if(a&&a._locale&&a._locale._abbr&&(a=a._locale._abbr),!a)return Be;if(!c(a)){if(b=Za(a))return b;a=[a]}return Ya(a)}function cb(){return Ad(Fe)}function db(a){var b,c=a._a;return c&&n(a).overflow===-2&&(b=c[fe]<0||c[fe]>11?fe:c[ge]<1||c[ge]>ea(c[ee],c[fe])?ge:c[he]<0||c[he]>24||24===c[he]&&(0!==c[ie]||0!==c[je]||0!==c[ke])?he:c[ie]<0||c[ie]>59?ie:c[je]<0||c[je]>59?je:c[ke]<0||c[ke]>999?ke:-1,n(a)._overflowDayOfYear&&(bge)&&(b=ge),n(a)._overflowWeeks&&b===-1&&(b=le),n(a)._overflowWeekday&&b===-1&&(b=me),n(a).overflow=b),a}function eb(a){var b,c,d,e,f,g,h=a._i,i=He.exec(h)||Ie.exec(h);if(i){for(n(a).iso=!0,b=0,c=Ke.length;b10?"YYYY ":"YY "),f="HH:mm"+(c[4]?":ss":""),c[1]){var l=new Date(c[2]),m=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][l.getDay()];if(c[1].substr(0,3)!==m)return n(a).weekdayMismatch=!0,void(a._isValid=!1)}switch(c[5].length){case 2:0===i?h=" +0000":(i=k.indexOf(c[5][1].toUpperCase())-12,h=(i<0?" -":" +")+(""+i).replace(/^-?/,"0").match(/..$/)[0]+"00");break;case 4:h=j[c[5]];break;default:h=j[" GMT"]}c[5]=h,a._i=c.splice(1).join(""),g=" ZZ",a._f=d+e+f+g,lb(a),n(a).rfc2822=!0}else a._isValid=!1}function gb(b){var c=Me.exec(b._i);return null!==c?void(b._d=new Date(+c[1])):(eb(b),void(b._isValid===!1&&(delete b._isValid,fb(b),b._isValid===!1&&(delete b._isValid,a.createFromInputFallback(b)))))}function hb(a,b,c){return null!=a?a:null!=b?b:c}function ib(b){var c=new Date(a.now());return b._useUTC?[c.getUTCFullYear(),c.getUTCMonth(),c.getUTCDate()]:[c.getFullYear(),c.getMonth(),c.getDate()]}function jb(a){var b,c,d,e,f=[];if(!a._d){for(d=ib(a),a._w&&null==a._a[ge]&&null==a._a[fe]&&kb(a),null!=a._dayOfYear&&(e=hb(a._a[ee],d[ee]),(a._dayOfYear>pa(e)||0===a._dayOfYear)&&(n(a)._overflowDayOfYear=!0),c=ta(e,0,a._dayOfYear),a._a[fe]=c.getUTCMonth(),a._a[ge]=c.getUTCDate()),b=0;b<3&&null==a._a[b];++b)a._a[b]=f[b]=d[b];for(;b<7;b++)a._a[b]=f[b]=null==a._a[b]?2===b?1:0:a._a[b];24===a._a[he]&&0===a._a[ie]&&0===a._a[je]&&0===a._a[ke]&&(a._nextDay=!0,a._a[he]=0),a._d=(a._useUTC?ta:sa).apply(null,f),null!=a._tzm&&a._d.setUTCMinutes(a._d.getUTCMinutes()-a._tzm),a._nextDay&&(a._a[he]=24)}}function kb(a){var b,c,d,e,f,g,h,i;if(b=a._w,null!=b.GG||null!=b.W||null!=b.E)f=1,g=4,c=hb(b.GG,a._a[ee],wa(tb(),1,4).year),d=hb(b.W,1),e=hb(b.E,1),(e<1||e>7)&&(i=!0);else{f=a._locale._week.dow,g=a._locale._week.doy;var j=wa(tb(),f,g);c=hb(b.gg,a._a[ee],j.year),d=hb(b.w,j.week),null!=b.d?(e=b.d,(e<0||e>6)&&(i=!0)):null!=b.e?(e=b.e+f,(b.e<0||b.e>6)&&(i=!0)):e=f}d<1||d>xa(c,f,g)?n(a)._overflowWeeks=!0:null!=i?n(a)._overflowWeekday=!0:(h=va(c,d,e,f,g),a._a[ee]=h.year,a._dayOfYear=h.dayOfYear)}function lb(b){if(b._f===a.ISO_8601)return void eb(b);if(b._f===a.RFC_2822)return void fb(b);b._a=[],n(b).empty=!0;var c,d,e,f,g,h=""+b._i,i=h.length,j=0;for(e=Y(b._f,b._locale).match(Jd)||[],c=0;c0&&n(b).unusedInput.push(g),h=h.slice(h.indexOf(d)+d.length),j+=d.length),Md[f]?(d?n(b).empty=!1:n(b).unusedTokens.push(f),da(f,d,b)):b._strict&&!d&&n(b).unusedTokens.push(f);n(b).charsLeftOver=i-j,h.length>0&&n(b).unusedInput.push(h),b._a[he]<=12&&n(b).bigHour===!0&&b._a[he]>0&&(n(b).bigHour=void 0),n(b).parsedDateParts=b._a.slice(0),n(b).meridiem=b._meridiem,b._a[he]=mb(b._locale,b._a[he],b._meridiem),jb(b),db(b)}function mb(a,b,c){var d;return null==c?b:null!=a.meridiemHour?a.meridiemHour(b,c):null!=a.isPM?(d=a.isPM(c),d&&b<12&&(b+=12),d||12!==b||(b=0),b):b}function nb(a){var b,c,d,e,f;if(0===a._f.length)return n(a).invalidFormat=!0,void(a._d=new Date(NaN));for(e=0;ethis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function Ob(){if(!f(this._isDSTShifted))return this._isDSTShifted;var a={};if(q(a,this),a=qb(a),a._a){var b=a._isUTC?l(a._a):tb(a._a);this._isDSTShifted=this.isValid()&&v(a._a,b.toArray())>0}else this._isDSTShifted=!1;return this._isDSTShifted}function Pb(){return!!this.isValid()&&!this._isUTC}function Qb(){return!!this.isValid()&&this._isUTC}function Rb(){return!!this.isValid()&&(this._isUTC&&0===this._offset)}function Sb(a,b){var c,d,e,f=a,h=null;return Bb(a)?f={ms:a._milliseconds,d:a._days,M:a._months}:g(a)?(f={},b?f[b]=a:f.milliseconds=a):(h=Te.exec(a))?(c="-"===h[1]?-1:1,f={y:0,d:u(h[ge])*c,h:u(h[he])*c,m:u(h[ie])*c,s:u(h[je])*c,ms:u(Cb(1e3*h[ke]))*c}):(h=Ue.exec(a))?(c="-"===h[1]?-1:1,f={y:Tb(h[2],c),M:Tb(h[3],c),w:Tb(h[4],c),d:Tb(h[5],c),h:Tb(h[6],c),m:Tb(h[7],c),s:Tb(h[8],c)}):null==f?f={}:"object"==typeof f&&("from"in f||"to"in f)&&(e=Vb(tb(f.from),tb(f.to)),f={},f.ms=e.milliseconds,f.M=e.months),d=new Ab(f),Bb(a)&&j(a,"_locale")&&(d._locale=a._locale),d}function Tb(a,b){var c=a&&parseFloat(a.replace(",","."));return(isNaN(c)?0:c)*b}function Ub(a,b){var c={milliseconds:0,months:0};return c.months=b.month()-a.month()+12*(b.year()-a.year()),a.clone().add(c.months,"M").isAfter(b)&&--c.months,c.milliseconds=+b-+a.clone().add(c.months,"M"),c}function Vb(a,b){var c;return a.isValid()&&b.isValid()?(b=Fb(b,a),a.isBefore(b)?c=Ub(a,b):(c=Ub(b,a),c.milliseconds=-c.milliseconds,c.months=-c.months),c):{milliseconds:0,months:0}}function Wb(a,b){return function(c,d){var e,f;return null===d||isNaN(+d)||(y(b,"moment()."+b+"(period, number) is deprecated. Please use moment()."+b+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),f=c,c=d,d=f),c="string"==typeof c?+c:c,e=Sb(c,d),Xb(this,e,a),this}}function Xb(b,c,d,e){var f=c._milliseconds,g=Cb(c._days),h=Cb(c._months);b.isValid()&&(e=null==e||e,f&&b._d.setTime(b._d.valueOf()+f*d),g&&Q(b,"Date",P(b,"Date")+g*d),h&&ja(b,P(b,"Month")+h*d),e&&a.updateOffset(b,g||h))}function Yb(a,b){var c=a.diff(b,"days",!0);return c<-6?"sameElse":c<-1?"lastWeek":c<0?"lastDay":c<1?"sameDay":c<2?"nextDay":c<7?"nextWeek":"sameElse"}function Zb(b,c){var d=b||tb(),e=Fb(d,this).startOf("day"),f=a.calendarFormat(this,e)||"sameElse",g=c&&(z(c[f])?c[f].call(this,d):c[f]);return this.format(g||this.localeData().calendar(f,this,tb(d)))}function $b(){return new r(this)}function _b(a,b){var c=s(a)?a:tb(a);return!(!this.isValid()||!c.isValid())&&(b=K(f(b)?"millisecond":b),"millisecond"===b?this.valueOf()>c.valueOf():c.valueOf()9999?X(a,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):z(Date.prototype.toISOString)?this.toDate().toISOString():X(a,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")}function jc(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var a="moment",b="";this.isLocal()||(a=0===this.utcOffset()?"moment.utc":"moment.parseZone",b="Z");var c="["+a+'("]',d=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",e="-MM-DD[T]HH:mm:ss.SSS",f=b+'[")]';return this.format(c+d+e+f)}function kc(b){b||(b=this.isUtc()?a.defaultFormatUtc:a.defaultFormat);var c=X(this,b);return this.localeData().postformat(c)}function lc(a,b){return this.isValid()&&(s(a)&&a.isValid()||tb(a).isValid())?Sb({to:this,from:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function mc(a){return this.from(tb(),a)}function nc(a,b){return this.isValid()&&(s(a)&&a.isValid()||tb(a).isValid())?Sb({from:this,to:a}).locale(this.locale()).humanize(!b):this.localeData().invalidDate()}function oc(a){return this.to(tb(),a)}function pc(a){var b;return void 0===a?this._locale._abbr:(b=bb(a),null!=b&&(this._locale=b),this)}function qc(){return this._locale}function rc(a){switch(a=K(a)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a&&this.weekday(0),"isoWeek"===a&&this.isoWeekday(1),"quarter"===a&&this.month(3*Math.floor(this.month()/3)),this}function sc(a){return a=K(a),void 0===a||"millisecond"===a?this:("date"===a&&(a="day"),this.startOf(a).add(1,"isoWeek"===a?"week":a).subtract(1,"ms"))}function tc(){return this._d.valueOf()-6e4*(this._offset||0)}function uc(){return Math.floor(this.valueOf()/1e3)}function vc(){return new Date(this.valueOf())}function wc(){var a=this;return[a.year(),a.month(),a.date(),a.hour(),a.minute(),a.second(),a.millisecond()]}function xc(){var a=this;return{years:a.year(),months:a.month(),date:a.date(),hours:a.hours(),minutes:a.minutes(),seconds:a.seconds(),milliseconds:a.milliseconds()}}function yc(){return this.isValid()?this.toISOString():null}function zc(){return o(this)}function Ac(){ 7 | return k({},n(this))}function Bc(){return n(this).overflow}function Cc(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function Dc(a,b){U(0,[a,a.length],0,b)}function Ec(a){return Ic.call(this,a,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)}function Fc(a){return Ic.call(this,a,this.isoWeek(),this.isoWeekday(),1,4)}function Gc(){return xa(this.year(),1,4)}function Hc(){var a=this.localeData()._week;return xa(this.year(),a.dow,a.doy)}function Ic(a,b,c,d,e){var f;return null==a?wa(this,d,e).year:(f=xa(a,d,e),b>f&&(b=f),Jc.call(this,a,b,c,d,e))}function Jc(a,b,c,d,e){var f=va(a,b,c,d,e),g=ta(f.year,0,f.dayOfYear);return this.year(g.getUTCFullYear()),this.month(g.getUTCMonth()),this.date(g.getUTCDate()),this}function Kc(a){return null==a?Math.ceil((this.month()+1)/3):this.month(3*(a-1)+this.month()%3)}function Lc(a){var b=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==a?b:this.add(a-b,"d")}function Mc(a,b){b[ke]=u(1e3*("0."+a))}function Nc(){return this._isUTC?"UTC":""}function Oc(){return this._isUTC?"Coordinated Universal Time":""}function Pc(a){return tb(1e3*a)}function Qc(){return tb.apply(null,arguments).parseZone()}function Rc(a){return a}function Sc(a,b,c,d){var e=bb(),f=l().set(d,b);return e[c](f,a)}function Tc(a,b,c){if(g(a)&&(b=a,a=void 0),a=a||"",null!=b)return Sc(a,b,c,"month");var d,e=[];for(d=0;d<12;d++)e[d]=Sc(a,d,c,"month");return e}function Uc(a,b,c,d){"boolean"==typeof a?(g(b)&&(c=b,b=void 0),b=b||""):(b=a,c=b,a=!1,g(b)&&(c=b,b=void 0),b=b||"");var e=bb(),f=a?e._week.dow:0;if(null!=c)return Sc(b,(c+f)%7,d,"day");var h,i=[];for(h=0;h<7;h++)i[h]=Sc(b,(h+f)%7,d,"day");return i}function Vc(a,b){return Tc(a,b,"months")}function Wc(a,b){return Tc(a,b,"monthsShort")}function Xc(a,b,c){return Uc(a,b,c,"weekdays")}function Yc(a,b,c){return Uc(a,b,c,"weekdaysShort")}function Zc(a,b,c){return Uc(a,b,c,"weekdaysMin")}function $c(){var a=this._data;return this._milliseconds=df(this._milliseconds),this._days=df(this._days),this._months=df(this._months),a.milliseconds=df(a.milliseconds),a.seconds=df(a.seconds),a.minutes=df(a.minutes),a.hours=df(a.hours),a.months=df(a.months),a.years=df(a.years),this}function _c(a,b,c,d){var e=Sb(b,c);return a._milliseconds+=d*e._milliseconds,a._days+=d*e._days,a._months+=d*e._months,a._bubble()}function ad(a,b){return _c(this,a,b,1)}function bd(a,b){return _c(this,a,b,-1)}function cd(a){return a<0?Math.floor(a):Math.ceil(a)}function dd(){var a,b,c,d,e,f=this._milliseconds,g=this._days,h=this._months,i=this._data;return f>=0&&g>=0&&h>=0||f<=0&&g<=0&&h<=0||(f+=864e5*cd(fd(h)+g),g=0,h=0),i.milliseconds=f%1e3,a=t(f/1e3),i.seconds=a%60,b=t(a/60),i.minutes=b%60,c=t(b/60),i.hours=c%24,g+=t(c/24),e=t(ed(g)),h+=e,g-=cd(fd(e)),d=t(h/12),h%=12,i.days=g,i.months=h,i.years=d,this}function ed(a){return 4800*a/146097}function fd(a){return 146097*a/4800}function gd(a){if(!this.isValid())return NaN;var b,c,d=this._milliseconds;if(a=K(a),"month"===a||"year"===a)return b=this._days+d/864e5,c=this._months+ed(b),"month"===a?c:c/12;switch(b=this._days+Math.round(fd(this._months)),a){case"week":return b/7+d/6048e5;case"day":return b+d/864e5;case"hour":return 24*b+d/36e5;case"minute":return 1440*b+d/6e4;case"second":return 86400*b+d/1e3;case"millisecond":return Math.floor(864e5*b)+d;default:throw new Error("Unknown unit "+a)}}function hd(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*u(this._months/12):NaN}function id(a){return function(){return this.as(a)}}function jd(a){return a=K(a),this.isValid()?this[a+"s"]():NaN}function kd(a){return function(){return this.isValid()?this._data[a]:NaN}}function ld(){return t(this.days()/7)}function md(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function nd(a,b,c){var d=Sb(a).abs(),e=uf(d.as("s")),f=uf(d.as("m")),g=uf(d.as("h")),h=uf(d.as("d")),i=uf(d.as("M")),j=uf(d.as("y")),k=e<=vf.ss&&["s",e]||e0,k[4]=c,md.apply(null,k)}function od(a){return void 0===a?uf:"function"==typeof a&&(uf=a,!0)}function pd(a,b){return void 0!==vf[a]&&(void 0===b?vf[a]:(vf[a]=b,"s"===a&&(vf.ss=b-1),!0))}function qd(a){if(!this.isValid())return this.localeData().invalidDate();var b=this.localeData(),c=nd(this,!a,b);return a&&(c=b.pastFuture(+this,c)),b.postformat(c)}function rd(){if(!this.isValid())return this.localeData().invalidDate();var a,b,c,d=wf(this._milliseconds)/1e3,e=wf(this._days),f=wf(this._months);a=t(d/60),b=t(a/60),d%=60,a%=60,c=t(f/12),f%=12;var g=c,h=f,i=e,j=b,k=a,l=d,m=this.asSeconds();return m?(m<0?"-":"")+"P"+(g?g+"Y":"")+(h?h+"M":"")+(i?i+"D":"")+(j||k||l?"T":"")+(j?j+"H":"")+(k?k+"M":"")+(l?l+"S":""):"P0D"}var sd,td;td=Array.prototype.some?Array.prototype.some:function(a){for(var b=Object(this),c=b.length>>>0,d=0;d68?1900:2e3)};var te=O("FullYear",!0);U("w",["ww",2],"wo","week"),U("W",["WW",2],"Wo","isoWeek"),J("week","w"),J("isoWeek","W"),M("week",5),M("isoWeek",5),Z("w",Sd),Z("ww",Sd,Od),Z("W",Sd),Z("WW",Sd,Od),ca(["w","ww","W","WW"],function(a,b,c,d){b[d.substr(0,1)]=u(a)});var ue={dow:0,doy:6};U("d",0,"do","day"),U("dd",0,0,function(a){return this.localeData().weekdaysMin(this,a)}),U("ddd",0,0,function(a){return this.localeData().weekdaysShort(this,a)}),U("dddd",0,0,function(a){return this.localeData().weekdays(this,a)}),U("e",0,0,"weekday"),U("E",0,0,"isoWeekday"),J("day","d"),J("weekday","e"),J("isoWeekday","E"),M("day",11),M("weekday",11),M("isoWeekday",11),Z("d",Sd),Z("e",Sd),Z("E",Sd),Z("dd",function(a,b){return b.weekdaysMinRegex(a)}),Z("ddd",function(a,b){return b.weekdaysShortRegex(a)}),Z("dddd",function(a,b){return b.weekdaysRegex(a)}),ca(["dd","ddd","dddd"],function(a,b,c,d){var e=c._locale.weekdaysParse(a,d,c._strict);null!=e?b.d=e:n(c).invalidWeekday=a}),ca(["d","e","E"],function(a,b,c,d){b[d]=u(a)});var ve="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),we="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),xe="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),ye=be,ze=be,Ae=be;U("H",["HH",2],0,"hour"),U("h",["hh",2],0,Ra),U("k",["kk",2],0,Sa),U("hmm",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)}),U("hmmss",0,0,function(){return""+Ra.apply(this)+T(this.minutes(),2)+T(this.seconds(),2)}),U("Hmm",0,0,function(){return""+this.hours()+T(this.minutes(),2)}),U("Hmmss",0,0,function(){return""+this.hours()+T(this.minutes(),2)+T(this.seconds(),2)}),Ta("a",!0),Ta("A",!1),J("hour","h"),M("hour",13),Z("a",Ua),Z("A",Ua),Z("H",Sd),Z("h",Sd),Z("k",Sd),Z("HH",Sd,Od),Z("hh",Sd,Od),Z("kk",Sd,Od),Z("hmm",Td),Z("hmmss",Ud),Z("Hmm",Td),Z("Hmmss",Ud),ba(["H","HH"],he),ba(["k","kk"],function(a,b,c){var d=u(a);b[he]=24===d?0:d}),ba(["a","A"],function(a,b,c){c._isPm=c._locale.isPM(a),c._meridiem=a}),ba(["h","hh"],function(a,b,c){b[he]=u(a),n(c).bigHour=!0}),ba("hmm",function(a,b,c){var d=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d)),n(c).bigHour=!0}),ba("hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d,2)),b[je]=u(a.substr(e)),n(c).bigHour=!0}),ba("Hmm",function(a,b,c){var d=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d))}),ba("Hmmss",function(a,b,c){var d=a.length-4,e=a.length-2;b[he]=u(a.substr(0,d)),b[ie]=u(a.substr(d,2)),b[je]=u(a.substr(e))});var Be,Ce=/[ap]\.?m?\.?/i,De=O("Hours",!0),Ee={calendar:Bd,longDateFormat:Cd,invalidDate:Dd,ordinal:Ed,dayOfMonthOrdinalParse:Fd,relativeTime:Gd,months:pe,monthsShort:qe,week:ue,weekdays:ve,weekdaysMin:xe,weekdaysShort:we,meridiemParse:Ce},Fe={},Ge={},He=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Ie=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,Je=/Z|[+-]\d\d(?::?\d\d)?/,Ke=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Le=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Me=/^\/?Date\((\-?\d+)/i,Ne=/^((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d?\d\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?:\d\d)?\d\d\s)(\d\d:\d\d)(\:\d\d)?(\s(?:UT|GMT|[ECMP][SD]T|[A-IK-Za-ik-z]|[+-]\d{4}))$/;a.createFromInputFallback=x("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(a){a._d=new Date(a._i+(a._useUTC?" UTC":""))}),a.ISO_8601=function(){},a.RFC_2822=function(){};var Oe=x("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var a=tb.apply(null,arguments);return this.isValid()&&a.isValid()?athis?this:a:p()}),Qe=function(){return Date.now?Date.now():+new Date},Re=["year","quarter","month","week","day","hour","minute","second","millisecond"];Db("Z",":"),Db("ZZ",""),Z("Z",_d),Z("ZZ",_d),ba(["Z","ZZ"],function(a,b,c){c._useUTC=!0,c._tzm=Eb(_d,a)});var Se=/([\+\-]|\d\d)/gi;a.updateOffset=function(){};var Te=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Ue=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;Sb.fn=Ab.prototype,Sb.invalid=zb;var Ve=Wb(1,"add"),We=Wb(-1,"subtract");a.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",a.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var Xe=x("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(a){return void 0===a?this.localeData():this.locale(a)});U(0,["gg",2],0,function(){return this.weekYear()%100}),U(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Dc("gggg","weekYear"),Dc("ggggg","weekYear"),Dc("GGGG","isoWeekYear"),Dc("GGGGG","isoWeekYear"),J("weekYear","gg"),J("isoWeekYear","GG"),M("weekYear",1),M("isoWeekYear",1),Z("G",Zd),Z("g",Zd),Z("GG",Sd,Od),Z("gg",Sd,Od),Z("GGGG",Wd,Qd),Z("gggg",Wd,Qd),Z("GGGGG",Xd,Rd),Z("ggggg",Xd,Rd),ca(["gggg","ggggg","GGGG","GGGGG"],function(a,b,c,d){b[d.substr(0,2)]=u(a)}),ca(["gg","GG"],function(b,c,d,e){c[e]=a.parseTwoDigitYear(b)}),U("Q",0,"Qo","quarter"),J("quarter","Q"),M("quarter",7),Z("Q",Nd),ba("Q",function(a,b){b[fe]=3*(u(a)-1)}),U("D",["DD",2],"Do","date"),J("date","D"),M("date",9),Z("D",Sd),Z("DD",Sd,Od),Z("Do",function(a,b){return a?b._dayOfMonthOrdinalParse||b._ordinalParse:b._dayOfMonthOrdinalParseLenient}),ba(["D","DD"],ge),ba("Do",function(a,b){b[ge]=u(a.match(Sd)[0],10)});var Ye=O("Date",!0);U("DDD",["DDDD",3],"DDDo","dayOfYear"),J("dayOfYear","DDD"),M("dayOfYear",4),Z("DDD",Vd),Z("DDDD",Pd),ba(["DDD","DDDD"],function(a,b,c){c._dayOfYear=u(a)}),U("m",["mm",2],0,"minute"),J("minute","m"),M("minute",14),Z("m",Sd),Z("mm",Sd,Od),ba(["m","mm"],ie);var Ze=O("Minutes",!1);U("s",["ss",2],0,"second"),J("second","s"),M("second",15),Z("s",Sd),Z("ss",Sd,Od),ba(["s","ss"],je);var $e=O("Seconds",!1);U("S",0,0,function(){return~~(this.millisecond()/100)}),U(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),U(0,["SSS",3],0,"millisecond"),U(0,["SSSS",4],0,function(){return 10*this.millisecond()}),U(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),U(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),U(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),U(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),U(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),J("millisecond","ms"),M("millisecond",16),Z("S",Vd,Nd),Z("SS",Vd,Od),Z("SSS",Vd,Pd);var _e;for(_e="SSSS";_e.length<=9;_e+="S")Z(_e,Yd);for(_e="S";_e.length<=9;_e+="S")ba(_e,Mc);var af=O("Milliseconds",!1);U("z",0,0,"zoneAbbr"),U("zz",0,0,"zoneName");var bf=r.prototype;bf.add=Ve,bf.calendar=Zb,bf.clone=$b,bf.diff=fc,bf.endOf=sc,bf.format=kc,bf.from=lc,bf.fromNow=mc,bf.to=nc,bf.toNow=oc,bf.get=R,bf.invalidAt=Bc,bf.isAfter=_b,bf.isBefore=ac,bf.isBetween=bc,bf.isSame=cc,bf.isSameOrAfter=dc,bf.isSameOrBefore=ec,bf.isValid=zc,bf.lang=Xe,bf.locale=pc,bf.localeData=qc,bf.max=Pe,bf.min=Oe,bf.parsingFlags=Ac,bf.set=S,bf.startOf=rc,bf.subtract=We,bf.toArray=wc,bf.toObject=xc,bf.toDate=vc,bf.toISOString=ic,bf.inspect=jc,bf.toJSON=yc,bf.toString=hc,bf.unix=uc,bf.valueOf=tc,bf.creationData=Cc,bf.year=te,bf.isLeapYear=ra,bf.weekYear=Ec,bf.isoWeekYear=Fc,bf.quarter=bf.quarters=Kc,bf.month=ka,bf.daysInMonth=la,bf.week=bf.weeks=Ba,bf.isoWeek=bf.isoWeeks=Ca,bf.weeksInYear=Hc,bf.isoWeeksInYear=Gc,bf.date=Ye,bf.day=bf.days=Ka,bf.weekday=La,bf.isoWeekday=Ma,bf.dayOfYear=Lc,bf.hour=bf.hours=De,bf.minute=bf.minutes=Ze,bf.second=bf.seconds=$e,bf.millisecond=bf.milliseconds=af,bf.utcOffset=Hb,bf.utc=Jb,bf.local=Kb,bf.parseZone=Lb,bf.hasAlignedHourOffset=Mb,bf.isDST=Nb,bf.isLocal=Pb,bf.isUtcOffset=Qb,bf.isUtc=Rb,bf.isUTC=Rb,bf.zoneAbbr=Nc,bf.zoneName=Oc,bf.dates=x("dates accessor is deprecated. Use date instead.",Ye),bf.months=x("months accessor is deprecated. Use month instead",ka),bf.years=x("years accessor is deprecated. Use year instead",te),bf.zone=x("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",Ib),bf.isDSTShifted=x("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",Ob);var cf=C.prototype;cf.calendar=D,cf.longDateFormat=E,cf.invalidDate=F,cf.ordinal=G,cf.preparse=Rc,cf.postformat=Rc,cf.relativeTime=H,cf.pastFuture=I,cf.set=A,cf.months=fa,cf.monthsShort=ga,cf.monthsParse=ia,cf.monthsRegex=na,cf.monthsShortRegex=ma,cf.week=ya,cf.firstDayOfYear=Aa,cf.firstDayOfWeek=za,cf.weekdays=Fa,cf.weekdaysMin=Ha,cf.weekdaysShort=Ga,cf.weekdaysParse=Ja,cf.weekdaysRegex=Na,cf.weekdaysShortRegex=Oa,cf.weekdaysMinRegex=Pa,cf.isPM=Va,cf.meridiem=Wa,$a("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(a){var b=a%10,c=1===u(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),a.lang=x("moment.lang is deprecated. Use moment.locale instead.",$a),a.langData=x("moment.langData is deprecated. Use moment.localeData instead.",bb);var df=Math.abs,ef=id("ms"),ff=id("s"),gf=id("m"),hf=id("h"),jf=id("d"),kf=id("w"),lf=id("M"),mf=id("y"),nf=kd("milliseconds"),of=kd("seconds"),pf=kd("minutes"),qf=kd("hours"),rf=kd("days"),sf=kd("months"),tf=kd("years"),uf=Math.round,vf={ss:44,s:45,m:45,h:22,d:26,M:11},wf=Math.abs,xf=Ab.prototype;return xf.isValid=yb,xf.abs=$c,xf.add=ad,xf.subtract=bd,xf.as=gd,xf.asMilliseconds=ef,xf.asSeconds=ff,xf.asMinutes=gf,xf.asHours=hf,xf.asDays=jf,xf.asWeeks=kf,xf.asMonths=lf,xf.asYears=mf,xf.valueOf=hd,xf._bubble=dd,xf.get=jd,xf.milliseconds=nf,xf.seconds=of,xf.minutes=pf,xf.hours=qf,xf.days=rf,xf.weeks=ld,xf.months=sf,xf.years=tf,xf.humanize=qd,xf.toISOString=rd,xf.toString=rd,xf.toJSON=rd,xf.locale=pc,xf.localeData=qc,xf.toIsoString=x("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",rd),xf.lang=Xe,U("X",0,0,"unix"),U("x",0,0,"valueOf"),Z("x",Zd),Z("X",ae),ba("X",function(a,b,c){c._d=new Date(1e3*parseFloat(a,10))}),ba("x",function(a,b,c){c._d=new Date(u(a))}),a.version="2.18.1",b(tb),a.fn=bf,a.min=vb,a.max=wb,a.now=Qe,a.utc=l,a.unix=Pc,a.months=Vc,a.isDate=h,a.locale=$a,a.invalid=p,a.duration=Sb,a.isMoment=s,a.weekdays=Xc,a.parseZone=Qc,a.localeData=bb,a.isDuration=Bb,a.monthsShort=Wc,a.weekdaysMin=Zc,a.defineLocale=_a,a.updateLocale=ab,a.locales=cb,a.weekdaysShort=Yc,a.normalizeUnits=K,a.relativeTimeRounding=od,a.relativeTimeThreshold=pd,a.calendarFormat=Yb,a.prototype=bf,a}); --------------------------------------------------------------------------------