def create_app():
    app = Flask(__name__)

    app.config.from_object('bepasty.config.Config')
    if os.environ.get('BEPASTY_CONFIG'):
        app.config.from_envvar('BEPASTY_CONFIG')

    setup_secret_key(app)

    prefix = app.config.get('APP_BASE_PATH')
    if prefix is not None:
        app.wsgi_app = PrefixMiddleware(app.wsgi_app, prefix=prefix)

    app.storage = create_storage(app)
    setup_werkzeug_routing(app)

    app.register_blueprint(blueprint)
    app.register_blueprint(blueprint_apis)

    @app.errorhandler(403)
    def url_forbidden(e):
        heading = 'Forbidden'
        body = Markup("""\
            <p>
                You are not allowed to access the requested URL.
            </p>
            <p>
                If you entered the URL manually please check your spelling and try again.
            </p>
            <p>
                Also check if you maybe forgot to log in or if your permissions are insufficient for this.
            </p>
""")
        return render_template('error.html', heading=heading, body=body), 403

    @app.errorhandler(404)
    def url_not_found(e):
        heading = 'Not found'
        body = Markup("""\
            <p>
                The requested URL was not found on the server.
            </p>
            <p>
                If you entered the URL manually please check your spelling and try again.
            </p>
""")
        return render_template('error.html', heading=heading, body=body), 404

    @app.before_request
    def before_request():
        """
        before the request is handled (by its view function), we compute some
        stuff here and make it easily available.
        """
        flaskg.logged_in = logged_in()
        flaskg.permissions = get_permissions()
        flaskg.icon_permissions = get_permission_icons()
        if flaskg.logged_in:
            session.permanent = current_app.config['PERMANENT_SESSION']

    def datetime_format(ts):
        """
        takes a unix timestamp and outputs a iso8601-like formatted string.
        times are always UTC, but we don't include the TZ here for brevity.
        it should be made clear (e.g. in the template) that the date/time is UTC.
        """
        if not ts:  # we use 0 to indicate undefined time
            return 'undefined'
        return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(ts))

    app.jinja_env.filters['datetime'] = datetime_format

    app.jinja_env.globals['flaskg'] = flaskg
    app.jinja_env.globals['may'] = may
    app.jinja_env.globals['ADMIN'] = ADMIN
    app.jinja_env.globals['LIST'] = LIST
    app.jinja_env.globals['CREATE'] = CREATE
    app.jinja_env.globals['MODIFY'] = MODIFY
    app.jinja_env.globals['READ'] = READ
    app.jinja_env.globals['DELETE'] = DELETE

    return app class Config:
    """
    This is the basic configuration class for bepasty.

    IMPORTANT:

    The config is only loaded at startup time of the app, so if you change it,
    you need to restart the wsgi app process(es) to make it load the updated
    config.
    """

    #: name of this site (put YOUR bepasty fqdn here)
    SITENAME = 'bepasty.example.org'

    #: base URL path of app (if not served on root URL, but e.g. on http://example.org/bepasty ).
    #: setting this to a non-None value will activate the PrefixMiddleware that splits PATH_INFO
    #: into SCRIPT_NAME (== APP_BASE_PATH) and the rest of PATH_INFO.
    APP_BASE_PATH = None  # '/bepasty'

    #: Whether files are automatically locked after upload.
    #:
    #: If you want to require admin approval and manual unlocking for each
    #: uploaded file, set this to True.
    UPLOAD_LOCKED = False

    #: The asciinema player theme (one of asciinema, tango, solarized-dark,
    #: solarized-light, monokai)
    ASCIINEMA_THEME = 'asciinema'

    #: The site admin can set some maximum allowed file size he wants to
    #: accept here. This is the maximum size an uploaded file may have.
    MAX_ALLOWED_FILE_SIZE = 5 * 1000 * 1000 * 1000

    #: The maximum http request body size.
    #: This is an information given to rest api clients so they can adjust
    #: their chunk size accordingly.
    #:
    #: This needs to be in sync with (or at least not beyond) the web server
    #: settings:
    #: apache:  LimitRequestBody 1048576 # apache default is 0 (unlimited)
    #: nginx:  client_max_body_size 1m; # nginx default (== 1048576)
    MAX_BODY_SIZE = 1 * 1024 * 1024 - 8192  # 8kiB safety margin, issue #155

    #: Setup maximum file sizes for specific content-types. If an item is
    #: beyond the limit set for its type, it will not be rendered, but just
    #: offered for download. Lookup within MAX_RENDER_SIZE is done by
    #: first-match and it is automatically sorted for longer content-type-
    #: prefixes first.
    #:
    #: Format of entries: content-type-prefix: max_size
    MAX_RENDER_SIZE = {
        # each list entry has 38 bytes, do not render > 1000 items
        'text/x-bepasty-list': 1000 * 38,
        # stuff rendered with syntax highlighting (expensive for server and
        # client) and also used for other text/* types as we use same code to
        # get a (non-highlighted) display with line numbers:
        'HIGHLIGHT_TYPES': 100 * 1000,
        # the in-browser pdf reader is sometimes rather slow and should
        # rather not be used for big PDFs:
        'application/pdf': 10 * 1000 * 1000,
        'application/x-pdf': 10 * 1000 * 1000,
        # images / audio / video can be rather big, we do not process them:
        'image/': 10 * 1000 * 1000,
        'audio/': 1 * 1000 * 1000 * 1000,
        'video/': 5 * 1000 * 1000 * 1000,
        # DEFAULT - matches everything not matched otherwise.
        # As we have catched everything we are able to render already,
        # this maybe should be a medium size, just for the case we forget
        # something:
        '': 1 * 1000 * 1000,
    }

    # Whether to use the python-magic module to guess a file's mime
    # type by looking into its content (if the mime type can not be
    # determined from the filename extension).
    # NOTE:
    # libmagic may have security issues, so maybe you should only use
    # it if you trust all users with upload permissions ('create').
    USE_PYTHON_MAGIC = False

    #: Define storage backend, choose from:
    #:
    #: - 'filesystem'
    #:
    STORAGE = 'filesystem'

    #: Filesystem storage path
    STORAGE_FILESYSTEM_DIRECTORY = '/tmp/'

    #: server secret key needed for safe session cookies.
    #: you must set a very long, very random, very secret string here,
    #: otherwise bepasty will not work (and crash when trying to log in)!
    SECRET_KEY = ''

    #: transmit cookie only over https (if you use http, set this to False)
    SESSION_COOKIE_SECURE = True
    #: use a permanent session (True, cookie will expire after the given
    #: time, see below) or not (False, cookie will get removed when browser
    #: is closed)
    PERMANENT_SESSION = False
    #: lifetime of the permanent session (in seconds)
    PERMANENT_SESSION_LIFETIME = 31 * 24 * 3600

    #: Bepasty does **not** use the usual user/password scheme, but **only**
    #: uses passwords (or passphrases - we'll call both "a secret" below) as
    #: log-in credentials - there are no separate user names.
    #:
    #: People who log-in using such a secret may get more permissions than
    #: those who do not log-in (who just get DEFAULT_PERMISSIONS).
    #:
    #: Depending on the secret used to log-in, they will get the permissions
    #: configured here, see below.
    #:
    #: You can use same secret / same permissions for all privileged users or
    #: set up different secrets / different permissions for each user.
    #:
    #: If you want to be able to revoke permissions for some user / some group
    #: of users, it might be a good idea to remember to whom you gave which
    #: secret (and also handle it in a rather fine-grained way).
    #:
    #: PERMISSIONS is a dict that maps secrets to permissions, use it like:
    #:
    #: ::
    #:
    #:     PERMISSIONS = {
    #:         'myadminsecret_1.21d-3!wdar34': 'admin,list,create,modify,read,delete',
    #:         'uploadersecret_rtghtrbrrrfsd': 'create,read',
    #:         'joe_doe_89359299887711335537': 'create,read,delete',
    #:     }
    PERMISSIONS = {
        # 'foo': 'admin,list,create,modify,read,delete',
    }

    #: not-logged-in users get these permissions -
    #: usually they are either no permissions ('') or read-only ('read').
    DEFAULT_PERMISSIONS = '' FILENAME = 'filename' TYPE = 'type' LOCKED = 'locked' SIZE = 'size' COMPLETE = 'complete' HASH = 'hash' TIMESTAMP_DOWNLOAD = 'timestamp-download' TIMESTAMP_UPLOAD = 'timestamp-upload' TIMESTAMP_MAX_LIFE = 'timestamp-max-life' TRANSACTION_ID = 'Transaction-ID'  # keep in sync with bepasty-cli def get_maxlife(data, underscore):
    unit_key = 'maxlife_unit' if underscore else 'maxlife-unit'
    unit_default = 'MONTHS'
    unit = data.get(unit_key, unit_default).upper()
    value_key = 'maxlife_value' if underscore else 'maxlife-value'
    value_default = '1'
    try:
        value = int(data.get(value_key, value_default))
    except (ValueError, TypeError):
        raise BadRequest(description=f'{value_key} header is incorrect')
    try:
        return time_unit_to_sec(value, unit)
    except KeyError:
        raise BadRequest(description=f'{unit_key} header is incorrect')
