You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
syncserver/syncserver/__init__.py

224 lines
9.4 KiB
Python

# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import logging
from urlparse import urlparse, urlunparse
from pyramid.response import Response
from pyramid.events import NewRequest, subscriber
try:
import requests.packages.urllib3.contrib.pyopenssl
HAS_PYOPENSSL = True
except ImportError:
HAS_PYOPENSSL = False
import mozsvc.config
from tokenserver.util import _JSONError
logger = logging.getLogger("syncserver")
DEFAULT_TOKENSERVER_BACKEND = "syncserver.staticnode.StaticNodeAssignment"
def includeme(config):
"""Install SyncServer application into the given Pyramid configurator."""
# Set the umask so that files are created with secure permissions.
# Necessary for e.g. created-on-demand sqlite database files.
os.umask(0077)
# If PyOpenSSL is available, configure requests to use it.
# This helps improve security on older python versions.
if HAS_PYOPENSSL:
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
settings = config.registry.settings
import_settings_from_environment_variables(settings)
# Sanity-check the deployment settings and provide sensible defaults.
public_url = settings.get("syncserver.public_url")
if public_url is None:
raise RuntimeError("you must configure syncserver.public_url")
public_url = public_url.rstrip("/")
settings["syncserver.public_url"] = public_url
secret = settings.get("syncserver.secret")
if secret is None:
secret = os.urandom(32).encode("hex")
sqluri = settings.get("syncserver.sqluri")
if sqluri is None:
rootdir = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
sqluri = "sqlite:///" + os.path.join(rootdir, "syncserver.db")
# Configure app-specific defaults based on top-level configuration.
settings.pop("config", None)
if "tokenserver.backend" not in settings:
# Default to our simple static node-assignment backend
settings["tokenserver.backend"] = DEFAULT_TOKENSERVER_BACKEND
if settings["tokenserver.backend"] == DEFAULT_TOKENSERVER_BACKEND:
# Provide some additional defaults for the default backend,
# unless overridden in the config.
if "tokenserver.sqluri" not in settings:
settings["tokenserver.sqluri"] = sqluri
if "tokenserver.node_url" not in settings:
settings["tokenserver.node_url"] = public_url
if "endpoints.sync-1.5" not in settings:
settings["endpoints.sync-1.5"] = "{node}/storage/1.5/{uid}"
if "tokenserver.monkey_patch_gevent" not in settings:
# Default to no gevent monkey-patching
settings["tokenserver.monkey_patch_gevent"] = False
if "tokenserver.applications" not in settings:
# Default to just the sync-1.5 application
settings["tokenserver.applications"] = "sync-1.5"
if "tokenserver.secrets.backend" not in settings:
# Default to a single fixed signing secret
settings["tokenserver.secrets.backend"] = "mozsvc.secrets.FixedSecrets"
settings["tokenserver.secrets.secrets"] = [secret]
if "tokenserver.allow_new_users" not in settings:
allow_new_users = settings.get("syncserver.allow_new_users")
if allow_new_users is not None:
settings["tokenserver.allow_new_users"] = allow_new_users
if "hawkauth.secrets.backend" not in settings:
# Default to the same secrets backend as the tokenserver
for key in settings.keys():
if key.startswith("tokenserver.secrets."):
newkey = "hawkauth" + key[len("tokenserver"):]
settings[newkey] = settings[key]
if "storage.backend" not in settings:
# Default to sql syncstorage backend
settings["storage.backend"] = "syncstorage.storage.sql.SQLStorage"
settings["storage.sqluri"] = sqluri
settings["storage.create_tables"] = True
# The batch-upload API is not yet stable in production.
if "storage.batch_upload_enabled" not in settings:
settings["storage.batch_upload_enabled"] = False
if "browserid.backend" not in settings:
# Default to local verifier to reduce external dependencies.
# Use base of public_url as only audience
audience = urlunparse(urlparse(public_url)._replace(path=""))
settings["browserid.backend"] = "tokenserver.verifiers.LocalVerifier"
settings["browserid.audiences"] = audience
if "loggers" not in settings:
# Default to basic logging config.
root_logger = logging.getLogger("")
if not root_logger.handlers:
logging.basicConfig(level=logging.WARN)
if "fxa.metrics_uid_secret_key" not in settings:
# Default to a randomly-generated secret.
# This setting isn't useful in a self-hosted setup
# and setting a default avoids scary-sounding warnings.
settings["fxa.metrics_uid_secret_key"] = os.urandom(16).encode("hex")
# Include the relevant sub-packages.
config.include("syncstorage", route_prefix="/storage")
config.include("tokenserver", route_prefix="/token")
# Add a top-level "it works!" view.
def itworks(request):
return Response("it works!")
config.add_route('itworks', '/')
config.add_view(itworks, route_name='itworks')
def import_settings_from_environment_variables(settings, environ=None):
"""Helper function to import settings from environment variables.
This helper exists to allow the most commonly-changed settings to be
configured via environment variables, which is useful when deploying
with docker. For more complex configuration needs you should write
a .ini config file.
"""
if environ is None:
environ = os.environ
SETTINGS_FROM_ENVIRON = (
("SYNCSERVER_PUBLIC_URL", "syncserver.public_url", str),
("SYNCSERVER_SECRET", "syncserver.secret", str),
("SYNCSERVER_SQLURI", "syncserver.sqluri", str),
("SYNCSERVER_ALLOW_NEW_USERS",
"syncserver.allow_new_users",
str_to_bool),
("SYNCSERVER_BATCH_UPLOAD_ENABLED",
"storage.batch_upload_enabled",
str_to_bool),
)
for key, name, convert in SETTINGS_FROM_ENVIRON:
try:
settings[name] = convert(environ[key])
except KeyError:
pass
def str_to_bool(value):
"""Helper to convert textual boolean strings to actual booleans."""
if value.lower() in ("true", "on", "1", "yes"):
return True
if value.lower() in ("false", "off", "0", "no"):
return True
raise ValueError("unable to parse boolean from %r" % (value,))
@subscriber(NewRequest)
def reconcile_wsgi_environ_with_public_url(event):
"""Event-listener that checks and tweaks WSGI environ based on public_url.
This is a simple trick to help ensure that the configured public_url
matches the actual deployed address. It fixes fixes parts of the WSGI
environ where it makes sense (e.g. SCRIPT_NAME) and warns about any parts
that seem obviously mis-configured (e.g. http:// versus https://).
It's very important to get public_url and WSGI environ matching exactly,
since they're used for browserid audience checking and HAWK signature
validation, so mismatches can easily cause strange and cryptic errors.
"""
request = event.request
public_url = request.registry.settings["syncserver.public_url"]
p_public_url = urlparse(public_url)
# If we don't have a SCRIPT_NAME, take it from the public_url.
# This is often the case if we're behind e.g. an nginx proxy that
# is serving us at some sub-path.
if not request.script_name:
request.script_name = p_public_url.path.rstrip("/")
# If the environ does not match public_url, requests are almost certainly
# going to fail due to auth errors. We can either bail out early, or we
# can forcibly clobber the WSGI environ with the values from public_url.
# This is a security risk if you've e.g. mis-configured the server, so
# it's not enabled by default.
application_url = request.application_url
if public_url != application_url:
if not request.registry.settings.get("syncserver.force_wsgi_environ"):
msg = "\n".join((
"The public_url setting doesn't match the application url.",
"This will almost certainly cause authentication failures!",
" public_url setting is: %s" % (public_url,),
" application url is: %s" % (application_url,),
"You can disable this check by setting the force_wsgi_environ",
"option in your config file, but do so at your own risk.",
))
logger.error(msg)
raise _JSONError([msg], status_code=500)
request.scheme = p_public_url.scheme
request.host = p_public_url.netloc
request.script_name = p_public_url.path.rstrip("/")
def get_configurator(global_config, **settings):
"""Load a SyncStorge configurator object from deployment settings."""
config = mozsvc.config.get_configurator(global_config, **settings)
config.begin()
try:
config.include(includeme)
finally:
config.end()
return config
def main(global_config={}, **settings):
"""Load a SyncStorage WSGI app from deployment settings."""
config = get_configurator(global_config, **settings)
return config.make_wsgi_app()