Responsive x11 size

This commit is contained in:
Doro Wu 2015-05-21 14:09:11 +08:00
parent 5d706b6e1e
commit 8cf04c4ae3
7 changed files with 84 additions and 601 deletions

View file

@ -1,4 +1,4 @@
FROM ubuntu:14.04
FROM ubuntu:14.04.2
MAINTAINER Doro Wu <fcwu.tw@gmail.com>
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"]

View file

@ -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;
# }
#}

View file

@ -45,7 +45,7 @@
<body style="margin: 0px;">
<div id="noVNC_screen">
<div id="noVNC_status_bar" class="noVNC_status_bar" style="margin-top: 0px;">
<div id="noVNC_status_bar" class="noVNC_status_bar" style="margin-top: 0px; height: 0px;">
<table border=0 width="100%"><tr>
<td><div id="noVNC_status" style="position: relative; height: auto;">
Loading

View file

@ -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

View file

@ -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

View file

@ -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 = '''<html><head>
<script type="text/javascript">
var w = window,
d = document,
e = d.documentElement,
g = d.getElementsByTagName('body')[0],
x = w.innerWidth || e.clientWidth || g.clientWidth,
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
window.location.href = "redirect.html?width=" + x + "&height=" + (parseInt(y));
</script>
<title>Page Redirection</title>
</head><body></body></html>'''
HTML_REDIRECT = '''<html><head>
<script type="text/javascript">
var port = window.location.port;
if (!port)
port = window.location.protocol[4] == 's' ? 443 : 80;
window.location.href = "vnc_auto.html";
</script>
<title>Page Redirection</title>
</head><body></body></html>'''
@app.route('/')
def index():
return redirect("index.html")
return HTML_INDEX
@app.route('/<path:url>')
@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/<cid>/')
@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/<path:path>')
@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/<cid>/<path:path>')
@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/<int:uid>", 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/<int:uid>", 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/<string:cid>", 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__':

View file

@ -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)