diff --git a/.gitignore b/.gitignore index e2bd56f..0ec6dc9 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,4 @@ **/__pycache__/ .DS_Store .coverage -/*.md -/.eggs/ -/devel/ \ No newline at end of file +/*.md \ No newline at end of file diff --git a/README.md b/README.md index 8be40cf..030d58b 100644 --- a/README.md +++ b/README.md @@ -2,40 +2,28 @@ ![](docs/_assets/images/favpng_parrot-royalty-free-cartoon.png) -**NOTE**: httpaste is publicly hosted at [httpaste.it](http://httpaste.it) and as a hidden Tor service ([https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion](https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion)). -Both services are to be considered evaluatory, as long as the source code -is in pre-release. Regarding voidance of pre-release status, see [Open Issues](https://victorykit.atlassian.net/issues/?jql=project%20%3D%20HTTPASTE%20AND%20fixVersion%20in%20(1.1.0-beta%2C%201.2.0-beta%2C%201.3.0)), for more information. +httpaste is a pastebin application for easily pasting and retrieving data over +HTTP from shell environments and web browsers. It is inspired by [sprunge.us](http://sprunge.us) +and [ix.io](http://ix.io/), but focuses on extendability, advanced security, with little to +no trade-off to simplicity. It can be hosted through WSGI, CGI, Fast CGI, or +as a standalone evaluation server. It offers multiple storage backends, such as +a filesystem backend, SQLite backend, MySQL backend, or MongoDB backend. -This program offers an HTTP interface for storing public and private data -(a.k.a. pastes), commonly referred to as a pastebin application. It is inspired by [sprunge.us](http://sprunge.us) and [ix.io](http://ix.io/). It can be hosted through WSGI, CGI, Fast -CGI, or as a standalone evaluation server. It offers multiple storage backends, -such as a filesystem backend, SQLite backend, or MySQL backend. +All pastes are being encrypted on the fly and can only be retrieved by an +authorized user, either through knowing the paste id of a public paste, or +having authentication credentials, as well as the paste id of a private paste. +This makes httpaste ideal as a pastebin for sensitive environments such as the +Tor network. Authentication credentials are created on-the-fly and don’t require a sign-up process. -Public data can be accessed through an URL, where as private pastes -additionally require HTTP basic authentication. Creation of authentication -credentials happens on the fly, there is no sign-up process. Public pastes can -only be accessed by knowing their paste ids, they are not listed on any index, -since it isn’t technically possible (by design). - -All pastes are symetrically encrypted server-side with an HMAC derived key and -SHA-256 hashing, a server-side salt and a randomly generated password. Public -paste’s passwords are derived from their ids. Private paste’s passwords are -randomly generated and stored inside a symetrically encrypted personal -database, with the encryption key also being derived through the same HMAC -mechanism, where the HTTP basic authentication credentials act as the master -password. - -Paste ids, usernames, and any other identifiable attributes are only stored -inside storage backends as keyed and salted BLAKE2 hashes. - -The program supports output formatting for syntax highlighting (powered by +httpaste supports output formatting for syntax highlighting (powered by [pygments](https://pygments.org/)), as well as MIME type output manipulation, and input encoding. -The program can therefore serve as a minimalist, anonymous object storage for -small data. +Therefore httpaste can server as an anonymous object storage for small data. -Minute-based and ‘burn-after-read’ paste expiration are also supported. +Minute-based and ‘burn-after-read’ paste expiration are supported. -# Getting Started +httpaste focuses on security through cryptography, making it a computationally intensive application. + +# Get Started ## Install diff --git a/docs/README.rst b/docs/README.rst index 96f45f3..0bf034c 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -9,41 +9,28 @@ httpaste - versatile HTTP pastebin .. image:: _assets/images/favpng_parrot-royalty-free-cartoon.png -.. note:: - httpaste is publicly hosted at `httpaste.it`_ and as a hidden Tor service (``_). - Both services are to be considered evaluatory, as long as the source code - is in pre-release. Regarding voidance of pre-release status, see `Open Issues`_, for more information. +httpaste is a pastebin application for easily pasting and retrieving data over +HTTP from shell environments and web browsers. It is inspired by `sprunge.us`_ +and `ix.io`_, but focuses on extendability, advanced security, with little to +no trade-off to simplicity. It can be hosted through WSGI, CGI, Fast CGI, or +as a standalone evaluation server. It offers multiple storage backends, such as +a filesystem backend, SQLite backend, MySQL backend, or MongoDB backend. -This program offers an HTTP interface for storing public and private data -(a.k.a. pastes), commonly referred to as a pastebin application. It is inspired by `sprunge.us`_ and `ix.io`_. It can be hosted through WSGI, CGI, Fast -CGI, or as a standalone evaluation server. It offers multiple storage backends, -such as a filesystem backend, SQLite backend, or MySQL backend. +All pastes are being encrypted on the fly and can only be retrieved by an +authorized user, either through knowing the paste id of a public paste, or +having authentication credentials, as well as the paste id of a private paste. +This makes httpaste ideal as a pastebin for sensitive environments such as the +Tor network. Authentication credentials are created on-the-fly and don't require a sign-up process. -Public data can be accessed through an URL, where as private pastes -additionally require HTTP basic authentication. Creation of authentication -credentials happens on the fly, there is no sign-up process. Public pastes can -only be accessed by knowing their paste ids, they are not listed on any index, -since it isn't technically possible (by design). - -All pastes are symetrically encrypted server-side with an HMAC derived key and -SHA-256 hashing, a server-side salt and a randomly generated password. Public -paste's passwords are derived from their ids. Private paste's passwords are -randomly generated and stored inside a symetrically encrypted personal -database, with the encryption key also being derived through the same HMAC -mechanism, where the HTTP basic authentication credentials act as the master -password. - -Paste ids, usernames, and any other identifiable attributes are only stored -inside storage backends as keyed and salted BLAKE2 hashes. - -The program supports output formatting for syntax highlighting (powered by +httpaste supports output formatting for syntax highlighting (powered by `pygments`_), as well as MIME type output manipulation, and input encoding. -The program can therefore serve as a minimalist, anonymous object storage for -small data. +Therefore httpaste can server as an anonymous object storage for small data. -Minute-based and 'burn-after-read' paste expiration are also supported. +Minute-based and 'burn-after-read' paste expiration are supported. -.. include:: guide/getting-started.rst +httpaste focuses on security through cryptography, making it a computationally intensive application. + +.. include:: guide/get-started.rst Documentation ------------- @@ -82,8 +69,4 @@ This program uses licensed third-party software. .. _ix.io: http://ix.io/ .. _sprunge.us: http://sprunge.us .. _pygments: https://pygments.org/ -.. _icon: https://favpng.com/png_view/parrot-parrot-royalty-free-cartoon-png/gps7HM42 - -.. _Open Issues: https://victorykit.atlassian.net/issues/?jql=project%20%3D%20HTTPASTE%20AND%20fixVersion%20in%20(1.1.0-beta%2C%201.2.0-beta%2C%201.3.0) - -.. _httpaste.it: http://httpaste.it \ No newline at end of file +.. _icon: https://favpng.com/png_view/parrot-parrot-royalty-free-cartoon-png/gps7HM42 \ No newline at end of file diff --git a/docs/guide/getting-started.rst b/docs/guide/get-started.rst similarity index 97% rename from docs/guide/getting-started.rst rename to docs/guide/get-started.rst index 048f06a..0bbeedc 100644 --- a/docs/guide/getting-started.rst +++ b/docs/guide/get-started.rst @@ -1,5 +1,5 @@ -Getting Started -=============== +Get Started +=========== Install """"""" diff --git a/docs/index.rst b/docs/index.rst index 6b0a60b..bb9e5a0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,7 +4,7 @@ :maxdepth: 1 :caption: Guides - guide/getting-started + guide/get-started guide/advanced-usage guide/backend guide/cli diff --git a/pyproject.toml b/pyproject.toml index cb91714..9111d02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,7 @@ [build-system] requires = [ "setuptools", - "wheel", - "setuptools-scm[toml]" + "wheel" ] build-backend = "setuptools.build_meta" @@ -10,6 +9,4 @@ build-backend = "setuptools.build_meta" max_line_length = 80 aggressive = 3 recursive = true -in-place = true - -[tool.setuptools_scm] +in-place = true \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 465a26b..d330606 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [metadata] name = httpaste-victorykit +version = 1.0.1-alpha author = Tiara Rodney author_email = t.rodney@victoryk.it description = a versatile HTTP pastebin @@ -36,8 +37,3 @@ console_scripts = [options.packages.find] where = src - -[options.package_data] -* = - *.json - *.sql \ No newline at end of file diff --git a/src/httpaste/__init__.py b/src/httpaste/__init__.py index 14f2385..6eea5ba 100755 --- a/src/httpaste/__init__.py +++ b/src/httpaste/__init__.py @@ -143,8 +143,6 @@ from inspect import isclass from configparser import ConfigParser from ast import literal_eval from io import StringIO -from os import environ -from importlib.resources import path as resource_path from connexion import FlaskApp from connexion.resolver import RestyResolver @@ -160,7 +158,7 @@ from httpaste.helper.http import ( UnauthorizedError) -CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIGPATH' +CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIG' def get_sanitized_config_charset(charset: str): @@ -200,17 +198,17 @@ class ServerConfig: bind_address = None -def get_config_path(var_name: str = CONFIGPATH_ENVIRON): +def get_config_path(environ: str = CONFIGPATH_ENVIRON): """ """ try: - return environ[var_name] + return os.environ[environ] except KeyError as e: raise ConfigError( - f'environment variable \'{var_name}\' not set.') from e + 'environment variable \'{environ}\' not set.') from e def load_config(path: str) -> Tuple[Config, ServerConfig]: @@ -302,16 +300,13 @@ def get_flask_app( options = {"swagger_ui": server_config.swagger_ui} - #context manager returns a pathlib.Path object - with resource_path('httpaste.schema', 'httpaste.openapi.json') as path: + application = FlaskApp(__name__, specification_dir='schema/') - application = FlaskApp(__name__, specification_dir=path.parent) - - application.add_api( - path.name, - options=options, - resolver=RestyResolver('httpaste.controller') - ) + application.add_api( + 'httpaste.openapi.json', + options=options, + resolver=RestyResolver('httpaste.controller') + ) for err_cls in [ BadRequestError, @@ -327,14 +322,6 @@ def get_flask_app( with application.app.app_context(): application.app.httpaste = config - #add header for browsers to present a sign-in prompt - @application.app.after_request - def rewrite_forbidden_request(response): - - if response.status_code in [401]: - response.headers['WWW-Authenticate'] = 'Basic realm="private"' - return response - return application diff --git a/src/httpaste/__main__.py b/src/httpaste/__main__.py index ec35404..399417f 100644 --- a/src/httpaste/__main__.py +++ b/src/httpaste/__main__.py @@ -2,7 +2,6 @@ """ import argparse import os -from importlib.resources import open_text def _this_dir(basename: str) -> str: @@ -21,7 +20,7 @@ def _path_output(path, echo: bool = False) -> str: return path else: - with open_text('httpaste', path) as fh: + with open(path, 'r') as fh: return fh.read() @@ -31,14 +30,7 @@ def command_standalone(**kwargs): """ from httpaste import load_config, get_flask_app - - try: - from gevent.pywsgi import WSGIServer - except ImportError as e: - raise ImportError(' '.join(( - 'gevent is currently not installed.', - 'Please install it by running \'python3 -m pip install gevent\'.' - ))) from e + from gevent.pywsgi import WSGIServer config, server_config = load_config(kwargs.get('config_path')) @@ -52,21 +44,21 @@ def command_wsgi(**kwargs): """get WSGI script """ - print(_path_output('wsgi.py', kwargs.get('echo'))) + print(_path_output(_this_dir('wsgi.py'), kwargs.get('echo'))) def command_cgi(**kwargs): """get CGI script """ - print(_path_output('cgi.py', kwargs.get('echo'))) + print(_path_output(_this_dir('cgi.py'), kwargs.get('echo'))) def command_fcgi(**kwargs): """get FastCGI script """ - print(_path_output('fcgi.py', kwargs.get('echo'))) + print(_path_output(_this_dir('fcgi.py'), kwargs.get('echo'))) def command_default_config(**kwargs): @@ -100,25 +92,11 @@ def command_init_backend(**kwargs): config.backend.paste.init() -def command_sanitize_backend(**kwargs): - """sanitize the backend - """ - - from httpaste import load_config - - config, _ = load_config(kwargs.get('config')) - - config, _ = load_config(kwargs.get('config')) - - config.backend.user.sanitize() - config.backend.paste.sanitize() - - def parser(): p = argparse.ArgumentParser(description='Process some integers.') - sp = p.add_subparsers(dest='command', required=True) + sp = p.add_subparsers(dest='command') p_standalone = sp.add_parser('standalone', help=command_standalone.__doc__) p_standalone.add_argument('--config-path', '-c', required=True) @@ -143,11 +121,6 @@ def parser(): help=command_init_backend.__doc__) p_init_backend.add_argument('--config', '-c', required=True) - p_sanitize_backend = sp.add_parser( - 'sanitize-backend', - help=command_sanitize_backend.__doc__) - p_sanitize_backend.add_argument('--config', '-c', required=True) - return p @@ -163,8 +136,7 @@ def main(): 'cgi': command_cgi, 'fcgi': command_fcgi, 'default-config': command_default_config, - 'init-backend': command_init_backend, - 'sanitize-backend': command_sanitize_backend + 'init-backend': command_init_backend }[kwargs.pop('command')](**kwargs) diff --git a/src/httpaste/backend/__init__.py b/src/httpaste/backend/__init__.py index 48978a0..2e176d9 100644 --- a/src/httpaste/backend/__init__.py +++ b/src/httpaste/backend/__init__.py @@ -26,7 +26,7 @@ class SQLite(Backend): def __init__(self, parameters: SqliteParameters): - parameters = SqliteParameters(parameters.path, get_sqlite_connection(parameters)) + parameters['connection'] = get_sqlite_connection(parameters) self.user = SqliteUser(parameters, User) self.paste = SqlitePaste(parameters, Paste) diff --git a/src/httpaste/backend/file/__init__.py b/src/httpaste/backend/file/__init__.py index 9352f8f..a8d1126 100644 --- a/src/httpaste/backend/file/__init__.py +++ b/src/httpaste/backend/file/__init__.py @@ -58,13 +58,6 @@ class User(object): return user.init(self.path) - def sanitize(self): - - if self.path.exists(): - return user.sanitize(self.path, self.model_class, self.model_schema) - - return None - class Paste(object): """Filesystem paste model backend @@ -103,10 +96,3 @@ class Paste(object): def init(self): return paste.init(self.path) - - def sanitize(self): - - if self.path.exists(): - return paste.sanitize(self.path, self.model_class, self.model_schema) - - return None \ No newline at end of file diff --git a/src/httpaste/backend/file/paste.py b/src/httpaste/backend/file/paste.py index dbeb341..d27b7ca 100644 --- a/src/httpaste/backend/file/paste.py +++ b/src/httpaste/backend/file/paste.py @@ -5,16 +5,6 @@ acting as cells. """ from pathlib import Path from ast import literal_eval -from time import time - - -COLUMNS = [ - 'data', - 'data_hash', - 'sub', - 'expiration', - 'encoding' -] def load( @@ -32,7 +22,13 @@ def load( return None cells = {} - for column in COLUMNS: + for column in [ + 'data', + 'data_hash', + 'sub', + 'timestamp', + 'lifetime', + 'encoding']: cell = row.joinpath(column) @@ -60,7 +56,8 @@ def load( cells['sub'], cells['data'], cells['data_hash'], - cells['expiration'], + cells['timestamp'], + cells['lifetime'], cells['encoding']) @@ -71,7 +68,13 @@ def dump(model: object, path: Path, model_schema: type) -> None: row = path.joinpath(model.pid.hex()) row.mkdir(parents=True, exist_ok=True) - for column in COLUMNS: + for column in [ + 'data', + 'data_hash', + 'sub', + 'timestamp', + 'lifetime', + 'encoding']: cell = row.joinpath(column) cell_schema = getattr(model_schema, column) @@ -99,22 +102,6 @@ def init(path: Path): return None -def sanitize(path: Path, model_class: type, model_schema: type): - - for row in path.iterdir(): - - expiration_cell = row.joinpath('expiration') - - if not expiration_cell.exists(): - continue - - expiration = literal_eval(expiration_cell.read_text()) - - if expiration < int(time()) and expiration > 0: - - delete(model_class(bytes.fromhex(row.name)), path) - - def _rm_tree(pth: Path): for child in pth.iterdir(): if child.is_file(): diff --git a/src/httpaste/backend/file/user.py b/src/httpaste/backend/file/user.py index 21e32d0..02255b2 100644 --- a/src/httpaste/backend/file/user.py +++ b/src/httpaste/backend/file/user.py @@ -6,12 +6,6 @@ acting as cells. from pathlib import Path from ast import literal_eval -COLUMNS = [ - 'sub', - 'key_hash', - 'index', -] - def load( proto: object, @@ -28,17 +22,15 @@ def load( return None cells = {} - for column in COLUMNS[1:]: + for column in ['key_hash', 'index']: cell = row.joinpath(column) - if not cell.exists(): - cells[column] = None - elif getattr(model_schema, column) == bytes: + if getattr(model_schema, column) == bytes: + cells[column] = cell.read_bytes() - elif getattr(model_schema, column) == str: - cells[column] = cell.read_text() else: + cells[column] = literal_eval(cell.read_text()) return model_class( @@ -54,7 +46,7 @@ def dump(model: object, path: Path, model_schema: object) -> None: row = path.joinpath(model.sub.hex()) row.mkdir(parents=True, exist_ok=True) - for column in COLUMNS[1:]: + for column in ['key_hash', 'index']: cell = row.joinpath(column) @@ -82,11 +74,6 @@ def init(path: Path): return None -def sanitize(path: Path, model_class: type, model_schema: type): - - return None - - def _rm_tree(pth: Path): for child in pth.iterdir(): if child.is_file(): diff --git a/src/httpaste/backend/sqlite/__init__.py b/src/httpaste/backend/sqlite/__init__.py index 5680f5c..8bd14e5 100644 --- a/src/httpaste/backend/sqlite/__init__.py +++ b/src/httpaste/backend/sqlite/__init__.py @@ -45,10 +45,6 @@ class User(object): return user.init(self.connection) - def sanitize(self): - - return user.sanitize(self.connection, self.model_class) - class Paste(object): """SQLite paste model backend @@ -78,10 +74,6 @@ class Paste(object): return paste.init(self.connection) - def sanitize(self): - - return paste.sanitize(self.connection, self.model_class) - def get_connection(parameters: Parameters): """get an sqlite connection object diff --git a/src/httpaste/backend/sqlite/paste.py b/src/httpaste/backend/sqlite/paste.py index 51c9fc6..61f8cd1 100644 --- a/src/httpaste/backend/sqlite/paste.py +++ b/src/httpaste/backend/sqlite/paste.py @@ -2,8 +2,6 @@ """ from os import path from sqlite3 import Connection -from time import time -from importlib.resources import open_text def load(proto: object, connection: Connection, model_class: type): @@ -13,7 +11,7 @@ def load(proto: object, connection: Connection, model_class: type): cur = connection.cursor() cur.execute( - 'SELECT pid, data, data_hash, sub, expiration, encoding FROM pastes WHERE pid=?', + 'SELECT pid, data, data_hash, sub, timestamp, lifetime, encoding FROM pastes WHERE pid=?', (proto.pid, )) @@ -26,7 +24,8 @@ def load(proto: object, connection: Connection, model_class: type): result['sub'], result['data'], result['data_hash'], - result['expiration'], + result['timestamp'], + result['lifetime'], result['encoding']) return None @@ -39,13 +38,14 @@ def dump(model: object, connection: Connection): cur = connection.cursor() cur.execute( - '''INSERT INTO pastes (pid, data, data_hash, sub, expiration, encoding) - VALUES (?,?,?,?,?,?)''', + '''INSERT INTO pastes (pid, data, data_hash, sub, timestamp, lifetime, encoding) + VALUES (?,?,?,?,?,?,?)''', (model.pid, model.data, model.data_hash, model.sub, - model.expiration, + model.timestamp, + model.lifetime, model.encoding)) connection.commit() @@ -64,19 +64,8 @@ def init(connection: Connection): cur = connection.cursor() - with open_text('httpaste.backend.sqlite', 'paste.sql') as fh: + with open(path.join(path.dirname(__file__), 'paste.sql'), 'r') as fh: cur.execute(fh.read()) connection.commit() - - -def sanitize(connection: Connection, model_class: type) -> bool: - - cur = connection.cursor() - - cur.execute('''SELECT pid FROM pastes WHERE expiration < ? AND expiration > 0''', (int(time()),)) - - for row in cur.fetchall(): - - delete(model_class(row['pid'])) \ No newline at end of file diff --git a/src/httpaste/backend/sqlite/paste.sql b/src/httpaste/backend/sqlite/paste.sql index 7f9bb46..5788d5b 100644 --- a/src/httpaste/backend/sqlite/paste.sql +++ b/src/httpaste/backend/sqlite/paste.sql @@ -1,9 +1,10 @@ CREATE TABLE IF NOT EXISTS "pastes" ( - "pid" BLOB NOT NULL UNIQUE, + "id" BLOB NOT NULL UNIQUE, "data" BLOB NOT NULL, "data_hash" BLOB NOT NULL, "sub" BLOB UNIQUE, - "expiration" INTEGER NOT NULL, - "encoding" TEXT, - PRIMARY KEY("pid") + "timestamp" INTEGER NOT NULL, + "lifetime" INTEGER NOT NULL, + "encoding" TEXT + PRIMARY KEY("id") ); \ No newline at end of file diff --git a/src/httpaste/backend/sqlite/user.py b/src/httpaste/backend/sqlite/user.py index e4f712d..423b797 100644 --- a/src/httpaste/backend/sqlite/user.py +++ b/src/httpaste/backend/sqlite/user.py @@ -3,7 +3,6 @@ from os import path from sqlite3 import Connection from httpaste.model import User -from importlib.resources import open_text def load(proto: User, connection: Connection): @@ -49,13 +48,8 @@ def init(connection: Connection): cur = connection.cursor() - with open_text('httpaste.backend.sqlite', 'user.sql') as fh: + with open(path.join(path.dirname(__file__), 'user.sql'), 'r') as fh: cur.execute(fh.read()) connection.commit() - - -def sanitize(connection: Connection, model_class) -> bool: - - return None \ No newline at end of file diff --git a/src/httpaste/controller/paste/__init__.py b/src/httpaste/controller/paste/__init__.py index c8f4b87..fcd6ab6 100644 --- a/src/httpaste/controller/paste/__init__.py +++ b/src/httpaste/controller/paste/__init__.py @@ -71,7 +71,7 @@ def get(**kwargs): config.salt, config.hmac_iterations) try: - data, expiration, encoding = call() + data, lifetime, encoding = call() except paste_model.LifetimeError as e: if kwargs.get('user') is not None: paste_model.remove_safe(pid, sub, pkey, config.backend.paste, @@ -85,7 +85,7 @@ def get(**kwargs): raise ForbiddenError(str(e)) # burn after read - if expiration < 0: + if lifetime < 0: if kwargs.get('user') is not None: paste_model.remove_safe(pid, sub, pkey, config.backend.paste, config.salt, config.hmac_iterations) diff --git a/src/httpaste/helper/common.py b/src/httpaste/helper/common.py index 2e6d1c7..92cc6d7 100644 --- a/src/httpaste/helper/common.py +++ b/src/httpaste/helper/common.py @@ -1,10 +1,6 @@ from random import choice from base64 import b64decode from urllib.parse import urljoin -from tempfile import mkdtemp -from pathlib import Path -from contextlib import contextmanager - class DecodeError(Exception): """ @@ -33,4 +29,4 @@ def decode(data: str, encoding: str) -> bytes: def join_url(base:str, url: str) -> str: - return urljoin(base, url, True) + return urljoin(base, url, True) \ No newline at end of file diff --git a/src/httpaste/model/__init__.py b/src/httpaste/model/__init__.py index 7d1280f..955ded4 100644 --- a/src/httpaste/model/__init__.py +++ b/src/httpaste/model/__init__.py @@ -1,6 +1,6 @@ """Model """ -from typing import NamedTuple, Optional, Dict, Union, Any, TypedDict +from typing import NamedTuple, Optional, Dict, Union class PasteDataSchema: @@ -12,7 +12,6 @@ class PasteDataSchema: sub = bytes timestamp = int lifetime = int - expiration = int encoding = str @@ -55,14 +54,6 @@ class PasteEncoding(PasteDataSchema.encoding): """ -class PasteExpiration(PasteDataSchema.expiration): - """Paste Expiration - - < 0: after first acccess - 0: never - """ - - class PasteLifetime(PasteDataSchema.lifetime): """Paste Lifetime """ @@ -98,11 +89,9 @@ class Sub(UserDataSchema.sub): """ -class Index(TypedDict): +class Index(Dict[PasteId, PasteKey]): """User Paste Index """ - auth_expires: int - pastes: Dict[str, Dict[str, Any]] class SerializedIndex(UserDataSchema.index): @@ -139,6 +128,8 @@ class Paste(NamedTuple): #: paste data hash data_hash: Optional[PasteHash] = None #: paste timestamp - expiration: Optional[PasteExpiration] = None + timestamp: Optional[PasteTimestamp] = None + #: paste lifetime + lifetime: Optional[PasteLifetime] = None #: paste encoding encoding: Optional[PasteEncoding] = None diff --git a/src/httpaste/model/paste.py b/src/httpaste/model/paste.py index f7f856d..5ad9dc8 100755 --- a/src/httpaste/model/paste.py +++ b/src/httpaste/model/paste.py @@ -10,7 +10,7 @@ from httpaste.helper.crypto import dhash, shash, encrypt, decrypt from httpaste.helper.common import generate_random_string from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt, PasteData, PasteHash, PasteTimestamp, PasteSub, - PasteLifetime, PasteEncoding, PasteExpiration) + PasteLifetime, PasteEncoding) class NotFoundError(Exception): @@ -87,7 +87,8 @@ def load(proto: Paste, backend: object) -> Optional[Paste]: raise SubError('Paste not owned by user') - if model.expiration > 0 and model.expiration < int(time.time()): + if model.lifetime >= 0 and model.timestamp + \ + (60 * model.lifetime) < int(time.time()): raise LifetimeError('Paste expired') @@ -120,7 +121,8 @@ def load_safe( proto.sub, data, model.data_hash, - model.expiration, + model.timestamp, + model.lifetime, model.encoding) @@ -193,11 +195,6 @@ def create( sub = None timestamp = PasteTimestamp(int(time.time())) - if lifetime < 0: - expiration = -1 - else: - expiration = PasteExpiration(timestamp + (lifetime * 60)) - safe_data = PasteData(encrypt(data, pid, salt, hmac_iter)) model = Paste( @@ -205,7 +202,8 @@ def create( sub, safe_data, data_hash, - expiration, + timestamp, + lifetime, encoding) dump(model, backend) @@ -236,11 +234,6 @@ def create_safe(data: PasteData, safe_sub = PasteSub(shash(sub, data_hash, pid)) timestamp = PasteTimestamp(int(time.time())) - if lifetime < 0: - expiration = -1 - else: - expiration = PasteExpiration(timestamp + (lifetime * 60)) - safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter)) dump(Paste( @@ -248,7 +241,8 @@ def create_safe(data: PasteData, safe_sub, safe_data, data_hash, - expiration, + timestamp, + lifetime, encoding ), backend) @@ -285,7 +279,7 @@ def get(pid: PasteId, backend: object, salt: Salt = Config.salt, hmac_iter: int data = decrypt(model.data, pid, salt, hmac_iter) - return PasteData(data), model.expiration, model.encoding + return PasteData(data), model.lifetime, model.encoding def get_safe( @@ -300,4 +294,4 @@ def get_safe( model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter) - return PasteData(model.data), model.expiration, model.encoding + return PasteData(model.data), model.lifetime, model.encoding diff --git a/src/httpaste/model/user.py b/src/httpaste/model/user.py index b925063..7639127 100755 --- a/src/httpaste/model/user.py +++ b/src/httpaste/model/user.py @@ -2,7 +2,6 @@ """user model interface """ import json -from time import time from typing import Optional from httpaste import Config @@ -35,7 +34,7 @@ class IndexError(Exception): """ -def _load( +def load( proto: User, master_key: str, backend: object, @@ -55,19 +54,16 @@ def _load( return None try: - serialized_data = decrypt(model.index, master_key, salt, hmac_iter) + return User( + *model[:-1], + Index(**json.loads(decrypt(model.index, master_key, salt, hmac_iter))) + ) except DecryptionError as e: + raise IndexError('unable to decrypt user index') from e - else: - data = json.loads(serialized_data) - - return User( - *model[:-1], - Index(**data) - ) -def _dump( +def dump( model: User, key: MasterKey, backend: object, @@ -81,7 +77,7 @@ def _dump( :param salt: randomization salt """ - if model.index is not None and not isinstance(model.index, dict): + if not isinstance(model.index, Index): raise BaseException('index serialization pre-processing not allowed.') @@ -106,13 +102,11 @@ def load_paste_key( :param salt: randomization salt """ - model = _load(User(sub), key, backend, salt, hmac_iter) - - for k, v in model.index.get('pastes').items(): + for k, v in load(User(sub), key, backend, salt, hmac_iter).index.items(): if bytes.fromhex(k) == pid: - return PasteKey(bytes.fromhex(v.get('key'))) + return PasteKey(bytes.fromhex(v)) return None @@ -134,13 +128,12 @@ def dump_paste_key( :param backend: user model backend """ - model = _load(User(sub), key, backend, salt, hmac_iter) + model = load(User(sub), key, backend, salt, hmac_iter) - model.index.setdefault('pastes', {})[pid.hex()] = { - 'key': pkey.hex() - } - - _dump(model, key, backend, salt, hmac_iter) + dump(User(*model[:-1], Index({ + **model.index, + **{pid.hex(): pkey.hex()} + })), key, backend, salt, hmac_iter) def authenticate( @@ -161,36 +154,22 @@ def authenticate( proto = User(sub) - bogus_decline_msg = 'unable to authenticate' - try: - model = _load(proto, key, backend, salt, hmac_iter) + model = load(proto, key, backend, salt, hmac_iter) except IndexError as e: - raise AuthenticationError(bogus_decline_msg) from e + raise AuthenticationError('you dun goofed') if not model: - data = { - 'auth_expires': int(time()) + (1 * 60) - } - - model = User(sub, key_hash, Index(data)) - _dump(model, key, backend, salt, hmac_iter) + model = User(sub, key_hash, Index({})) + dump(model, key, backend, salt, hmac_iter) else: if model.key_hash != key_hash: - raise AuthenticationError(bogus_decline_msg) + raise AuthenticationError('you dun goofed') return { 'sub': sub, 'master_key': key } - - -__all__ = [ - AuthenticationError, - load_paste_key, - dump_paste_key, - authenticate -] \ No newline at end of file diff --git a/src/httpaste/schema/__init__.py b/src/httpaste/schema/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/httpaste/schema/httpaste.openapi.json b/src/httpaste/schema/httpaste.openapi.json index 8f55586..163a57e 100644 --- a/src/httpaste/schema/httpaste.openapi.json +++ b/src/httpaste/schema/httpaste.openapi.json @@ -161,15 +161,6 @@ }, { "$ref": "#/components/parameters/syntax" - }, - { - "$ref": "#/components/parameters/format" - }, - { - "$ref": "#/components/parameters/linenos" - }, - { - "$ref": "#/components/parameters/mime" } ], "responses": { diff --git a/src/httpaste/wsgi.py b/src/httpaste/wsgi.py index 9e43ff0..e45f821 100755 --- a/src/httpaste/wsgi.py +++ b/src/httpaste/wsgi.py @@ -3,6 +3,6 @@ """ from httpaste import load_config, get_flask_app, get_config_path -config, server_config = load_config(get_config_path()) +config, server_config = load_config(get_config_path) application = get_flask_app(config, server_config)