mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,5 +5,7 @@ node_modules
|
||||
build
|
||||
dist
|
||||
share/jupyter/static/components
|
||||
share/jupyter/static/css/style.min.css
|
||||
share/jupyter/static/css/style.min.css.map
|
||||
*.egg-info
|
||||
MANIFEST
|
||||
|
@@ -23,7 +23,11 @@ Basic principles:
|
||||
|
||||
# install the Python pargs (-e for editable/development install)
|
||||
pip install [-e] .
|
||||
|
||||
|
||||
You will also need `bower` to fetch frontend-javascript and `less` to compile CSS:
|
||||
|
||||
npm install -g bower less
|
||||
|
||||
Note on debian/ubuntu machines, you may need to install the `nodejs-legacy` package
|
||||
to get node executables to work:
|
||||
|
||||
|
@@ -9,10 +9,12 @@ import logging
|
||||
import os
|
||||
from subprocess import Popen
|
||||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
import tornado.httpserver
|
||||
import tornado.options
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.log import LogFormatter
|
||||
from tornado.log import LogFormatter, app_log
|
||||
from tornado import gen, web
|
||||
|
||||
from IPython.utils.traitlets import (
|
||||
@@ -25,6 +27,7 @@ from IPython.utils.importstring import import_item
|
||||
here = os.path.dirname(__file__)
|
||||
|
||||
from .handlers import (
|
||||
Template404,
|
||||
RootHandler,
|
||||
LoginHandler,
|
||||
LogoutHandler,
|
||||
@@ -36,6 +39,13 @@ from . import orm
|
||||
from ._data import DATA_FILES_PATH
|
||||
from .utils import url_path_join
|
||||
|
||||
|
||||
class RedirectHandler(web.RedirectHandler):
|
||||
def get(self, *a, **kw):
|
||||
self.set_header("mu-redirect", True)
|
||||
app_log.warn("mu redirect: %s -> %s", self.request.path, self._url)
|
||||
return super(RedirectHandler, self).get(*a, **kw)
|
||||
|
||||
class JupyterHubApp(Application):
|
||||
"""An Application for starting a Multi-User Notebook server."""
|
||||
data_files_path = Unicode(DATA_FILES_PATH, config=True,
|
||||
@@ -64,6 +74,10 @@ class JupyterHubApp(Application):
|
||||
help="The base URL of the entire application"
|
||||
)
|
||||
|
||||
jinja_environment_options = Dict(config=True,
|
||||
help="Supply extra arguments that will be passed to Jinja environment."
|
||||
)
|
||||
|
||||
proxy_cmd = Unicode('configurable-http-proxy', config=True,
|
||||
help="""The command to start the http proxy.
|
||||
|
||||
@@ -157,7 +171,7 @@ class JupyterHubApp(Application):
|
||||
|
||||
def _log_format_default(self):
|
||||
"""override default log format to include time"""
|
||||
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
|
||||
return u"H %(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
|
||||
|
||||
def init_logging(self):
|
||||
# This prevents double log messages because tornado use a root logger that
|
||||
@@ -191,8 +205,11 @@ class JupyterHubApp(Application):
|
||||
self.handlers = self.add_url_prefix(self.hub_prefix, handlers)
|
||||
self.handlers.extend([
|
||||
(r"/user/([^/]+)/?.*", UserHandler),
|
||||
(r"/", web.RedirectHandler, {"url" : self.hub_prefix}),
|
||||
(r"/?", RedirectHandler, {"url" : self.hub_prefix}),
|
||||
])
|
||||
self.handlers.append(
|
||||
(r'(.*)', Template404)
|
||||
)
|
||||
|
||||
def init_db(self):
|
||||
# TODO: load state from db for resume
|
||||
@@ -240,20 +257,28 @@ class JupyterHubApp(Application):
|
||||
'--api-ip', self.proxy.api_server.ip,
|
||||
'--api-port', str(self.proxy.api_server.port),
|
||||
'--default-target', self.hub.server.host,
|
||||
'--log-level=debug',
|
||||
]
|
||||
if self.ssl_key:
|
||||
cmd.extend(['--ssl-key', self.ssl_key])
|
||||
if self.ssl_cert:
|
||||
cmd.extend(['--ssl-cert', self.ssl_cert])
|
||||
|
||||
|
||||
self.proxy = Popen(cmd, env=env)
|
||||
|
||||
def init_tornado_settings(self):
|
||||
"""Set up the tornado settings dict."""
|
||||
base_url = self.base_url
|
||||
template_path = os.path.join(self.data_files_path, 'templates'),
|
||||
jinja_env = Environment(
|
||||
loader=FileSystemLoader(template_path),
|
||||
**self.jinja_environment_options
|
||||
)
|
||||
|
||||
settings = dict(
|
||||
config=self.config,
|
||||
# log=self.log,
|
||||
log=self.log,
|
||||
db=self.db,
|
||||
hub=self.hub,
|
||||
authenticator=import_item(self.authenticator)(config=self.config),
|
||||
@@ -261,8 +286,10 @@ class JupyterHubApp(Application):
|
||||
base_url=base_url,
|
||||
cookie_secret=self.cookie_secret,
|
||||
login_url=url_path_join(self.hub.server.base_url, 'login'),
|
||||
template_path=os.path.join(self.data_files_path, 'templates'),
|
||||
static_files_path=os.path.join(self.data_files_path, 'static'),
|
||||
static_path=os.path.join(self.data_files_path, 'static'),
|
||||
static_url_prefix=url_path_join(self.hub.server.base_url, 'static/'),
|
||||
template_path=template_path,
|
||||
jinja2_env=jinja_env,
|
||||
)
|
||||
# allow configured settings to have priority
|
||||
settings.update(self.tornado_settings)
|
||||
|
@@ -5,9 +5,16 @@
|
||||
|
||||
import json
|
||||
import re
|
||||
try:
|
||||
# py3
|
||||
from http.client import responses
|
||||
except ImportError:
|
||||
from httplib import responses
|
||||
|
||||
import requests
|
||||
|
||||
from jinja2 import TemplateNotFound
|
||||
|
||||
from tornado.log import app_log
|
||||
from tornado.escape import url_escape
|
||||
from tornado.httputil import url_concat
|
||||
@@ -54,26 +61,97 @@ class BaseHandler(RequestHandler):
|
||||
return cookie_token.user.name
|
||||
else:
|
||||
# have cookie, but it's not valid. Clear it and start over.
|
||||
self.clear_cookie(self.hub.server.cookie_name)
|
||||
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
|
||||
|
||||
def clear_login_cookie(self):
|
||||
username = self.get_current_user()
|
||||
if username is not None:
|
||||
user = self.db.query(orm.User).filter(name=username).first()
|
||||
if user is not None:
|
||||
if user is not None and user.server is not None:
|
||||
self.clear_cookie(user.server.cookie_name, path=user.server.base_url)
|
||||
self.clear_cookie(self.cookie_name, path=self.hub.base_url)
|
||||
self.clear_cookie(self.hub.server.cookie_name, path=self.hub.server.base_url)
|
||||
|
||||
@property
|
||||
def spawner_class(self):
|
||||
return self.settings.get('spawner_class', LocalProcessSpawner)
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# template rendering
|
||||
#---------------------------------------------------------------
|
||||
|
||||
def get_template(self, name):
|
||||
"""Return the jinja template object for a given name"""
|
||||
return self.settings['jinja2_env'].get_template(name)
|
||||
|
||||
def render_template(self, name, **ns):
|
||||
ns.update(self.template_namespace)
|
||||
template = self.get_template(name)
|
||||
return template.render(**ns)
|
||||
|
||||
@property
|
||||
def logged_in(self):
|
||||
"""Is a user currently logged in?"""
|
||||
user = self.get_current_user()
|
||||
return (user and not user == 'anonymous')
|
||||
|
||||
@property
|
||||
def template_namespace(self):
|
||||
return dict(
|
||||
base_url=self.base_url,
|
||||
logged_in=self.logged_in,
|
||||
login_url=self.settings['login_url'],
|
||||
static_url=self.static_url,
|
||||
)
|
||||
|
||||
def write_error(self, status_code, **kwargs):
|
||||
"""render custom error pages"""
|
||||
exc_info = kwargs.get('exc_info')
|
||||
message = ''
|
||||
status_message = responses.get(status_code, 'Unknown HTTP Error')
|
||||
if exc_info:
|
||||
exception = exc_info[1]
|
||||
# get the custom message, if defined
|
||||
try:
|
||||
message = exception.log_message % exception.args
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# construct the custom reason, if defined
|
||||
reason = getattr(exception, 'reason', '')
|
||||
if reason:
|
||||
status_message = reason
|
||||
|
||||
# build template namespace
|
||||
ns = dict(
|
||||
status_code=status_code,
|
||||
status_message=status_message,
|
||||
message=message,
|
||||
exception=exception,
|
||||
)
|
||||
|
||||
self.set_header('Content-Type', 'text/html')
|
||||
# render the template
|
||||
try:
|
||||
html = self.render_template('%s.html' % status_code, **ns)
|
||||
except TemplateNotFound:
|
||||
self.log.debug("No template for %d", status_code)
|
||||
html = self.render_template('error.html', **ns)
|
||||
|
||||
self.write(html)
|
||||
|
||||
|
||||
class Template404(BaseHandler):
|
||||
"""Render our 404 template"""
|
||||
def prepare(self):
|
||||
raise web.HTTPError(404)
|
||||
|
||||
|
||||
class RootHandler(BaseHandler):
|
||||
"""Redirect from / to /user/foo/ after logging in."""
|
||||
@web.authenticated
|
||||
def get(self):
|
||||
self.redirect("/user/%s/" % self.get_current_user())
|
||||
uri = "/user/%s/" % self.get_current_user()
|
||||
self.redirect(uri, permanent=False)
|
||||
|
||||
|
||||
class UserHandler(BaseHandler):
|
||||
@@ -98,24 +176,25 @@ class LogoutHandler(BaseHandler):
|
||||
"""Log a user out by clearing their login cookie."""
|
||||
def get(self):
|
||||
self.clear_login_cookie()
|
||||
self.write("logged out")
|
||||
html = self.render_template('logout.html')
|
||||
self.finish(html)
|
||||
|
||||
class LoginHandler(BaseHandler):
|
||||
"""Render the login page."""
|
||||
|
||||
def _render(self, message=None, username=None):
|
||||
self.render('login.html',
|
||||
return self.render_template('login.html',
|
||||
next=url_escape(self.get_argument('next', default='')),
|
||||
username=username,
|
||||
message=message,
|
||||
)
|
||||
|
||||
def get(self):
|
||||
if False and self.get_current_user():
|
||||
self.redirect(self.get_argument('next', default='/'))
|
||||
if self.get_argument('next', False) and self.get_current_user():
|
||||
self.redirect(self.get_argument('next'))
|
||||
else:
|
||||
username = self.get_argument('username', default='')
|
||||
self._render(username=username)
|
||||
self.finish(self._render(username=username))
|
||||
|
||||
@gen.coroutine
|
||||
def notify_proxy(self, user):
|
||||
@@ -211,13 +290,15 @@ class LoginHandler(BaseHandler):
|
||||
user = yield self.spawn_single_user(username)
|
||||
self.set_login_cookies(user)
|
||||
next_url = self.get_argument('next', default='') or '/user/%s/' % username
|
||||
print('next', next_url)
|
||||
self.redirect(next_url)
|
||||
else:
|
||||
self.log.debug("Failed login for %s", username)
|
||||
self._render(
|
||||
html = self._render(
|
||||
message={'error': 'Invalid username or password'},
|
||||
username=username,
|
||||
)
|
||||
self.finish(html)
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
|
36
setup.py
36
setup.py
@@ -42,9 +42,12 @@ except NameError:
|
||||
locs = locs or globs
|
||||
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
pjoin = os.path.join
|
||||
|
||||
here = os.path.abspath(os.path.dirname(__file__))
|
||||
share_jupyter = pjoin(here, 'share', 'jupyter')
|
||||
static = pjoin(share_jupyter, 'static')
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Build basic package data, etc.
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -53,7 +56,6 @@ def get_data_files():
|
||||
"""Get data files in share/jupyter"""
|
||||
|
||||
data_files = []
|
||||
share_jupyter = pjoin(here, 'share', 'jupyter')
|
||||
ntrim = len(here) + 1
|
||||
|
||||
for (d, dirs, filenames) in os.walk(share_jupyter):
|
||||
@@ -117,19 +119,41 @@ class Bower(Command):
|
||||
|
||||
def run(self):
|
||||
check_call(['bower', 'install', '--allow-root'])
|
||||
# update data-files in case this created new files
|
||||
self.distribution.data_files = get_data_files()
|
||||
|
||||
class CSS(Command):
|
||||
description = "compile CSS from LESS"
|
||||
|
||||
def get_outputs(self):
|
||||
return []
|
||||
user_options = []
|
||||
|
||||
def get_inputs(self):
|
||||
return []
|
||||
def initialize_options(self):
|
||||
pass
|
||||
|
||||
def finalize_options(self):
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
style_less = pjoin(static, 'less', 'style.less')
|
||||
style_css = pjoin(static, 'css', 'style.min.css')
|
||||
sourcemap = style_css + '.map'
|
||||
check_call([
|
||||
'lessc', '-x', '--verbose',
|
||||
'--source-map-basepath={}'.format(static),
|
||||
'--source-map={}'.format(sourcemap),
|
||||
'--source-map-rootpath=../',
|
||||
style_less, style_css,
|
||||
])
|
||||
# update data-files in case this created new files
|
||||
self.distribution.data_files = get_data_files()
|
||||
|
||||
# ensure bower is run as part of install
|
||||
install.sub_commands.insert(0, ('bower', None))
|
||||
install.sub_commands.insert(1, ('css', None))
|
||||
|
||||
setup_args['cmdclass'] = {
|
||||
'bower': Bower,
|
||||
'css': CSS,
|
||||
}
|
||||
|
||||
# setuptools requirements
|
||||
|
BIN
share/jupyter/static/images/jupyterlogo.png
Normal file
BIN
share/jupyter/static/images/jupyterlogo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
20
share/jupyter/static/less/error.less
Normal file
20
share/jupyter/static/less/error.less
Normal file
@@ -0,0 +1,20 @@
|
||||
div.error {
|
||||
margin: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.error > h1 {
|
||||
font-size: 500%;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
div.error > p {
|
||||
font-size: 200%;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
div.traceback-wrapper {
|
||||
text-align: left;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
14
share/jupyter/static/less/logout.less
Normal file
14
share/jupyter/static/less/logout.less
Normal file
@@ -0,0 +1,14 @@
|
||||
div.logout-main {
|
||||
margin: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
div.logout-main > h1 {
|
||||
font-size: 400%;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
div.logout-main > p {
|
||||
font-size: 200%;
|
||||
line-height: normal;
|
||||
}
|
3
share/jupyter/static/less/page.less
Normal file
3
share/jupyter/static/less/page.less
Normal file
@@ -0,0 +1,3 @@
|
||||
.jpy-logo {
|
||||
height: 48px;
|
||||
}
|
26
share/jupyter/static/less/style.less
Normal file
26
share/jupyter/static/less/style.less
Normal file
@@ -0,0 +1,26 @@
|
||||
/*!
|
||||
*
|
||||
* Twitter Bootstrap
|
||||
*
|
||||
*/
|
||||
@import "../components/bootstrap/less/bootstrap.less";
|
||||
@import "../components/bootstrap/less/responsive-utilities.less";
|
||||
|
||||
/*!
|
||||
*
|
||||
* Font Awesome
|
||||
*
|
||||
*/
|
||||
@import "../components/font-awesome/less/font-awesome.less";
|
||||
@fa-font-path: "../components/font-awesome/fonts";
|
||||
|
||||
/*!
|
||||
*
|
||||
* Jupyter
|
||||
*
|
||||
*/
|
||||
|
||||
@import "./variables.less";
|
||||
@import "./page.less";
|
||||
@import "./error.less";
|
||||
@import "./logout.less";
|
0
share/jupyter/static/less/variables.less
Normal file
0
share/jupyter/static/less/variables.less
Normal file
6
share/jupyter/templates/404.html
Normal file
6
share/jupyter/templates/404.html
Normal file
@@ -0,0 +1,6 @@
|
||||
{% extends "error.html" %}
|
||||
|
||||
{% block error_detail %}
|
||||
<p>Jupyter has lots of moons, but this is not one...</p>
|
||||
{% endblock %}
|
||||
|
22
share/jupyter/templates/error.html
Normal file
22
share/jupyter/templates/error.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% extends "page.html" %}
|
||||
|
||||
{% block login_widget %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<div class="error">
|
||||
{% block h1_error %}
|
||||
<h1>{{status_code}} : {{status_message}}</h1>
|
||||
{% endblock h1_error %}
|
||||
{% block error_detail %}
|
||||
{% if message %}
|
||||
<p>The error was:</p>
|
||||
<div class="traceback-wrapper">
|
||||
<pre class="traceback">{{message}}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</header>
|
||||
|
||||
{% endblock %}
|
@@ -1,18 +1,44 @@
|
||||
<html>
|
||||
<head>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{% if message %}
|
||||
<div id="message">{{message}}</div>
|
||||
{% end if %}
|
||||
<form action="?next={{next}}" method="post">
|
||||
<input type="text" name="username" id="user_input" value="{{username}}">
|
||||
<input type="password" name="password" id="password_input">
|
||||
<button type="submit" id="login_submit">Log in</button>
|
||||
</form>
|
||||
{% extends "page.html" %}
|
||||
|
||||
<script type="text/javascript">
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{% block login_widget %}
|
||||
{% endblock %}
|
||||
|
||||
{% block site %}
|
||||
|
||||
<div id="login-main" class="container">
|
||||
<div class="row">
|
||||
<form action="{{login_url}}?next={{next}}" method="post" role="form">
|
||||
<div class="col-lg-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">Username:</span>
|
||||
<input type="username" class="form-control" name="username" id="username_input" val="{{username}}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">Password:</span>
|
||||
<input type="password" class="form-control" name="password" id="password_input">
|
||||
<span class="input-group-btn">
|
||||
<button type="submit" id="login_submit" class="btn btn-default">Log in</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if message %}
|
||||
<div class="row">
|
||||
<div class="message">
|
||||
{{message}}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div/>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
{{super()}}
|
||||
|
||||
{% endblock %}
|
||||
|
13
share/jupyter/templates/logout.html
Normal file
13
share/jupyter/templates/logout.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "page.html" %}
|
||||
|
||||
{% block login_widget %}
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<div class="container logout-main">
|
||||
<h1>You have been logged out</h1>
|
||||
<p><a href="{base_url}login">Log in again...</a></p>
|
||||
<div/>
|
||||
|
||||
{% endblock %}
|
76
share/jupyter/templates/page.html
Normal file
76
share/jupyter/templates/page.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{% block title %}Jupyter Hub{% endblock %}</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{static_url("images/favicon.ico") }}">
|
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
{% block stylesheet %}
|
||||
<link rel="stylesheet" href="{{ static_url("css/style.min.css") }}" type="text/css"/>
|
||||
{% endblock %}
|
||||
<script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
|
||||
<script>
|
||||
require.config({
|
||||
baseUrl: '{{static_url("", include_version=False)}}',
|
||||
paths: {
|
||||
jquery: 'components/jquery/jquery.min',
|
||||
bootstrap: 'components/bootstrap/js/bootstrap.min',
|
||||
moment: "components/moment/moment",
|
||||
},
|
||||
shim: {
|
||||
bootstrap: {
|
||||
deps: ["jquery"],
|
||||
exports: "bootstrap"
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block meta %}
|
||||
{% endblock %}
|
||||
|
||||
</head>
|
||||
|
||||
<body {% block params %}{% endblock %}>
|
||||
|
||||
<noscript>
|
||||
<div id='noscript'>
|
||||
Jupyter Hub requires JavaScript.<br>
|
||||
Please enable it to proceed.
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<div id="header" class="navbar navbar-static-top">
|
||||
<div class="container">
|
||||
<span id="jupyterlogo" class="pull-left"><a href="{{base_url}}home/" alt='dashboard'><img src='{{static_url("images/jupyterlogo.png") }}' alt='Jupyter Hub' class='jpy-logo'/></a></span>
|
||||
|
||||
{% block login_widget %}
|
||||
|
||||
<span id="login_widget">
|
||||
{% if logged_in %}
|
||||
<button id="logout">Logout</button>
|
||||
{% else %}
|
||||
<button id="login">Login</button>
|
||||
{% endif %}
|
||||
</span>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block main %}
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Reference in New Issue
Block a user