import os
import base64
import threading
import time
import copy
import hashlib
import re
import pytest
        import magic
from requests.auth import _basic_auth_str
from flask import current_app, url_for, json
from ..app import create_app
from ..config import Config
from ..constants import FILENAME, TYPE, LOCKED, SIZE, COMPLETE, HASH, \
    TIMESTAMP_DOWNLOAD, TIMESTAMP_UPLOAD, TIMESTAMP_MAX_LIFE, TRANSACTION_ID
from ..utils.date_funcs import get_maxlife
    assert response.headers['Content-Range'] == range_str
    if check_data:
        assert data == response.data


def check_json_response(response, metas, code=200, check_data=True):
    check_response(response, code, check_data=check_data)
    if check_data:
        assert metas == response.json


def check_upload_response(response, code=201, check_data=True):
    check_json_response(response, {}, code=code, check_data=check_data)

    if code == 200:
        assert len(response.headers[TRANSACTION_ID]) > 0
        return None

    url_prefix = RestUrl().upload + '/'
    assert response.headers['Content-Location'].startswith(url_prefix)
    return response.headers['Content-Location']


def test_auth(client_fixture):
    _, client, _ = client_fixture
    url = RestUrl()

    # basic auth (unknown user)
    response = client.get(url.list, headers=add_auth('user', 'invalid'))
    check_err_response(response, 403)
    # basic auth (valid user)
    response = client.get(url.list, headers=add_auth('user', 'full'))
    check_response(response, 200)

    # token auth (unknown user)
    response = client.get(url.list + '?token=invalid')
    check_err_response(response, 403)
    # token auth (valid user)
    response = client.get(url.list + '?token=full')
    check_response(response, 200)


