mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 10:34:10 +00:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d7d8459edb | ||
![]() |
39a7116d16 | ||
![]() |
d27c970cc4 | ||
![]() |
cf56dbb97b | ||
![]() |
a4ccfe4e11 | ||
![]() |
f1871bbe24 | ||
![]() |
1cc9153a91 | ||
![]() |
4258254c39 | ||
![]() |
f3aee9bd16 | ||
![]() |
5cb8ccf8b2 | ||
![]() |
1d63e417ca | ||
![]() |
ee0020e8fa | ||
![]() |
2d83575a24 | ||
![]() |
33c168530e | ||
![]() |
5d4d34b24d | ||
![]() |
49cc794937 | ||
![]() |
7f9e77ce5b | ||
![]() |
6fa3b429db | ||
![]() |
e89836c035 | ||
![]() |
784b5cb6f0 | ||
![]() |
daaa763c3b | ||
![]() |
2b18c64081 | ||
![]() |
785addc245 | ||
![]() |
b4758db017 | ||
![]() |
10fbfee157 | ||
![]() |
c58a251dbd | ||
![]() |
27be5e4847 | ||
![]() |
be97a0c95b | ||
![]() |
689a312756 | ||
![]() |
1484869ee3 | ||
![]() |
a090632a48 | ||
![]() |
451a16c57e | ||
![]() |
6e14e86a1a | ||
![]() |
a142f543ba | ||
![]() |
0bb3996c30 |
@@ -27,7 +27,7 @@ before_install:
|
||||
unset MYSQL_UNIX_PORT
|
||||
DB=mysql bash ci/docker-db.sh
|
||||
DB=mysql bash ci/init-db.sh
|
||||
pip install 'mysql-connector<2.2'
|
||||
pip install 'mysql-connector-python'
|
||||
elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then
|
||||
DB=postgres bash ci/init-db.sh
|
||||
pip install psycopg2-binary
|
||||
|
@@ -9,7 +9,7 @@ command line for details.
|
||||
|
||||
## 1.0
|
||||
|
||||
### [1.0.0] 2018-03-XX
|
||||
### [1.0.0] 2019-04-XX
|
||||
|
||||
JupyterHub 1.0 is a major milestone for JupyterHub.
|
||||
Huge thanks to the many people who have contributed to this release,
|
||||
@@ -94,6 +94,8 @@ whether it was through discussion, testing, documentation, or development.
|
||||
- `Spawner.options_from_form` may now be async
|
||||
- Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's
|
||||
servers when they log out.
|
||||
- When `Spawner.start` raises an Exception,
|
||||
a message can be passed on to the user if the exception has a `.jupyterhub_message` attribute.
|
||||
|
||||
|
||||
#### Changes
|
||||
@@ -131,6 +133,7 @@ whether it was through discussion, testing, documentation, or development.
|
||||
- Fewer redirects following a visit to the default `/` url
|
||||
- Error when progress is requested before progress is ready
|
||||
- Error when API requests are made to a not-running server without authentication
|
||||
- Avoid logging database password on connect if password is specified in `JupyterHub.db_url`.
|
||||
|
||||
#### Development changes
|
||||
|
||||
@@ -148,6 +151,14 @@ In general, see `CONTRIBUTING.md` for contribution info or ask if you have quest
|
||||
|
||||
## 0.9
|
||||
|
||||
### [0.9.6] 2019-04-01
|
||||
|
||||
JupyterHub 0.9.6 is a security release.
|
||||
|
||||
- Fixes an Open Redirect vulnerability (CVE-2019-10255).
|
||||
|
||||
JupyterHub 0.9.5 included a partial fix for this issue.
|
||||
|
||||
### [0.9.4] 2018-09-24
|
||||
|
||||
JupyterHub 0.9.4 is a small bugfix release.
|
||||
@@ -566,7 +577,8 @@ First preview release
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...HEAD
|
||||
[1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...HEAD
|
||||
[1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.5...HEAD
|
||||
[0.9.6]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...0.9.6
|
||||
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4
|
||||
[0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3
|
||||
[0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2
|
||||
|
@@ -4,8 +4,13 @@
|
||||
Community communication channels
|
||||
================================
|
||||
|
||||
We use `Gitter <https://gitter.im>`_ for online, real-time text chat. The
|
||||
primary channel for JupyterHub is `jupyterhub/jupyterhub <https://gitter.im/jupyterhub/jupyterhub>`_.
|
||||
We use `Discourse <https://discourse.jupyter.org>` for online discussion.
|
||||
Everyone in the Jupyter community is welcome to bring ideas and questions there.
|
||||
In addition, we use `Gitter <https://gitter.im>`_ for online, real-time text chat,
|
||||
a place for more ephemeral discussions.
|
||||
The primary Gitter channel for JupyterHub is `jupyterhub/jupyterhub <https://gitter.im/jupyterhub/jupyterhub>`_.
|
||||
Gitter isn't archived or searchable, so we recommend going to discourse first
|
||||
to make sure that discussions are most useful and accessible to the community.
|
||||
Remember that our community is distributed across the world in various
|
||||
timezones, so be patient if you do not get an answer immediately!
|
||||
|
||||
|
10
docs/source/contributing/security.rst
Normal file
10
docs/source/contributing/security.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Reporting security issues in Jupyter or JupyterHub
|
||||
==================================================
|
||||
|
||||
If you find a security vulnerability in Jupyter or JupyterHub,
|
||||
whether it is a failure of the security model described in :doc:`../reference/websecurity`
|
||||
or a failure in implementation,
|
||||
please report it to security@ipython.org.
|
||||
|
||||
If you prefer to encrypt your security reports,
|
||||
you can use :download:`this PGP public key </ipython_security.asc>`.
|
@@ -116,6 +116,7 @@ helps keep our community welcoming to as many people as possible.
|
||||
contributing/docs
|
||||
contributing/tests
|
||||
contributing/roadmap
|
||||
contributing/security
|
||||
|
||||
Upgrading JupyterHub
|
||||
--------------------
|
||||
|
52
docs/source/ipython_security.asc
Normal file
52
docs/source/ipython_security.asc
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: GnuPG v2.0.22 (GNU/Linux)
|
||||
|
||||
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
|
||||
5vmLc+Ibz+eT+rM=
|
||||
=XVDe
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
@@ -12,8 +12,11 @@ function get_hub_version() {
|
||||
split=( ${hub_xyz//./ } )
|
||||
hub_xy="${split[0]}.${split[1]}"
|
||||
# add .dev on hub_xy so it's 1.0.dev
|
||||
if [[ ! -z "${split[3]}" ]]; then
|
||||
if [[ ! -z "${split[3]:-}" ]]; then
|
||||
hub_xy="${hub_xy}.${split[3]}"
|
||||
latest=0
|
||||
else
|
||||
latest=1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -31,3 +34,11 @@ docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
|
||||
docker push $DOCKER_REPO:$hub_xy
|
||||
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:$hub_xy
|
||||
docker push $ONBUILD:$hub_xyz
|
||||
|
||||
# if building a stable release, tag latest as well
|
||||
if [[ "$latest" == "1" ]]; then
|
||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:latest
|
||||
docker push $DOCKER_REPO:latest
|
||||
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:latest
|
||||
docker push $ONBUILD:latest
|
||||
fi
|
||||
|
@@ -6,7 +6,7 @@ version_info = (
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
"b1", # release (b1, rc1, or "" for final or dev)
|
||||
"b2", # release (b1, rc1, or "" for final or dev)
|
||||
# "dev", # dev or nothing
|
||||
)
|
||||
|
||||
|
@@ -427,6 +427,7 @@ class UserServerAPIHandler(APIHandler):
|
||||
return
|
||||
self.log.info("Deleting spawner %s", spawner._log_name)
|
||||
self.db.delete(spawner.orm_spawner)
|
||||
user.spawners.pop(server_name, None)
|
||||
self.db.commit()
|
||||
|
||||
if server_name:
|
||||
|
@@ -1410,7 +1410,18 @@ class JupyterHub(Application):
|
||||
def init_db(self):
|
||||
"""Create the database connection"""
|
||||
|
||||
self.log.debug("Connecting to db: %s", self.db_url)
|
||||
urlinfo = urlparse(self.db_url)
|
||||
if urlinfo.password:
|
||||
# avoid logging the database password
|
||||
urlinfo = urlinfo._replace(
|
||||
netloc='{}:[redacted]@{}:{}'.format(
|
||||
urlinfo.username, urlinfo.hostname, urlinfo.port
|
||||
)
|
||||
)
|
||||
db_log_url = urlinfo.geturl()
|
||||
else:
|
||||
db_log_url = self.db_url
|
||||
self.log.debug("Connecting to db: %s", db_log_url)
|
||||
if self.upgrade_db:
|
||||
dbutil.upgrade_if_needed(self.db_url, log=self.log)
|
||||
|
||||
@@ -1420,7 +1431,7 @@ class JupyterHub(Application):
|
||||
)
|
||||
self.db = self.session_factory()
|
||||
except OperationalError as e:
|
||||
self.log.error("Failed to connect to db: %s", self.db_url)
|
||||
self.log.error("Failed to connect to db: %s", db_log_url)
|
||||
self.log.debug("Database error was:", exc_info=True)
|
||||
if self.db_url.startswith('sqlite:///'):
|
||||
self._check_db_path(self.db_url.split(':///', 1)[1])
|
||||
|
@@ -9,6 +9,7 @@ from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from subprocess import check_call
|
||||
from tempfile import TemporaryDirectory
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
@@ -118,7 +119,18 @@ def upgrade_if_needed(db_url, backup=True, log=None):
|
||||
else:
|
||||
# nothing to do
|
||||
return
|
||||
log.info("Upgrading %s", db_url)
|
||||
urlinfo = urlparse(db_url)
|
||||
if urlinfo.password:
|
||||
# avoid logging the database password
|
||||
urlinfo = urlinfo._replace(
|
||||
netloc='{}:[redacted]@{}:{}'.format(
|
||||
urlinfo.username, urlinfo.hostname, urlinfo.port
|
||||
)
|
||||
)
|
||||
db_log_url = urlinfo.geturl()
|
||||
else:
|
||||
db_log_url = db_url
|
||||
log.info("Upgrading %s", db_log_url)
|
||||
# we need to upgrade, backup the database
|
||||
if backup and db_url.startswith('sqlite:///'):
|
||||
db_file = db_url.split(':///', 1)[1]
|
||||
|
@@ -549,6 +549,8 @@ class BaseHandler(RequestHandler):
|
||||
- else: /hub/home
|
||||
"""
|
||||
next_url = self.get_argument('next', default='')
|
||||
# protect against some browsers' buggy handling of backslash as slash
|
||||
next_url = next_url.replace('\\', '%5C')
|
||||
if (next_url + '/').startswith(
|
||||
(
|
||||
'%s://%s/' % (self.request.protocol, self.request.host),
|
||||
@@ -562,15 +564,23 @@ class BaseHandler(RequestHandler):
|
||||
)
|
||||
):
|
||||
# treat absolute URLs for our host as absolute paths:
|
||||
# below, redirects that aren't strictly paths
|
||||
parsed = urlparse(next_url)
|
||||
next_url = parsed.path
|
||||
if parsed.query:
|
||||
next_url = next_url + '?' + parsed.query
|
||||
if parsed.fragment:
|
||||
next_url = next_url + '#' + parsed.fragment
|
||||
if next_url and (urlparse(next_url).netloc or not next_url.startswith('/')):
|
||||
|
||||
# if it still has host info, it didn't match our above check for *this* host
|
||||
if next_url and (
|
||||
'://' in next_url
|
||||
or next_url.startswith('//')
|
||||
or not next_url.startswith('/')
|
||||
):
|
||||
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
|
||||
next_url = ''
|
||||
|
||||
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
|
||||
# add /hub/ prefix, to ensure we redirect to the right user's server.
|
||||
# The next request will be handled by SpawnHandler,
|
||||
|
@@ -281,13 +281,20 @@ class SpawnPendingHandler(BaseHandler):
|
||||
# Implicit spawn on /user/:name is not allowed if the user's last spawn failed.
|
||||
# We should point the user to Home if the most recent spawn failed.
|
||||
exc = spawner._spawn_future.exception()
|
||||
self.log.error(
|
||||
"Preventing implicit spawn for %s because last spawn failed: %s",
|
||||
spawner._log_name,
|
||||
exc,
|
||||
self.log.error("Previous spawn for %s failed: %s", spawner._log_name, exc)
|
||||
spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name)
|
||||
self.set_status(500)
|
||||
html = self.render_template(
|
||||
"not_running.html",
|
||||
user=user,
|
||||
server_name=server_name,
|
||||
spawn_url=spawn_url,
|
||||
failed=True,
|
||||
failed_message=getattr(exc, 'jupyterhub_message', ''),
|
||||
exception=exc,
|
||||
)
|
||||
# raise a copy because each time an Exception object is re-raised, its traceback grows
|
||||
raise copy.copy(exc).with_traceback(exc.__traceback__)
|
||||
self.finish(html)
|
||||
return
|
||||
|
||||
# Check for pending events. This should usually be the case
|
||||
# when we are on this page.
|
||||
|
@@ -5,9 +5,11 @@ implements https://oauthlib.readthedocs.io/en/latest/oauth2/server.html
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from oauthlib import uri_validate
|
||||
from oauthlib.oauth2 import RequestValidator
|
||||
from oauthlib.oauth2 import WebApplicationServer
|
||||
from oauthlib.oauth2.rfc6749.grant_types import authorization_code
|
||||
from oauthlib.oauth2.rfc6749.grant_types import base
|
||||
from sqlalchemy.orm import scoped_session
|
||||
from tornado import web
|
||||
from tornado.escape import url_escape
|
||||
@@ -21,7 +23,16 @@ from ..utils import url_path_join
|
||||
# patch absolute-uri check
|
||||
# because we want to allow relative uri oauth
|
||||
# for internal services
|
||||
authorization_code.is_absolute_uri = lambda uri: True
|
||||
|
||||
|
||||
def is_absolute_uri(uri):
|
||||
if uri.startswith('/'):
|
||||
return True
|
||||
return uri_validate.is_absolute_uri(uri)
|
||||
|
||||
|
||||
authorization_code.is_absolute_uri = is_absolute_uri
|
||||
base.is_absolute_uri = is_absolute_uri
|
||||
|
||||
|
||||
class JupyterHubRequestValidator(RequestValidator):
|
||||
|
@@ -960,7 +960,7 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
|
||||
# validate OAuth state
|
||||
arg_state = self.get_argument("state", None)
|
||||
if arg_state is None:
|
||||
raise HTTPError("oauth state is missing. Try logging in again.")
|
||||
raise HTTPError(500, "oauth state is missing. Try logging in again.")
|
||||
cookie_name = self.hub_auth.get_state_cookie_name(arg_state)
|
||||
cookie_state = self.get_secure_cookie(cookie_name)
|
||||
# clear cookie state now that we've consumed it
|
||||
|
@@ -323,6 +323,8 @@ class MockHub(JupyterHub):
|
||||
self.pid_file = NamedTemporaryFile(delete=False).name
|
||||
self.db_file = NamedTemporaryFile()
|
||||
self.db_url = os.getenv('JUPYTERHUB_TEST_DB_URL') or self.db_file.name
|
||||
if 'mysql' in self.db_url:
|
||||
self.db_kwargs['connect_args'] = {'auth_plugin': 'mysql_native_password'}
|
||||
yield super().initialize([])
|
||||
|
||||
# add an initial user
|
||||
|
@@ -13,7 +13,10 @@ from jupyterhub import orm
|
||||
|
||||
def populate_db(url):
|
||||
"""Populate a jupyterhub database"""
|
||||
db = orm.new_session_factory(url)()
|
||||
connect_args = {}
|
||||
if 'mysql' in url:
|
||||
connect_args['auth_plugin'] = 'mysql_native_password'
|
||||
db = orm.new_session_factory(url, connect_args=connect_args)()
|
||||
# create some users
|
||||
admin = orm.User(name='admin', admin=True)
|
||||
db.add(admin)
|
||||
|
@@ -28,7 +28,7 @@ def generate_old_db(env_dir, hub_version, db_url):
|
||||
check_call([sys.executable, '-m', 'virtualenv', env_dir])
|
||||
pkgs = ['jupyterhub==' + hub_version]
|
||||
if 'mysql' in db_url:
|
||||
pkgs.append('mysql-connector<2.2')
|
||||
pkgs.append('mysql-connector-python')
|
||||
elif 'postgres' in db_url:
|
||||
pkgs.append('psycopg2')
|
||||
check_call([env_pip, 'install'] + pkgs)
|
||||
|
@@ -162,8 +162,10 @@ async def test_delete_named_server(app, named_servers):
|
||||
)
|
||||
r.raise_for_status()
|
||||
assert r.status_code == 204
|
||||
# low-level record is now removes
|
||||
# low-level record is now removed
|
||||
assert servername not in user.orm_spawners
|
||||
# and it's still not in the high-level wrapper dict
|
||||
assert servername not in user.spawners
|
||||
|
||||
|
||||
async def test_named_server_disabled(app):
|
||||
|
@@ -467,9 +467,12 @@ async def test_login_strip(app):
|
||||
(False, '/absolute', '/absolute'),
|
||||
(False, '/has?query#andhash', '/has?query#andhash'),
|
||||
# next_url outside is not allowed
|
||||
(False, 'relative/path', ''),
|
||||
(False, 'https://other.domain', ''),
|
||||
(False, 'ftp://other.domain', ''),
|
||||
(False, '//other.domain', ''),
|
||||
(False, '///other.domain/triple', ''),
|
||||
(False, '\\\\other.domain/backslashes', ''),
|
||||
],
|
||||
)
|
||||
async def test_login_redirect(app, running, next_url, location):
|
||||
@@ -485,7 +488,7 @@ async def test_login_redirect(app, running, next_url, location):
|
||||
|
||||
url = 'login'
|
||||
if next_url:
|
||||
if '//' not in next_url:
|
||||
if '//' not in next_url and next_url.startswith('/'):
|
||||
next_url = ujoin(app.base_url, next_url, '')
|
||||
url = url_concat(url, dict(next=next_url))
|
||||
|
||||
|
@@ -163,6 +163,10 @@ class User:
|
||||
|
||||
self.spawners = _SpawnerDict(self._new_spawner)
|
||||
|
||||
# ensure default spawner exists in the database
|
||||
if '' not in self.orm_user.orm_spawners:
|
||||
self._new_orm_spawner('')
|
||||
|
||||
@property
|
||||
def authenticator(self):
|
||||
return self.settings.get('authenticator', None)
|
||||
@@ -221,6 +225,14 @@ class User:
|
||||
# otherwise, yield low-level ORM object (server is not active)
|
||||
yield orm_spawner
|
||||
|
||||
def _new_orm_spawner(self, server_name):
|
||||
"""Creat the low-level orm Spawner object"""
|
||||
orm_spawner = orm.Spawner(user=self.orm_user, name=server_name)
|
||||
self.db.add(orm_spawner)
|
||||
self.db.commit()
|
||||
assert server_name in self.orm_spawners
|
||||
return orm_spawner
|
||||
|
||||
def _new_spawner(self, server_name, spawner_class=None, **kwargs):
|
||||
"""Create a new spawner"""
|
||||
if spawner_class is None:
|
||||
@@ -229,10 +241,7 @@ class User:
|
||||
|
||||
orm_spawner = self.orm_spawners.get(server_name)
|
||||
if orm_spawner is None:
|
||||
orm_spawner = orm.Spawner(user=self.orm_user, name=server_name)
|
||||
self.db.add(orm_spawner)
|
||||
self.db.commit()
|
||||
assert server_name in self.orm_spawners
|
||||
orm_spawner = self._new_orm_spawner(server_name)
|
||||
if server_name == '' and self.state:
|
||||
# migrate user.state to spawner.state
|
||||
orm_spawner.state = self.state
|
||||
|
13
package.json
13
package.json
@@ -14,16 +14,15 @@
|
||||
"lessc": "lessc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clean-css": "^3.4.13",
|
||||
"less": "^2.7.1",
|
||||
"less": "^3.9.0",
|
||||
"less-plugin-clean-css": "^1.5.1",
|
||||
"prettier": "^1.14.2"
|
||||
"prettier": "^1.16.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^3.4.0",
|
||||
"bootstrap": "^3.4.1",
|
||||
"font-awesome": "^4.7.0",
|
||||
"jquery": "^3.2.1",
|
||||
"moment": "^2.19.3",
|
||||
"requirejs": "^2.3.4"
|
||||
"jquery": "^3.3.1",
|
||||
"moment": "^2.24.0",
|
||||
"requirejs": "^2.3.6"
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ async_generator>=1.8
|
||||
certipy>=0.1.2
|
||||
entrypoints
|
||||
jinja2
|
||||
oauthlib>=2.0,<3
|
||||
oauthlib>=3.0
|
||||
pamela
|
||||
prometheus_client>=0.0.21
|
||||
python-dateutil
|
||||
|
@@ -126,7 +126,10 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function(
|
||||
var row = getRow(el);
|
||||
var user = row.data("user");
|
||||
var serverName = row.data("server-name");
|
||||
el.attr("href", utils.url_path_join(prefix, "hub/spawn", user, serverName));
|
||||
el.attr(
|
||||
"href",
|
||||
utils.url_path_join(prefix, "hub/spawn", user, serverName)
|
||||
);
|
||||
});
|
||||
// cannot start all servers in this case
|
||||
// since it would mean opening a bunch of tabs
|
||||
|
@@ -5,12 +5,36 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="text-center">
|
||||
{% block heading %}
|
||||
<h1>
|
||||
{% if failed %}
|
||||
Spawn failed
|
||||
{% else %}
|
||||
Server not running
|
||||
{% endif %}
|
||||
</h1>
|
||||
{% endblock %}
|
||||
{% block message %}
|
||||
<p>Server {{ server_name }} is not running. Would you like to start it?</p>
|
||||
<p>
|
||||
{% if failed %}
|
||||
The latest attempt to start your server {{ server_name }} has failed.
|
||||
{% if failed_message %}
|
||||
{{ failed_message }}
|
||||
{% endif %}
|
||||
Would you like to retry starting it?
|
||||
{% else %}
|
||||
Your server {{ server_name }} is not running. Would you like to start it?
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
{% block start_button %}
|
||||
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ spawn_url }}">
|
||||
Launch Server {{ server_name }}
|
||||
{% if failed %}
|
||||
Relaunch
|
||||
{% else %}
|
||||
Launch
|
||||
{% endif %}
|
||||
Server {{ server_name }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
@@ -14,7 +14,7 @@ function get_hub_version() {
|
||||
split=( ${hub_xyz//./ } )
|
||||
hub_xy="${split[0]}.${split[1]}"
|
||||
# add .dev on hub_xy so it's 1.0.dev
|
||||
if [[ ! -z "${split[3]}" ]]; then
|
||||
if [[ ! -z "${split[3]:-}" ]]; then
|
||||
hub_xy="${hub_xy}.${split[3]}"
|
||||
fi
|
||||
}
|
||||
|
Reference in New Issue
Block a user