mirror of
https://github.com/fcwu/docker-ubuntu-vnc-desktop
synced 2025-12-06 08:22:31 +01:00
Responsive x11 size
This commit is contained in:
parent
5d706b6e1e
commit
8cf04c4ae3
7 changed files with 84 additions and 601 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
FROM ubuntu:14.04
|
FROM ubuntu:14.04.2
|
||||||
MAINTAINER Doro Wu <fcwu.tw@gmail.com>
|
MAINTAINER Doro Wu <fcwu.tw@gmail.com>
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
|
@ -14,16 +14,19 @@ RUN apt-get update \
|
||||||
fonts-wqy-microhei \
|
fonts-wqy-microhei \
|
||||||
language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw \
|
language-pack-zh-hant language-pack-gnome-zh-hant firefox-locale-zh-hant libreoffice-l10n-zh-tw \
|
||||||
nginx \
|
nginx \
|
||||||
python-pip \
|
python-pip python-dev build-essential \
|
||||||
&& apt-get autoclean \
|
&& apt-get autoclean \
|
||||||
&& apt-get autoremove \
|
&& apt-get autoremove \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ADD web /web/
|
||||||
|
RUN pip install -r /web/requirements.txt
|
||||||
|
|
||||||
ADD noVNC /noVNC/
|
ADD noVNC /noVNC/
|
||||||
ADD nginx.conf /etc/nginx/sites-enabled/default
|
ADD nginx.conf /etc/nginx/sites-enabled/default
|
||||||
ADD startup.sh /
|
ADD startup.sh /
|
||||||
ADD supervisord.conf /etc/
|
ADD supervisord.conf /etc/supervisor/conf.d/
|
||||||
|
|
||||||
EXPOSE 6080
|
EXPOSE 6080
|
||||||
WORKDIR /root
|
WORKDIR /root
|
||||||
ENTRYPOINT ["/startup.sh"]
|
ENTRYPOINT ["/startup.sh"]
|
||||||
|
|
|
||||||
124
nginx.conf
124
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 {
|
server {
|
||||||
listen 6080 default_server;
|
listen 6080 default_server;
|
||||||
listen [::]:6080 default_server ipv6only=on;
|
listen [::]:6080 default_server ipv6only=on;
|
||||||
|
|
@ -25,7 +6,23 @@ server {
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
|
|
||||||
location / {
|
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 {
|
location @proxy {
|
||||||
|
|
@ -36,93 +33,10 @@ server {
|
||||||
max_ranges 0;
|
max_ranges 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /websockify {
|
location = /websockify {
|
||||||
proxy_pass http://127.0.0.1:6081;
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "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;
|
|
||||||
# }
|
|
||||||
#}
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
<body style="margin: 0px;">
|
<body style="margin: 0px;">
|
||||||
<div id="noVNC_screen">
|
<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>
|
<table border=0 width="100%"><tr>
|
||||||
<td><div id="noVNC_status" style="position: relative; height: auto;">
|
<td><div id="noVNC_status" style="position: relative; height: auto;">
|
||||||
Loading
|
Loading
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,6 @@ PASS=ubuntu
|
||||||
id -u ubuntu &>/dev/null || useradd --create-home --shell /bin/bash --user-group --groups adm,sudo ubuntu
|
id -u ubuntu &>/dev/null || useradd --create-home --shell /bin/bash --user-group --groups adm,sudo ubuntu
|
||||||
echo "ubuntu:$PASS" | chpasswd
|
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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
[program:xvfb]
|
||||||
priority=10
|
priority=10
|
||||||
directory=/
|
directory=/
|
||||||
|
|
@ -45,7 +35,7 @@ redirect_stderr=true
|
||||||
[program:novnc]
|
[program:novnc]
|
||||||
priority=25
|
priority=25
|
||||||
directory=/noVNC
|
directory=/noVNC
|
||||||
command=/noVNC/utils/launch.sh
|
command=/noVNC/utils/launch.sh --listen 6081
|
||||||
user=root
|
user=root
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
from flask import (Flask,
|
from flask import (Flask,
|
||||||
request,
|
request,
|
||||||
render_template,
|
|
||||||
abort,
|
abort,
|
||||||
Response,
|
|
||||||
redirect,
|
|
||||||
)
|
)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
@ -24,29 +21,14 @@ LoggingConfiguration.set(
|
||||||
logging.DEBUG if os.getenv('DEBUG') else logging.INFO,
|
logging.DEBUG if os.getenv('DEBUG') else logging.INFO,
|
||||||
'lightop.log', name='Web')
|
'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
|
import json
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import subprocess
|
import subprocess
|
||||||
import datetime
|
import time
|
||||||
import sha
|
|
||||||
import re
|
|
||||||
|
|
||||||
from db.sql import User as DbUser
|
|
||||||
|
|
||||||
|
|
||||||
CHUNK_SIZE = 1024
|
FIRST = True
|
||||||
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())))
|
|
||||||
|
|
||||||
|
|
||||||
def exception_to_json(func):
|
def exception_to_json(func):
|
||||||
|
|
@ -79,461 +61,63 @@ class BadRequest(Exception):
|
||||||
pass
|
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('/')
|
@app.route('/')
|
||||||
def index():
|
def index():
|
||||||
return redirect("index.html")
|
return HTML_INDEX
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<path:url>')
|
@app.route('/redirect.html')
|
||||||
@auth.login_required
|
def redirectme():
|
||||||
def root(url):
|
global FIRST
|
||||||
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)
|
|
||||||
|
|
||||||
|
if not FIRST:
|
||||||
|
return HTML_REDIRECT
|
||||||
|
|
||||||
@app.route('/u/<cid>/')
|
env = {'width': 1024, 'height': 768}
|
||||||
@auth.login_required
|
|
||||||
def proxy_user_root(cid):
|
|
||||||
return proxy_user(cid, '')
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
if 'width' in request.args:
|
||||||
env.append('WIDTH=' + str(request.args['width']))
|
env['width'] = request.args['width']
|
||||||
if 'height' in request.args:
|
if 'height' in request.args:
|
||||||
env.append('HEIGHT=' + str(request.args['height']))
|
env['height'] = 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()
|
# sed
|
||||||
while True:
|
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),
|
||||||
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),
|
|
||||||
shell=True)
|
shell=True)
|
||||||
with open('nginx/ws-login.conf', 'a+') as f:
|
# supervisorctrl reload
|
||||||
f.write('\nlocation /u/ubuntu-trusty-lxde/{user}/websockify\n'
|
subprocess.check_call(r"supervisorctl reload", shell=True)
|
||||||
'{{\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)
|
|
||||||
|
|
||||||
geometry = ''
|
# check all running
|
||||||
geometry += '&width=' + request.args.get('width', '1024')
|
for i in xrange(20):
|
||||||
geometry += '&height=' + request.args.get('height', '768')
|
output = subprocess.check_output(r"supervisorctl status | grep RUNNING | wc -l", shell=True)
|
||||||
return redirect('/u/ubuntu-trusty-lxde/' +
|
if output.strip() == "4":
|
||||||
'vnc.html' +
|
FIRST = False
|
||||||
('?host={host}&port={port}&path={path}'
|
return HTML_REDIRECT
|
||||||
'&hijact=1&autoconnect=1{geometry}').format(
|
time.sleep(2)
|
||||||
host=re.findall('https?://([^/:]+)([:0-9]*)/', request.url_root)[0][0],
|
abort(500, 'service is not ready, please restart container')
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
18
web/run.py
18
web/run.py
|
|
@ -92,20 +92,11 @@ def main():
|
||||||
create_instance_config()
|
create_instance_config()
|
||||||
|
|
||||||
def run_server():
|
def run_server():
|
||||||
from gevent import monkey
|
|
||||||
monkey.patch_all(subprocess=True)
|
|
||||||
from gevent.wsgi import WSGIServer
|
|
||||||
import socket
|
import socket
|
||||||
from geventwebsocket.handler import WebSocketHandler
|
|
||||||
|
|
||||||
os.environ['CONFIG'] = CONFIG
|
os.environ['CONFIG'] = CONFIG
|
||||||
from lightop import app
|
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
|
# websocket conflict: WebSocketHandler
|
||||||
if DEBUG or STAGING:
|
if DEBUG or STAGING:
|
||||||
# from werkzeug.debug import DebuggedApplication
|
# from werkzeug.debug import DebuggedApplication
|
||||||
|
|
@ -123,9 +114,7 @@ def main():
|
||||||
|
|
||||||
print('Running on port ' + str(PORT))
|
print('Running on port ' + str(PORT))
|
||||||
try:
|
try:
|
||||||
http_server = WSGIServer(('', PORT), app,
|
app.run(host='', port=PORT)
|
||||||
handler_class=WebSocketHandler)
|
|
||||||
http_server.serve_forever()
|
|
||||||
except socket.error as e:
|
except socket.error as e:
|
||||||
print(e)
|
print(e)
|
||||||
|
|
||||||
|
|
@ -133,8 +122,9 @@ def main():
|
||||||
STAGING = True if '--staging' in sys.argv else False
|
STAGING = True if '--staging' in sys.argv else False
|
||||||
CONFIG = 'config.Development' if DEBUG else 'config.Production'
|
CONFIG = 'config.Development' if DEBUG else 'config.Production'
|
||||||
CONFIG = 'config.Staging' if STAGING else CONFIG
|
CONFIG = 'config.Staging' if STAGING else CONFIG
|
||||||
PORT = 6050
|
PORT = 6079
|
||||||
PROGRAMS = (('sudo nginx -c ${PWD}/nginx.conf'),)
|
PROGRAMS = tuple()
|
||||||
|
#PROGRAMS = (('sudo nginx -c ${PWD}/nginx.conf'),)
|
||||||
#PROGRAMS = ('python lxc-monitor.py',
|
#PROGRAMS = ('python lxc-monitor.py',
|
||||||
# 'python docker-monitor.py')
|
# 'python docker-monitor.py')
|
||||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue