Add lengths to all Unicode() ones

- Otherwise does not work with MySQL
- Change JSONDict to be TEXT (Unbounded) rather than VARCHAR.
  This makes most sense, since you can't index these anyway.
- The 'ip' field in Server is set to 255, since that is the
  max allowed length of DNS entries.
- Most of the rest of the Unicodes have approximately high
  values that most people should not mostly run into
  (famous last words).
This commit is contained in:
YuviPanda
2016-03-01 12:57:00 -08:00
parent 62e30c1d79
commit bf9dea5522

View File

@@ -12,7 +12,7 @@ from tornado import gen
from tornado.log import app_log from tornado.log import app_log
from tornado.httpclient import HTTPRequest, AsyncHTTPClient from tornado.httpclient import HTTPRequest, AsyncHTTPClient
from sqlalchemy.types import TypeDecorator, VARCHAR from sqlalchemy.types import TypeDecorator, TEXT
from sqlalchemy import ( from sqlalchemy import (
inspect, inspect,
Column, Integer, ForeignKey, Unicode, Boolean, Column, Integer, ForeignKey, Unicode, Boolean,
@@ -39,7 +39,7 @@ class JSONDict(TypeDecorator):
""" """
impl = VARCHAR impl = TEXT
def process_bind_param(self, value, dialect): def process_bind_param(self, value, dialect):
if value is not None: if value is not None:
@@ -59,20 +59,20 @@ Base.log = app_log
class Server(Base): class Server(Base):
"""The basic state of a server """The basic state of a server
connection and cookie info connection and cookie info
""" """
__tablename__ = 'servers' __tablename__ = 'servers'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
proto = Column(Unicode, default='http') proto = Column(Unicode(15), default='http')
ip = Column(Unicode, default='') ip = Column(Unicode(255), default='') # could also be a DNS name
port = Column(Integer, default=random_port) port = Column(Integer, default=random_port)
base_url = Column(Unicode, default='/') base_url = Column(Unicode(255), default='/')
cookie_name = Column(Unicode, default='cookie') cookie_name = Column(Unicode(255), default='cookie')
def __repr__(self): def __repr__(self):
return "<Server(%s:%s)>" % (self.ip, self.port) return "<Server(%s:%s)>" % (self.ip, self.port)
@property @property
def host(self): def host(self):
ip = self.ip ip = self.ip
@@ -91,18 +91,18 @@ class Server(Base):
host=self.host, host=self.host,
uri=self.base_url, uri=self.base_url,
) )
@property @property
def bind_url(self): def bind_url(self):
"""representation of URL used for binding """representation of URL used for binding
Never used in APIs, only logging, Never used in APIs, only logging,
since it can be non-connectable value, such as '', meaning all interfaces. since it can be non-connectable value, such as '', meaning all interfaces.
""" """
if self.ip in {'', '0.0.0.0'}: if self.ip in {'', '0.0.0.0'}:
return self.url.replace('127.0.0.1', self.ip or '*', 1) return self.url.replace('127.0.0.1', self.ip or '*', 1)
return self.url return self.url
@gen.coroutine @gen.coroutine
def wait_up(self, timeout=10, http=False): def wait_up(self, timeout=10, http=False):
"""Wait for this server to come up""" """Wait for this server to come up"""
@@ -110,7 +110,7 @@ class Server(Base):
yield wait_for_http_server(self.url, timeout=timeout) yield wait_for_http_server(self.url, timeout=timeout)
else: else:
yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout) yield wait_for_server(self.ip or '127.0.0.1', self.port, timeout=timeout)
def is_up(self): def is_up(self):
"""Is the server accepting connections?""" """Is the server accepting connections?"""
try: try:
@@ -136,7 +136,7 @@ class Server(Base):
class Proxy(Base): class Proxy(Base):
"""A configurable-http-proxy instance. """A configurable-http-proxy instance.
A proxy consists of the API server info and the public-facing server info, A proxy consists of the API server info and the public-facing server info,
plus an auth token for configuring the proxy table. plus an auth token for configuring the proxy table.
""" """
@@ -147,7 +147,7 @@ class Proxy(Base):
public_server = relationship(Server, primaryjoin=_public_server_id == Server.id) public_server = relationship(Server, primaryjoin=_public_server_id == Server.id)
_api_server_id = Column(Integer, ForeignKey('servers.id')) _api_server_id = Column(Integer, ForeignKey('servers.id'))
api_server = relationship(Server, primaryjoin=_api_server_id == Server.id) api_server = relationship(Server, primaryjoin=_api_server_id == Server.id)
def __repr__(self): def __repr__(self):
if self.public_server: if self.public_server:
return "<%s %s:%s>" % ( return "<%s %s:%s>" % (
@@ -155,7 +155,7 @@ class Proxy(Base):
) )
else: else:
return "<%s [unconfigured]>" % self.__class__.__name__ return "<%s [unconfigured]>" % self.__class__.__name__
def api_request(self, path, method='GET', body=None, client=None): def api_request(self, path, method='GET', body=None, client=None):
"""Make an authenticated API request of the proxy""" """Make an authenticated API request of the proxy"""
client = client or AsyncHTTPClient() client = client or AsyncHTTPClient()
@@ -178,10 +178,11 @@ class Proxy(Base):
self.log.info("Adding user %s to proxy %s => %s", self.log.info("Adding user %s to proxy %s => %s",
user.name, user.proxy_path, user.server.host, user.name, user.proxy_path, user.server.host,
) )
if user.spawn_pending: if user.spawn_pending:
raise RuntimeError( raise RuntimeError(
"User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name) "User %s's spawn is pending, shouldn't be added to the proxy yet!", user.name)
yield self.api_request(user.proxy_path, yield self.api_request(user.proxy_path,
method='POST', method='POST',
body=dict( body=dict(
@@ -190,7 +191,7 @@ class Proxy(Base):
), ),
client=client, client=client,
) )
@gen.coroutine @gen.coroutine
def delete_user(self, user, client=None): def delete_user(self, user, client=None):
"""Remove a user's server to the proxy table.""" """Remove a user's server to the proxy table."""
@@ -199,7 +200,7 @@ class Proxy(Base):
method='DELETE', method='DELETE',
client=client, client=client,
) )
@gen.coroutine @gen.coroutine
def get_routes(self, client=None): def get_routes(self, client=None):
"""Fetch the proxy's routes""" """Fetch the proxy's routes"""
@@ -209,7 +210,7 @@ class Proxy(Base):
@gen.coroutine @gen.coroutine
def add_all_users(self, user_dict): def add_all_users(self, user_dict):
"""Update the proxy table from the database. """Update the proxy table from the database.
Used when loading up a new proxy. Used when loading up a new proxy.
""" """
db = inspect(self).session db = inspect(self).session
@@ -251,9 +252,9 @@ class Proxy(Base):
class Hub(Base): class Hub(Base):
"""Bring it all together at the hub. """Bring it all together at the hub.
The Hub is a server, plus its API path suffix The Hub is a server, plus its API path suffix
the api_url is the full URL plus the api_path suffix on the end the api_url is the full URL plus the api_path suffix on the end
of the server base_url. of the server base_url.
""" """
@@ -262,12 +263,12 @@ class Hub(Base):
_server_id = Column(Integer, ForeignKey('servers.id')) _server_id = Column(Integer, ForeignKey('servers.id'))
server = relationship(Server, primaryjoin=_server_id == Server.id) server = relationship(Server, primaryjoin=_server_id == Server.id)
host = '' host = ''
@property @property
def api_url(self): def api_url(self):
"""return the full API url (with proto://host...)""" """return the full API url (with proto://host...)"""
return url_path_join(self.server.url, 'api') return url_path_join(self.server.url, 'api')
def __repr__(self): def __repr__(self):
if self.server: if self.server:
return "<%s %s:%s>" % ( return "<%s %s:%s>" % (
@@ -279,31 +280,31 @@ class Hub(Base):
class User(Base): class User(Base):
"""The User table """The User table
Each user has a single server, Each user has a single server,
and multiple tokens used for authorization. and multiple tokens used for authorization.
API tokens grant access to the Hub's REST API. API tokens grant access to the Hub's REST API.
These are used by single-user servers to authenticate requests, These are used by single-user servers to authenticate requests,
and external services to manipulate the Hub. and external services to manipulate the Hub.
Cookies are set with a single ID. Cookies are set with a single ID.
Resetting the Cookie ID invalidates all cookies, forcing user to login again. Resetting the Cookie ID invalidates all cookies, forcing user to login again.
A `state` column contains a JSON dict, A `state` column contains a JSON dict,
used for restoring state of a Spawner. used for restoring state of a Spawner.
""" """
__tablename__ = 'users' __tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode) name = Column(Unicode(1023))
# should we allow multiple servers per user? # should we allow multiple servers per user?
_server_id = Column(Integer, ForeignKey('servers.id')) _server_id = Column(Integer, ForeignKey('servers.id'))
server = relationship(Server, primaryjoin=_server_id == Server.id) server = relationship(Server, primaryjoin=_server_id == Server.id)
admin = Column(Boolean, default=False) admin = Column(Boolean, default=False)
last_activity = Column(DateTime, default=datetime.utcnow) last_activity = Column(DateTime, default=datetime.utcnow)
api_tokens = relationship("APIToken", backref="user") api_tokens = relationship("APIToken", backref="user")
cookie_id = Column(Unicode, default=new_token) cookie_id = Column(Unicode(1023), default=new_token)
state = Column(JSONDict) state = Column(JSONDict)
other_user_cookies = set([]) other_user_cookies = set([])
@@ -321,7 +322,7 @@ class User(Base):
cls=self.__class__.__name__, cls=self.__class__.__name__,
name=self.name, name=self.name,
) )
def new_api_token(self): def new_api_token(self):
"""Create a new API token""" """Create a new API token"""
assert self.id is not None assert self.id is not None
@@ -344,29 +345,29 @@ class User(Base):
class APIToken(Base): class APIToken(Base):
"""An API token""" """An API token"""
__tablename__ = 'api_tokens' __tablename__ = 'api_tokens'
@declared_attr @declared_attr
def user_id(cls): def user_id(cls):
return Column(Integer, ForeignKey('users.id')) return Column(Integer, ForeignKey('users.id'))
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
hashed = Column(Unicode) hashed = Column(Unicode(1023))
prefix = Column(Unicode) prefix = Column(Unicode(1023))
prefix_length = 4 prefix_length = 4
algorithm = "sha512" algorithm = "sha512"
rounds = 16384 rounds = 16384
salt_bytes = 8 salt_bytes = 8
@property @property
def token(self): def token(self):
raise AttributeError("token is write-only") raise AttributeError("token is write-only")
@token.setter @token.setter
def token(self, token): def token(self, token):
"""Store the hashed value and prefix for a token""" """Store the hashed value and prefix for a token"""
self.prefix = token[:self.prefix_length] self.prefix = token[:self.prefix_length]
self.hashed = hash_token(token, rounds=self.rounds, salt=self.salt_bytes, algorithm=self.algorithm) self.hashed = hash_token(token, rounds=self.rounds, salt=self.salt_bytes, algorithm=self.algorithm)
def __repr__(self): def __repr__(self):
return "<{cls}('{pre}...', user='{u}')>".format( return "<{cls}('{pre}...', user='{u}')>".format(
cls=self.__class__.__name__, cls=self.__class__.__name__,
@@ -387,7 +388,7 @@ class APIToken(Base):
for orm_token in prefix_match: for orm_token in prefix_match:
if orm_token.match(token): if orm_token.match(token):
return orm_token return orm_token
def match(self, token): def match(self, token):
"""Is this my token?""" """Is this my token?"""
return compare_token(self.hashed, token) return compare_token(self.hashed, token)