mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
I want to be able to use this class in galaxy.model and it doesn't have any dependnecies on "web" stuff so I'd like to move it out of galaxy.model and into galaxy.security. galaxy.model shouldn't have dependencies on galaxy.web (and doesn't after the recent 976f5ad367), so this pre-emptively ensures it doesn't require these dependencies for https://github.com/galaxyproject/galaxy/pull/7367.
This has the benefit of also eliminating any galaxy.web dependencies from the generic test base file (test/base/testcase.py) which has long been a goal of mine as well.
There are places we legimately use ID encoding and decoding for security (i.e. job files API) but for the most part it doesn't provide security for API endpoints and this causes confusion repeatedly, so I've started the process of renaming the contained class SecurityHelper to something I feel makes it clearer this is just about encoding and decoding IDs.
225 lines
7.1 KiB
Python
Executable File
225 lines
7.1 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
Server for realtime communication between Galaxy uers.
|
|
|
|
At first you need to install a few requirements.
|
|
|
|
. GALAXY_ROOT/.venv/bin/activate # activate Galaxy's virtualenv
|
|
pip install flask flask-login flask-socketio eventlet # install the requirements
|
|
|
|
As a next step start the communication server with something like this:
|
|
|
|
./scripts/communication/communication_server.py --port 7070 --host localhost
|
|
|
|
Please make sure the host and the port matches the ones in ./config/galaxy.ini and
|
|
to set the `secret_id`.
|
|
|
|
This communication server can be controlled on three different levels:
|
|
1. The admin can activate/deactivate the communication server via ./config/galaxy.ini. [off by default]
|
|
2. The user can activate/deactivate it in their own personal-settings via Galaxy user preferences. [off by default]
|
|
3. The user can activate/deactivate communications for a session directly in the communication window. [on by default]
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import sys
|
|
from datetime import timedelta
|
|
from functools import update_wrapper
|
|
|
|
import flask.ext.login as flask_login
|
|
import six
|
|
from flask import (
|
|
current_app,
|
|
Flask,
|
|
make_response,
|
|
request,
|
|
send_file
|
|
)
|
|
from flask.ext.login import current_user
|
|
from flask_socketio import (
|
|
disconnect,
|
|
emit,
|
|
join_room,
|
|
leave_room,
|
|
SocketIO
|
|
)
|
|
|
|
sys.path.insert(1, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, 'lib')))
|
|
|
|
import galaxy.config
|
|
from galaxy.security.idencoding import IdEncodingHelper
|
|
from galaxy.util.sanitize_html import sanitize_html
|
|
from galaxy.util.script import app_properties_from_args, populate_config_args
|
|
|
|
logging.basicConfig()
|
|
log = logging.getLogger(__name__)
|
|
|
|
parser = argparse.ArgumentParser(description='Real-time communication server for Galaxy.')
|
|
parser.add_argument('--port', type=int, default="7070", help='Port number on which the server should run.')
|
|
parser.add_argument('--host', default='localhost', help='Hostname of the communication server.')
|
|
populate_config_args(parser)
|
|
args = parser.parse_args()
|
|
|
|
# With the config file we can load the full app properties
|
|
app_properties = app_properties_from_args(args)
|
|
|
|
config = galaxy.config.Configuration(**app_properties)
|
|
model = galaxy.config.init_models_from_config(config)
|
|
sa_session = model.context.current
|
|
|
|
# We need the ID secret for configuring the security helper to decrypt
|
|
# galaxysession cookies.
|
|
if "id_secret" not in app_properties:
|
|
log.warning('No ID_SECRET specified. Please set the "id_secret" in your galaxy.ini.')
|
|
|
|
id_secret = app_properties.get('id_secret', 'dangerous_default')
|
|
|
|
security_helper = IdEncodingHelper(id_secret=id_secret)
|
|
# And get access to the models
|
|
# Login manager to manage current_user functionality
|
|
login_manager = flask_login.LoginManager()
|
|
|
|
app = Flask(__name__)
|
|
app.config['SECRET_KEY'] = id_secret
|
|
login_manager.init_app(app)
|
|
socketio = SocketIO(app)
|
|
|
|
|
|
@login_manager.request_loader
|
|
def findUserByCookie(request):
|
|
cookie_value = request.cookies.get('galaxysession')
|
|
if not cookie_value:
|
|
return None
|
|
|
|
session_key = security_helper.decode_guid(cookie_value)
|
|
user_session = sa_session.query(model.GalaxySession).filter_by(session_key=session_key).first()
|
|
|
|
if user_session:
|
|
return user_session.user
|
|
|
|
return None
|
|
|
|
|
|
# Taken from flask.pocoo.org/snippets/56/
|
|
def crossdomain(origin=None, methods=None, headers=None,
|
|
max_age=21600, attach_to_all=True,
|
|
automatic_options=True):
|
|
if methods is not None:
|
|
methods = ', '.join(sorted(x.upper() for x in methods))
|
|
if headers is not None and not isinstance(headers, six.string_types):
|
|
headers = ', '.join(x.upper() for x in headers)
|
|
if not isinstance(origin, six.string_types):
|
|
origin = ', '.join(origin)
|
|
if isinstance(max_age, timedelta):
|
|
max_age = max_age.total_seconds()
|
|
|
|
def get_methods():
|
|
if methods is not None:
|
|
return methods
|
|
|
|
options_resp = current_app.make_default_options_response()
|
|
return options_resp.headers['allow']
|
|
|
|
def decorator(f):
|
|
def wrapped_function(*args, **kwargs):
|
|
if automatic_options and request.method == 'OPTIONS':
|
|
resp = current_app.make_default_options_response()
|
|
else:
|
|
resp = make_response(f(*args, **kwargs))
|
|
if not attach_to_all and request.method != 'OPTIONS':
|
|
return resp
|
|
|
|
h = resp.headers
|
|
|
|
h['Access-Control-Allow-Origin'] = origin
|
|
h['Access-Control-Allow-Methods'] = get_methods()
|
|
h['Access-Control-Max-Age'] = str(max_age)
|
|
if headers is not None:
|
|
h['Access-Control-Allow-Headers'] = headers
|
|
return resp
|
|
|
|
f.provide_automatic_options = False
|
|
return update_wrapper(wrapped_function, f)
|
|
return decorator
|
|
|
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
communication_directory = os.path.join(script_dir, 'template')
|
|
|
|
|
|
@app.route('/')
|
|
@crossdomain(origin='*')
|
|
def index():
|
|
return send_file(os.path.join(communication_directory, 'communication.html'))
|
|
|
|
|
|
@app.route('/communication.js')
|
|
def static_script():
|
|
return send_file(os.path.join(communication_directory, 'communication.js'))
|
|
|
|
|
|
@app.route('/communication.css')
|
|
def static_style():
|
|
return send_file(os.path.join(communication_directory, 'communication.css'))
|
|
|
|
|
|
@socketio.on('event connect', namespace='/chat')
|
|
def event_connect(message):
|
|
log.info("%s connected" % (current_user.username,))
|
|
|
|
|
|
@socketio.on('event broadcast', namespace='/chat')
|
|
def event_broadcast(message):
|
|
message = sanitize_html(message['data'])
|
|
|
|
log.debug("%s broadcast '%s'" % (current_user.username, message))
|
|
|
|
emit('event response',
|
|
{'data': message, 'user': current_user.username, 'gravatar': hashlib.md5(current_user.email).hexdigest()}, broadcast=True)
|
|
|
|
|
|
@socketio.on('event room', namespace='/chat')
|
|
def send_room_message(message):
|
|
data = sanitize_html(message['data'])
|
|
room = sanitize_html(message['room'])
|
|
|
|
log.debug("%s sent '%s' to %s" % (current_user.username, message, room))
|
|
|
|
emit('event response room',
|
|
{'data': data, 'user': current_user.username, 'gravatar': hashlib.md5(current_user.email).hexdigest(), 'chatroom': room}, room=room)
|
|
|
|
|
|
@socketio.on('event disconnect', namespace='/chat')
|
|
def event_disconnect(message):
|
|
log.info("%s disconnected" % current_user.username)
|
|
disconnect()
|
|
|
|
|
|
@socketio.on('join', namespace='/chat')
|
|
def join(message):
|
|
room = sanitize_html(message['room'])
|
|
|
|
log.debug("%s joined %s" % (current_user.username, room))
|
|
join_room(room)
|
|
|
|
emit('event response room',
|
|
{'data': room, 'userjoin': current_user.username}, broadcast=True)
|
|
|
|
|
|
@socketio.on('leave', namespace='/chat')
|
|
def leave(message):
|
|
room = sanitize_html(message['room'])
|
|
|
|
log.debug("%s left %s" % (current_user.username, room))
|
|
leave_room(room)
|
|
|
|
emit('event response room',
|
|
{'data': room, 'userleave': current_user.username}, broadcast=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
socketio.run(app, host=args.host, port=args.port)
|