def test_config(client_fixture):
    app, client, _ = client_fixture

    url = RestUrl()

    # get server config (post should fail)
    response = client.post(url.config)
    check_err_response(response, 405)

    # get server config
    response = client.get(url.config)
    check_response(response, 200)
    assert len(response.json) == 2
    assert response.json['MAX_ALLOWED_FILE_SIZE'] == app.config['MAX_ALLOWED_FILE_SIZE']
    assert response.json['MAX_BODY_SIZE'] == app.config['MAX_BODY_SIZE']

    # get server config (head)
    response = client.head(url.config)
    check_response(response, 200, check_data=False)


def _upload(client, data, token=None, filename=None, ftype=None, lifetime=None,
            range_str=None, trans_id=None, encode=True, set_range=True):
    if data:
        payload = base64.b64encode(data) if encode else data
        payload_len = len(payload)
    else:
        payload = None
        payload_len = 0

    headers = {
        'Content-Length': str(payload_len),
    }
    if set_range:
        if range_str is None:
            range_str = 'bytes 0-{}/{}'.format(payload_len - 1, payload_len)
        headers['Content-Range'] = range_str
    if filename is not None:
        headers['Content-Filename'] = filename
    if ftype is not None:
        headers['Content-Type'] = ftype
    if lifetime:
        headers['Maxlife-Value'] = str(lifetime[0])
        headers['Maxlife-Unit'] = lifetime[1]
    if trans_id is not None:
        headers[TRANSACTION_ID] = trans_id
    if token is not None:
        add_auth('user', token, headers)

    response = client.post(RestUrl().upload, headers=headers, data=payload)
    # FIXME: without waiting for background hash compute, following
    # detail request may not have hash yet
    wait_background()

    return response


def make_meta(data, filename=None, ftype=None, lifetime=None, uri=None):
    h = hashlib.sha256()
    h.update(data)

    if lifetime is None:
        # default maxlife is 1 MONTHS
        lifetime = [1, 'MONTHS']

    maxtime = get_maxlife({
        'maxlife_value': lifetime[0],
        'maxlife_unit': lifetime[1]
    }, True)
    maxlife = int(time.time()) + maxtime if maxtime > 0 else maxtime

    meta = {
        'file-meta': {
            COMPLETE: True,
            FILENAME: filename,
            HASH: h.hexdigest(),
            LOCKED: False,
            SIZE: len(data),

            TIMESTAMP_DOWNLOAD: 0,