From 8cf04c4ae383a9c9ec908054287d950fcf2ea737 Mon Sep 17 00:00:00 2001 From: Doro Wu Date: Thu, 21 May 2015 14:09:11 +0800 Subject: [PATCH] Responsive x11 size --- Dockerfile | 9 +- nginx.conf | 124 ++-------- noVNC/vnc_auto.html | 2 +- startup.sh | 4 +- supervisord.conf | 12 +- web/lightop/__init__.py | 516 ++++------------------------------------ web/run.py | 18 +- 7 files changed, 84 insertions(+), 601 deletions(-) diff --git a/Dockerfile b/Dockerfile index 44b4814..0800f0f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:14.04 +FROM ubuntu:14.04.2 MAINTAINER Doro Wu ENV DEBIAN_FRONTEND noninteractive @@ -14,16 +14,19 @@ RUN apt-get update \ fonts-wqy-microhei \ language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw \ nginx \ - python-pip \ + python-pip python-dev build-essential \ && apt-get autoclean \ && apt-get autoremove \ && rm -rf /var/lib/apt/lists/* +ADD web /web/ +RUN pip install -r /web/requirements.txt ADD noVNC /noVNC/ ADD nginx.conf /etc/nginx/sites-enabled/default ADD startup.sh / -ADD supervisord.conf /etc/ +ADD supervisord.conf /etc/supervisor/conf.d/ + EXPOSE 6080 WORKDIR /root ENTRYPOINT ["/startup.sh"] diff --git a/nginx.conf b/nginx.conf index 7b781d4..3a42b86 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,22 +1,3 @@ -# You may add here your -# server { -# ... -# } -# statements for each of your virtual hosts to this file - -## -# You should look at the following URL's in order to grasp a solid understanding -# of Nginx configuration files in order to fully unleash the power of Nginx. -# http://wiki.nginx.org/Pitfalls -# http://wiki.nginx.org/QuickStart -# http://wiki.nginx.org/Configuration -# -# Generally, you will want to move this file somewhere, and start with a clean -# file but keep this around for reference. Or just disable in sites-enabled. -# -# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. -## - server { listen 6080 default_server; listen [::]:6080 default_server ipv6only=on; @@ -25,7 +6,23 @@ server { index index.html index.htm; location / { - try_files $uri $uri/ @proxy; + try_files $uri @proxy; + } + + location = / { + try_files $uri @proxy2; + } + + location = /redirect.html { + try_files $uri @proxy2; + } + + location @proxy2 { + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Host $host; + proxy_pass http://127.0.0.1:6079; + max_ranges 0; } location @proxy { @@ -36,93 +33,10 @@ server { max_ranges 0; } - location /websockify { - proxy_pass http://127.0.0.1:6081; + location = /websockify { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; + proxy_pass http://127.0.0.1:6081; } - -#location / { -# # First attempt to serve request as file, then -# # as directory, then fall back to displaying a 404. -# try_files $uri $uri/ =404; -# # Uncomment to enable naxsi on this location -# # include /etc/nginx/naxsi.rules -# } - - # Only for nginx-naxsi used with nginx-naxsi-ui : process denied requests - #location /RequestDenied { - # proxy_pass http://127.0.0.1:8080; - #} - - #error_page 404 /404.html; - - # redirect server error pages to the static page /50x.html - # - #error_page 500 502 503 504 /50x.html; - #location = /50x.html { - # root /usr/share/nginx/html; - #} - - # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 - # - #location ~ \.php$ { - # fastcgi_split_path_info ^(.+\.php)(/.+)$; - # # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini - # - # # With php5-cgi alone: - # fastcgi_pass 127.0.0.1:9000; - # # With php5-fpm: - # fastcgi_pass unix:/var/run/php5-fpm.sock; - # fastcgi_index index.php; - # include fastcgi_params; - #} - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} } - - -# another virtual host using mix of IP-, name-, and port-based configuration -# -#server { -# listen 8000; -# listen somename:8080; -# server_name somename alias another.alias; -# root html; -# index index.html index.htm; -# -# location / { -# try_files $uri $uri/ =404; -# } -#} - - -# HTTPS server -# -#server { -# listen 443; -# server_name localhost; -# -# root html; -# index index.html index.htm; -# -# ssl on; -# ssl_certificate cert.pem; -# ssl_certificate_key cert.key; -# -# ssl_session_timeout 5m; -# -# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2; -# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES"; -# ssl_prefer_server_ciphers on; -# -# location / { -# try_files $uri $uri/ =404; -# } -#} diff --git a/noVNC/vnc_auto.html b/noVNC/vnc_auto.html index ec18ab8..ea0b974 100644 --- a/noVNC/vnc_auto.html +++ b/noVNC/vnc_auto.html @@ -45,7 +45,7 @@
-
+
Loading diff --git a/startup.sh b/startup.sh index a36ce57..8beac11 100755 --- a/startup.sh +++ b/startup.sh @@ -9,4 +9,6 @@ PASS=ubuntu id -u ubuntu &>/dev/null || useradd --create-home --shell /bin/bash --user-group --groups adm,sudo ubuntu echo "ubuntu:$PASS" | chpasswd -/usr/bin/supervisord -c /etc/supervisord.conf -n +cd /web && ./run.py > /var/log/web.log 2>&1 & +nginx -c /etc/nginx/nginx.conf +/usr/bin/supervisord -n diff --git a/supervisord.conf b/supervisord.conf index 3ce76d6..f797aae 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -1,13 +1,3 @@ -[supervisord] -nodaemon=true -logfile_maxbytes=10MB -pidfile=/var/run/supervisord.pid -logfile=/var/log/supervisord.log -nodaemon=false - -[supervisorctl] -serverurl=unix:///var/run/supervisor.sock - [program:xvfb] priority=10 directory=/ @@ -45,7 +35,7 @@ redirect_stderr=true [program:novnc] priority=25 directory=/noVNC -command=/noVNC/utils/launch.sh +command=/noVNC/utils/launch.sh --listen 6081 user=root autostart=true autorestart=true diff --git a/web/lightop/__init__.py b/web/lightop/__init__.py index 73440f1..afdcb65 100644 --- a/web/lightop/__init__.py +++ b/web/lightop/__init__.py @@ -1,9 +1,6 @@ from flask import (Flask, request, - render_template, abort, - Response, - redirect, ) import os @@ -24,29 +21,14 @@ LoggingConfiguration.set( logging.DEBUG if os.getenv('DEBUG') else logging.INFO, 'lightop.log', name='Web') -from auth import auth -auth.init_app(app, app.config['PHASE']) - -from gevent import spawn, sleep -from geventwebsocket import WebSocketError -import requests -import websocket -import docker import json from functools import wraps import subprocess -import datetime -import sha -import re - -from db.sql import User as DbUser +import time -CHUNK_SIZE = 1024 -CID2IMAGE = {'ubuntu-trusty-ttyjs': 'dorowu/lightop-ubuntu-trusty-ttyjs', - 'ubuntu-trusty-lxde': 'dorowu/lightop-ubuntu-trusty-lxde'} -RE_OWNER_CNAME = re.compile('^/(.*)_({})$'.format('|'.join(CID2IMAGE.keys()))) +FIRST = True def exception_to_json(func): @@ -79,461 +61,63 @@ class BadRequest(Exception): pass +HTML_INDEX = ''' + + Page Redirection +''' + + +HTML_REDIRECT = ''' + + Page Redirection +''' + + @app.route('/') def index(): - return redirect("index.html") + return HTML_INDEX -@app.route('/') -@auth.login_required -def root(url): - logging.info("Root route, path: %s", url) - # If referred from a proxy request, then redirect to a URL with the proxy prefix. - # This allows server-relative and protocol-relative URLs to work. - proxy_ref = proxy_ref_info(request) - if proxy_ref: - redirect_url = "/p/%s/%s%s" % (proxy_ref[0], url, ("?" + request.query_string if request.query_string else "")) - logging.info("Redirecting referred URL to: %s", redirect_url) - return redirect(redirect_url) - # Otherwise, default behavior - return render_template('hello.html', name=url, request=request) +@app.route('/redirect.html') +def redirectme(): + global FIRST + if not FIRST: + return HTML_REDIRECT -@app.route('/u//') -@auth.login_required -def proxy_user_root(cid): - return proxy_user(cid, '') + env = {'width': 1024, 'height': 768} + if 'width' in request.args: + env['width'] = request.args['width'] + if 'height' in request.args: + env['height'] = request.args['height'] - -def container_create_and_network(cid): - user = auth.current_user.username() - cname = user + '_' + cid - dc = docker.Client() - # create container - for c in dc.containers(all=True): - #logging.info(str(c['Names'])) - if '/' + cname in c['Names']: - break - else: - if cid not in CID2IMAGE: - raise BadRequest(cid, 'not exist') - try: - os.makedirs('mnt/home/' + user) - except OSError: - pass - try: - os.makedirs('mnt/public') - except OSError: - pass - env = ['USER=' + user, 'PASS=' + user] - if 'width' in request.args: - env.append('WIDTH=' + str(request.args['width'])) - if 'height' in request.args: - env.append('HEIGHT=' + str(request.args['height'])) - logging.info('create container') - dc.create_container(CID2IMAGE[cid], name=cname, - volumes=['/home/' + user, '/mnt/public'], - environment=env) - cinfo = dc.inspect_container(user + '_' + cid) - # start container - logging.info(cinfo['State']['Running']) - if not cinfo['State']['Running']: - binds = {} - binds[os.path.join(os.getcwd(), 'mnt', 'home', user)] = {'bind': '/home/' + user, 'ro': False} - binds[os.path.join(os.getcwd(), 'mnt', 'public')] = {'bind': '/mnt/public', 'ro': False} - logging.info('start container') - dc.start(cname, binds=binds) - cinfo = dc.inspect_container(user + '_' + cid) - # get ip and port - ipaddr = cinfo['NetworkSettings']['IPAddress'] - for p in cinfo['NetworkSettings']['Ports'].keys(): - port, proto = p.split('/') - if port != '22': - port = int(port) - break - else: - raise RuntimeError('port', cinfo['NetworkSettings']['Ports']) - - start = datetime.datetime.now() - while True: - now = datetime.datetime.now() - if (now - start).total_seconds() > 10: - logging.error('probe failed') - raise RuntimeError('probe failed') - try: - r = requests.get('http://{}:{}/'.format(ipaddr, port)) - if 200 <= r.status_code < 400: - break - except Exception: - pass - sleep(1) - return ipaddr, port - - -# hijact LXDE VNC -@app.route('/u/ubuntu-trusty-lxde/') -@auth.login_required -def proxy_user_vnc(path): - logging.info('vnc ' + path) - # auth pass - if path.endswith('/websockify'): - logging.info('vnc done') - return '' - if path != 'vnc_auto.html': - return proxy_user('ubuntu-trusty-lxde', path) - if len(request.args.get('hijact', '')) >= 1: - return proxy_user('ubuntu-trusty-lxde', path) - ipaddr, port = container_create_and_network('ubuntu-trusty-lxde') - user = auth.current_user.username() - #TODO remove when user deleted - subprocess.check_call(r"sed -i '/^location .*ubuntu-trusty-lxde\/{user}\//,/}}/d' nginx/ws-login.conf".format(user=user), + # sed + subprocess.check_call(r"sed -i 's#^command=/usr/bin/Xvfb.*$#command=/usr/bin/Xvfb :1 -screen 0 {width}x{height}x16#' /etc/supervisor/conf.d/supervisord.conf".format(**env), shell=True) - with open('nginx/ws-login.conf', 'a+') as f: - f.write('\nlocation /u/ubuntu-trusty-lxde/{user}/websockify\n' - '{{\n' - ' auth_request /login_refresh_code;\n' - ' proxy_pass http://{ipaddr}:{port}/websockify;\n' - ' proxy_redirect off;\n' - ' proxy_buffering off;\n' - ' proxy_set_header Host $host;\n' - ' proxy_set_header X-Real-IP $remote_addr;\n' - ' proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n' - ' proxy_http_version 1.1;\n' - ' proxy_set_header Upgrade $http_upgrade;\n' - ' proxy_set_header Connection "Upgrade";\n' - '}}'.format(user=user, ipaddr=ipaddr, port=port) - ) - subprocess.check_call('sudo nginx -c ' + os.getcwd() + - '/nginx.conf -s reload', shell=True) + # supervisorctrl reload + subprocess.check_call(r"supervisorctl reload", shell=True) - geometry = '' - geometry += '&width=' + request.args.get('width', '1024') - geometry += '&height=' + request.args.get('height', '768') - return redirect('/u/ubuntu-trusty-lxde/' + - 'vnc.html' + - ('?host={host}&port={port}&path={path}' - '&hijact=1&autoconnect=1{geometry}').format( - host=re.findall('https?://([^/:]+)([:0-9]*)/', request.url_root)[0][0], - port=6051, - path='u/ubuntu-trusty-lxde/' + user + '/websockify', - geometry=geometry) - ) - - -@app.route('/u//') -@auth.login_required -def proxy_user(cid, path): - try: - ipaddr, port = container_create_and_network(cid) - except docker.errors.APIError as e: - return json.dumps({'status': 400, 'message': str(e)}) - except RuntimeError as e: - return json.dumps({'status': 500, 'message': str(e)}) - - # websocket - if request.environ.get('wsgi.websocket'): - return proxy_user_websocket(ipaddr, port, path) - - # page - url = '%s:%d' % (ipaddr, port) - if len(path) > 0: - url += '/' + path - r = get_source_rsp(url) - logging.info("Got %s response from %s", r.status_code, url) - headers = dict(r.headers) - - def generate(): - for chunk in r.iter_content(CHUNK_SIZE): - yield chunk - return Response(generate(), headers=headers) - - -@app.route("/session", methods=["GET"]) -@exception_to_json -def sessions(): - return json.dumps(CID2IMAGE.keys()) - - -@app.route("/user/", methods=["GET"]) -@exception_to_json -@auth.login_required -def users(): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - result = [] - for u in DbUser.select(): - result.append({'name': u.user, - 'id': u.id, - 'volume': ['/mnt/public', '/home/' + u.user]}) - return json.dumps(result) - - -@app.route("/user/", methods=["POST"]) -@exception_to_json -@auth.login_required -def user_create(): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - try: - username = request.form['username'] - password = request.form['password'] - except KeyError: - raise BadRequest('username or password') - u = DbUser.create(user=username, password=sha.new(password).hexdigest()) - return user_detail(u.id) - - -@app.route("/user/", methods=["GET"]) -@exception_to_json -@auth.login_required -def user_detail(uid): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - try: - u = DbUser.get(DbUser.id == uid) - except: - return '{}' - return json.dumps({'name': u.user, - 'id': u.id, - 'volume': ['/mnt/public', '/home/' + u.user]}) - - -@app.route("/user/", methods=["DELETE"]) -@exception_to_json -@auth.login_required -def user_delete(uid): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - try: - u = DbUser.get(DbUser.id == uid) - except: - return json.dumps({'num': 0}) - return json.dumps({'num': u.delete_instance()}) - - -@app.route("/login", methods=["POST", "PUT"]) -@exception_to_json -def login(): - """Login - """ - try: - kargs = dict() - kargs['username'] = request.form['username'] - kargs['password'] = request.form['password'] - kargs['remember'] = request.form['remember'] \ - if 'remember' in request.form else False - except KeyError: - raise BadRequest('username, password or sid') - user = auth.login(**kargs) - if user is not None: - if user.is_anonymous(): - return json.dumps({'username': 'nobody', - 'isAdmin': False, - 'anonymous': True}) - return json.dumps({'username': user.username(), - 'isAdmin': user.is_admin()}) - raise PermissionDenied('Wrong user name or password') - - -@app.route("/container/", methods=["GET"]) -@exception_to_json -@auth.login_required -def containers(): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - result = [] - dc = docker.Client() - for c in dc.containers(): - r = RE_OWNER_CNAME.match(c['Names'][0]) - if r is None: - continue - result.append({'id': c['Id'], - 'session': r.group(2), - 'owner': r.group(1)}) - return json.dumps(result) - - -@app.route("/container/", methods=["DELETE"]) -@exception_to_json -@auth.login_required -def container_delete(cid): - user = auth.current_user.username() - if user != 'admin': - raise PermissionDenied('admin only') - try: - dc = docker.Client() - logging.info(cid) - c = dc.inspect_container(cid) - r = RE_OWNER_CNAME.match(c['Name']) - if r is None: - raise RuntimeError() - dc.kill(cid) - dc.remove_container(cid) - except Exception as e: - logging.error(str(e)) - return json.dumps({'num': 0}) - return json.dumps({'num': 1}) - - -@app.route("/login_refresh", methods=["GET"]) -@exception_to_json -@auth.login_required -def login_refresh(): - """Refresh token - """ - user = auth.current_user - #raise PermissionDenied('Not a valid user') - return json.dumps({'username': user.username(), 'isAdmin': False}) - - -@app.route("/login_refresh_code", methods=["GET"]) -@exception_to_json -@auth.login_required -def login_refresh_code(): - """Refresh token - """ - logging.info('!!!!!!!!!!!!!!!!! refresh code') - user = auth.current_user - #raise PermissionDenied('Not a valid user') - return json.dumps({'username': user.username(), 'isAdmin': False}) - - -@app.route("/logout", methods=["PUT"]) -@exception_to_json -@auth.login_required -def logout(): - """Logout - """ - try: - username = auth.current_user.username() - except KeyError: - return json.dumps({'error': {'code': 400}}) - if auth.logout(username): - return json.dumps({'username': username}) - return json.dumps({'error': {'code': 403}}) - - -def proxy_user_websocket(ipaddr, port, path): - def c2s(client, server): - while True: - inp = client.receive() - if inp is None: - raise WebSocketError() - server.send(inp) - - def get_headers(): - headers = [] - #for header in request.environ: - # if not header.startswith('HTTP_'): - # continue - # if not header.startswith('HTTP_SEC_') \ - # and not header.startswith('HTTP_ACCEPT_') \ - # and not header.startswith('HTTP_USER_AGENT'): - # continue - # upper = True - # k = '' - # for c in header[5:].replace('_', '-').lower(): - # if upper: - # k += c.upper() - # upper = False - # else: - # k += c - # if c == '-': - # upper = True - # headers.append('%s: %s' % (k, request.environ[header])) - return headers - - #https://stackoverflow.com/questions/18240358/html5-websocket-connecting-to-python - client = request.environ['wsgi.websocket'] - url = '%s:%d' % (ipaddr, port) - if len(path) > 0: - url += '/' + path - logging.info('websocket: ' + url) - headers = [] - #headers = get_headers() - #logging.info('headers: ' + str(headers)) - server = websocket.create_connection("ws://" + url, header=headers) - try: - spawn(c2s, client, server) - while True: - inp = server.recv() - if inp is None: - raise WebSocketError() - client.send(inp) - except WebSocketError as e: - logging.error(e) - except client.WebSocketConnectionClosedException: - pass - return json.dumps({'status': 200}) - - -def get_source_rsp(url): - url = 'http://%s' % url - logging.info("Fetching %s", url) - # Ensure the URL is approved, else abort - if not is_approved(url): - logging.warn("URL is not approved: %s", url) - abort(403) - # Pass original Referer for subsequent resource requests - proxy_ref = proxy_ref_info(request) - headers = {"Referer": "http://%s/%s" % (proxy_ref[0], proxy_ref[1])} if proxy_ref else {} - # Fetch the URL, and stream it back - logging.info("Fetching with headers: %s, %s", url, headers) - return requests.get(url, stream=True, params=request.args, headers=headers) - - -def is_approved(url): - return True - - -def split_url(url): - """Splits the given URL into a tuple of (protocol, host, uri)""" - proto, rest = url.split(':', 1) - rest = rest[2:].split('/', 1) - host, uri = (rest[0], rest[1]) if len(rest) == 2 else (rest[0], "") - return (proto, host, uri) - - -def proxy_ref_info(request): - """Parses out Referer info indicating the request is from a previously proxied page. - - For example, if: - Referer: http://localhost:8080/p/google.com/search?q=foo - then the result is: - ("google.com", "search?q=foo") - """ - ref = request.headers.get('referer') - if ref: - _, _, uri = split_url(ref) - if uri.find("/") < 0: - return None - first, rest = uri.split("/", 1) - if first in "pd": - parts = rest.split("/", 1) - r = (parts[0], parts[1]) if len(parts) == 2 else (parts[0], "") - logging.info("Referred by proxy host, uri: %s, %s", r[0], r[1]) - return r - return None - - -for image in CID2IMAGE.values(): - image = image.split(':')[0] - cmd = 'docker images | grep -q "^{0} " || docker pull {0}'.format(image) - logging.info(cmd) - subprocess.check_call(cmd, shell=True) - - -try: - os.makedirs('nginx') -except: - pass -with open('nginx/ws-login.conf', 'w+') as f: - f.truncate() + # check all running + for i in xrange(20): + output = subprocess.check_output(r"supervisorctl status | grep RUNNING | wc -l", shell=True) + if output.strip() == "4": + FIRST = False + return HTML_REDIRECT + time.sleep(2) + abort(500, 'service is not ready, please restart container') if __name__ == '__main__': diff --git a/web/run.py b/web/run.py index ae5656a..7950aef 100755 --- a/web/run.py +++ b/web/run.py @@ -92,20 +92,11 @@ def main(): create_instance_config() def run_server(): - from gevent import monkey - monkey.patch_all(subprocess=True) - from gevent.wsgi import WSGIServer import socket - from geventwebsocket.handler import WebSocketHandler os.environ['CONFIG'] = CONFIG from lightop import app - if not DEBUG: # run on NAS - from werkzeug import SharedDataMiddleware - app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { - '/': os.path.join(os.path.dirname(__file__), 'static') - }) # websocket conflict: WebSocketHandler if DEBUG or STAGING: # from werkzeug.debug import DebuggedApplication @@ -123,9 +114,7 @@ def main(): print('Running on port ' + str(PORT)) try: - http_server = WSGIServer(('', PORT), app, - handler_class=WebSocketHandler) - http_server.serve_forever() + app.run(host='', port=PORT) except socket.error as e: print(e) @@ -133,8 +122,9 @@ def main(): STAGING = True if '--staging' in sys.argv else False CONFIG = 'config.Development' if DEBUG else 'config.Production' CONFIG = 'config.Staging' if STAGING else CONFIG - PORT = 6050 - PROGRAMS = (('sudo nginx -c ${PWD}/nginx.conf'),) + PORT = 6079 + PROGRAMS = tuple() + #PROGRAMS = (('sudo nginx -c ${PWD}/nginx.conf'),) #PROGRAMS = ('python lxc-monitor.py', # 'python docker-monitor.py') signal.signal(signal.SIGCHLD, signal.SIG_IGN)