Compare commits
41 commits
v1.0.1-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c6165871f | ||
|
|
89c1a67a47 | ||
|
|
0119bae329 | ||
|
|
f3a3e95163 | ||
|
|
95761b9de5 | ||
|
|
2762d6d67f | ||
|
|
c8dffbbdf8 | ||
|
|
a4116832e7 | ||
|
|
c888f22b93 | ||
|
|
ecd7d03a20 | ||
|
|
7a7b70422f | ||
|
|
e330bbf70a | ||
|
|
94f9063e1d | ||
|
|
65f5077095 | ||
|
|
e6c0371dcb | ||
|
|
fc367ed4cb | ||
|
|
88851970d2 | ||
|
|
bdefd7e592 | ||
|
|
fedd42933c | ||
|
|
6e0229f34f | ||
|
|
dfff5f1523 | ||
|
|
fdf69d37db | ||
|
|
d367d43524 | ||
|
|
55ec3a41e8 | ||
|
|
6c838cd2cf | ||
|
|
6bef30f1f3 | ||
|
|
c88e3ab17e | ||
|
|
50af1a4587 | ||
|
|
678935bd0e | ||
|
|
45f53223b0 | ||
|
|
3c342caa27 | ||
|
|
6c1ffdcc0c | ||
|
|
ab0d3d0805 | ||
|
|
268bfaacc4 | ||
|
|
dc5c644f29 | ||
|
|
27bc790d5d | ||
|
|
83fee6293f | ||
|
|
6bf785f36c | ||
|
|
0612fd43ee | ||
|
|
bd7af05850 | ||
|
|
f0c0188d58 |
25 changed files with 327 additions and 135 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -9,4 +9,6 @@
|
||||||
**/__pycache__/
|
**/__pycache__/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.coverage
|
.coverage
|
||||||
/*.md
|
/*.md
|
||||||
|
/.eggs/
|
||||||
|
/devel/
|
||||||
46
README.md
46
README.md
|
|
@ -2,28 +2,40 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
httpaste is a pastebin application for easily pasting and retrieving data over
|
**NOTE**: httpaste is publicly hosted at [httpaste.it](http://httpaste.it) and as a hidden Tor service ([https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion](https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion)).
|
||||||
HTTP from shell environments and web browsers. It is inspired by [sprunge.us](http://sprunge.us)
|
Both services are to be considered evaluatory, as long as the source code
|
||||||
and [ix.io](http://ix.io/), but focuses on extendability, advanced security, with little to
|
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.
|
||||||
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.
|
|
||||||
|
|
||||||
All pastes are being encrypted on the fly and can only be retrieved by an
|
This program offers an HTTP interface for storing public and private data
|
||||||
authorized user, either through knowing the paste id of a public paste, or
|
(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
|
||||||
having authentication credentials, as well as the paste id of a private paste.
|
CGI, or as a standalone evaluation server. It offers multiple storage backends,
|
||||||
This makes httpaste ideal as a pastebin for sensitive environments such as the
|
such as a filesystem backend, SQLite backend, or MySQL backend.
|
||||||
Tor network. Authentication credentials are created on-the-fly and don’t require a sign-up process.
|
|
||||||
|
|
||||||
httpaste supports output formatting for syntax highlighting (powered by
|
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
|
||||||
[pygments](https://pygments.org/)), as well as MIME type output manipulation, and input encoding.
|
[pygments](https://pygments.org/)), as well as MIME type output manipulation, and input encoding.
|
||||||
Therefore httpaste can server as an anonymous object storage for small data.
|
The program can therefore serve as a minimalist, anonymous object storage for
|
||||||
|
small data.
|
||||||
|
|
||||||
Minute-based and ‘burn-after-read’ paste expiration are supported.
|
Minute-based and ‘burn-after-read’ paste expiration are also supported.
|
||||||
|
|
||||||
httpaste focuses on security through cryptography, making it a computationally intensive application.
|
# Getting Started
|
||||||
|
|
||||||
# Get Started
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,28 +9,41 @@ httpaste - versatile HTTP pastebin
|
||||||
|
|
||||||
.. image:: _assets/images/favpng_parrot-royalty-free-cartoon.png
|
.. image:: _assets/images/favpng_parrot-royalty-free-cartoon.png
|
||||||
|
|
||||||
httpaste is a pastebin application for easily pasting and retrieving data over
|
.. note::
|
||||||
HTTP from shell environments and web browsers. It is inspired by `sprunge.us`_
|
httpaste is publicly hosted at `httpaste.it`_ and as a hidden Tor service (`<https://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion>`_).
|
||||||
and `ix.io`_, but focuses on extendability, advanced security, with little to
|
Both services are to be considered evaluatory, as long as the source code
|
||||||
no trade-off to simplicity. It can be hosted through WSGI, CGI, Fast CGI, or
|
is in pre-release. Regarding voidance of pre-release status, see `Open Issues`_, for more information.
|
||||||
as a standalone evaluation server. It offers multiple storage backends, such as
|
|
||||||
a filesystem backend, SQLite backend, MySQL backend, or MongoDB backend.
|
|
||||||
|
|
||||||
All pastes are being encrypted on the fly and can only be retrieved by an
|
This program offers an HTTP interface for storing public and private data
|
||||||
authorized user, either through knowing the paste id of a public paste, or
|
(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
|
||||||
having authentication credentials, as well as the paste id of a private paste.
|
CGI, or as a standalone evaluation server. It offers multiple storage backends,
|
||||||
This makes httpaste ideal as a pastebin for sensitive environments such as the
|
such as a filesystem backend, SQLite backend, or MySQL backend.
|
||||||
Tor network. Authentication credentials are created on-the-fly and don't require a sign-up process.
|
|
||||||
|
|
||||||
httpaste supports output formatting for syntax highlighting (powered by
|
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
|
||||||
`pygments`_), as well as MIME type output manipulation, and input encoding.
|
`pygments`_), as well as MIME type output manipulation, and input encoding.
|
||||||
Therefore httpaste can server as an anonymous object storage for small data.
|
The program can therefore serve as a minimalist, anonymous object storage for
|
||||||
|
small data.
|
||||||
|
|
||||||
Minute-based and 'burn-after-read' paste expiration are supported.
|
Minute-based and 'burn-after-read' paste expiration are also supported.
|
||||||
|
|
||||||
httpaste focuses on security through cryptography, making it a computationally intensive application.
|
.. include:: guide/getting-started.rst
|
||||||
|
|
||||||
.. include:: guide/get-started.rst
|
|
||||||
|
|
||||||
Documentation
|
Documentation
|
||||||
-------------
|
-------------
|
||||||
|
|
@ -69,4 +82,8 @@ This program uses licensed third-party software.
|
||||||
.. _ix.io: http://ix.io/
|
.. _ix.io: http://ix.io/
|
||||||
.. _sprunge.us: http://sprunge.us
|
.. _sprunge.us: http://sprunge.us
|
||||||
.. _pygments: https://pygments.org/
|
.. _pygments: https://pygments.org/
|
||||||
.. _icon: https://favpng.com/png_view/parrot-parrot-royalty-free-cartoon-png/gps7HM42
|
.. _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
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
Get Started
|
Getting Started
|
||||||
===========
|
===============
|
||||||
|
|
||||||
Install
|
Install
|
||||||
"""""""
|
"""""""
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
:caption: Guides
|
:caption: Guides
|
||||||
|
|
||||||
guide/get-started
|
guide/getting-started
|
||||||
guide/advanced-usage
|
guide/advanced-usage
|
||||||
guide/backend
|
guide/backend
|
||||||
guide/cli
|
guide/cli
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools",
|
"setuptools",
|
||||||
"wheel"
|
"wheel",
|
||||||
|
"setuptools-scm[toml]"
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
|
@ -9,4 +10,6 @@ build-backend = "setuptools.build_meta"
|
||||||
max_line_length = 80
|
max_line_length = 80
|
||||||
aggressive = 3
|
aggressive = 3
|
||||||
recursive = true
|
recursive = true
|
||||||
in-place = true
|
in-place = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
[metadata]
|
[metadata]
|
||||||
name = httpaste-victorykit
|
name = httpaste-victorykit
|
||||||
version = 1.0.1-alpha
|
|
||||||
author = Tiara Rodney
|
author = Tiara Rodney
|
||||||
author_email = t.rodney@victoryk.it
|
author_email = t.rodney@victoryk.it
|
||||||
description = a versatile HTTP pastebin
|
description = a versatile HTTP pastebin
|
||||||
|
|
@ -37,3 +36,8 @@ console_scripts =
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
where = src
|
where = src
|
||||||
|
|
||||||
|
[options.package_data]
|
||||||
|
* =
|
||||||
|
*.json
|
||||||
|
*.sql
|
||||||
|
|
@ -143,6 +143,8 @@ from inspect import isclass
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
from os import environ
|
||||||
|
from importlib.resources import path as resource_path
|
||||||
|
|
||||||
from connexion import FlaskApp
|
from connexion import FlaskApp
|
||||||
from connexion.resolver import RestyResolver
|
from connexion.resolver import RestyResolver
|
||||||
|
|
@ -158,7 +160,7 @@ from httpaste.helper.http import (
|
||||||
UnauthorizedError)
|
UnauthorizedError)
|
||||||
|
|
||||||
|
|
||||||
CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIG'
|
CONFIGPATH_ENVIRON = 'HTTPASTE_CONFIGPATH'
|
||||||
|
|
||||||
|
|
||||||
def get_sanitized_config_charset(charset: str):
|
def get_sanitized_config_charset(charset: str):
|
||||||
|
|
@ -198,17 +200,17 @@ class ServerConfig:
|
||||||
bind_address = None
|
bind_address = None
|
||||||
|
|
||||||
|
|
||||||
def get_config_path(environ: str = CONFIGPATH_ENVIRON):
|
def get_config_path(var_name: str = CONFIGPATH_ENVIRON):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
return os.environ[environ]
|
return environ[var_name]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
|
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
'environment variable \'{environ}\' not set.') from e
|
f'environment variable \'{var_name}\' not set.') from e
|
||||||
|
|
||||||
|
|
||||||
def load_config(path: str) -> Tuple[Config, ServerConfig]:
|
def load_config(path: str) -> Tuple[Config, ServerConfig]:
|
||||||
|
|
@ -300,13 +302,16 @@ def get_flask_app(
|
||||||
|
|
||||||
options = {"swagger_ui": server_config.swagger_ui}
|
options = {"swagger_ui": server_config.swagger_ui}
|
||||||
|
|
||||||
application = FlaskApp(__name__, specification_dir='schema/')
|
#context manager returns a pathlib.Path object
|
||||||
|
with resource_path('httpaste.schema', 'httpaste.openapi.json') as path:
|
||||||
|
|
||||||
application.add_api(
|
application = FlaskApp(__name__, specification_dir=path.parent)
|
||||||
'httpaste.openapi.json',
|
|
||||||
options=options,
|
application.add_api(
|
||||||
resolver=RestyResolver('httpaste.controller')
|
path.name,
|
||||||
)
|
options=options,
|
||||||
|
resolver=RestyResolver('httpaste.controller')
|
||||||
|
)
|
||||||
|
|
||||||
for err_cls in [
|
for err_cls in [
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
|
|
@ -322,6 +327,14 @@ def get_flask_app(
|
||||||
with application.app.app_context():
|
with application.app.app_context():
|
||||||
application.app.httpaste = config
|
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
|
return application
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
from importlib.resources import open_text
|
||||||
|
|
||||||
|
|
||||||
def _this_dir(basename: str) -> str:
|
def _this_dir(basename: str) -> str:
|
||||||
|
|
@ -20,7 +21,7 @@ def _path_output(path, echo: bool = False) -> str:
|
||||||
return path
|
return path
|
||||||
else:
|
else:
|
||||||
|
|
||||||
with open(path, 'r') as fh:
|
with open_text('httpaste', path) as fh:
|
||||||
|
|
||||||
return fh.read()
|
return fh.read()
|
||||||
|
|
||||||
|
|
@ -30,7 +31,14 @@ def command_standalone(**kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from httpaste import load_config, get_flask_app
|
from httpaste import load_config, get_flask_app
|
||||||
from gevent.pywsgi import WSGIServer
|
|
||||||
|
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
|
||||||
|
|
||||||
config, server_config = load_config(kwargs.get('config_path'))
|
config, server_config = load_config(kwargs.get('config_path'))
|
||||||
|
|
||||||
|
|
@ -44,21 +52,21 @@ def command_wsgi(**kwargs):
|
||||||
"""get WSGI script
|
"""get WSGI script
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(_path_output(_this_dir('wsgi.py'), kwargs.get('echo')))
|
print(_path_output('wsgi.py', kwargs.get('echo')))
|
||||||
|
|
||||||
|
|
||||||
def command_cgi(**kwargs):
|
def command_cgi(**kwargs):
|
||||||
"""get CGI script
|
"""get CGI script
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(_path_output(_this_dir('cgi.py'), kwargs.get('echo')))
|
print(_path_output('cgi.py', kwargs.get('echo')))
|
||||||
|
|
||||||
|
|
||||||
def command_fcgi(**kwargs):
|
def command_fcgi(**kwargs):
|
||||||
"""get FastCGI script
|
"""get FastCGI script
|
||||||
"""
|
"""
|
||||||
|
|
||||||
print(_path_output(_this_dir('fcgi.py'), kwargs.get('echo')))
|
print(_path_output('fcgi.py', kwargs.get('echo')))
|
||||||
|
|
||||||
|
|
||||||
def command_default_config(**kwargs):
|
def command_default_config(**kwargs):
|
||||||
|
|
@ -92,11 +100,25 @@ def command_init_backend(**kwargs):
|
||||||
config.backend.paste.init()
|
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():
|
def parser():
|
||||||
|
|
||||||
p = argparse.ArgumentParser(description='Process some integers.')
|
p = argparse.ArgumentParser(description='Process some integers.')
|
||||||
|
|
||||||
sp = p.add_subparsers(dest='command')
|
sp = p.add_subparsers(dest='command', required=True)
|
||||||
|
|
||||||
p_standalone = sp.add_parser('standalone', help=command_standalone.__doc__)
|
p_standalone = sp.add_parser('standalone', help=command_standalone.__doc__)
|
||||||
p_standalone.add_argument('--config-path', '-c', required=True)
|
p_standalone.add_argument('--config-path', '-c', required=True)
|
||||||
|
|
@ -121,6 +143,11 @@ def parser():
|
||||||
help=command_init_backend.__doc__)
|
help=command_init_backend.__doc__)
|
||||||
p_init_backend.add_argument('--config', '-c', required=True)
|
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
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -136,7 +163,8 @@ def main():
|
||||||
'cgi': command_cgi,
|
'cgi': command_cgi,
|
||||||
'fcgi': command_fcgi,
|
'fcgi': command_fcgi,
|
||||||
'default-config': command_default_config,
|
'default-config': command_default_config,
|
||||||
'init-backend': command_init_backend
|
'init-backend': command_init_backend,
|
||||||
|
'sanitize-backend': command_sanitize_backend
|
||||||
}[kwargs.pop('command')](**kwargs)
|
}[kwargs.pop('command')](**kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ class SQLite(Backend):
|
||||||
|
|
||||||
def __init__(self, parameters: SqliteParameters):
|
def __init__(self, parameters: SqliteParameters):
|
||||||
|
|
||||||
parameters['connection'] = get_sqlite_connection(parameters)
|
parameters = SqliteParameters(parameters.path, get_sqlite_connection(parameters))
|
||||||
|
|
||||||
self.user = SqliteUser(parameters, User)
|
self.user = SqliteUser(parameters, User)
|
||||||
self.paste = SqlitePaste(parameters, Paste)
|
self.paste = SqlitePaste(parameters, Paste)
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,13 @@ class User(object):
|
||||||
|
|
||||||
return user.init(self.path)
|
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):
|
class Paste(object):
|
||||||
"""Filesystem paste model backend
|
"""Filesystem paste model backend
|
||||||
|
|
@ -96,3 +103,10 @@ class Paste(object):
|
||||||
def init(self):
|
def init(self):
|
||||||
|
|
||||||
return paste.init(self.path)
|
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
|
||||||
|
|
@ -5,6 +5,16 @@ acting as cells.
|
||||||
"""
|
"""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
from time import time
|
||||||
|
|
||||||
|
|
||||||
|
COLUMNS = [
|
||||||
|
'data',
|
||||||
|
'data_hash',
|
||||||
|
'sub',
|
||||||
|
'expiration',
|
||||||
|
'encoding'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def load(
|
def load(
|
||||||
|
|
@ -22,13 +32,7 @@ def load(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cells = {}
|
cells = {}
|
||||||
for column in [
|
for column in COLUMNS:
|
||||||
'data',
|
|
||||||
'data_hash',
|
|
||||||
'sub',
|
|
||||||
'timestamp',
|
|
||||||
'lifetime',
|
|
||||||
'encoding']:
|
|
||||||
|
|
||||||
cell = row.joinpath(column)
|
cell = row.joinpath(column)
|
||||||
|
|
||||||
|
|
@ -56,8 +60,7 @@ def load(
|
||||||
cells['sub'],
|
cells['sub'],
|
||||||
cells['data'],
|
cells['data'],
|
||||||
cells['data_hash'],
|
cells['data_hash'],
|
||||||
cells['timestamp'],
|
cells['expiration'],
|
||||||
cells['lifetime'],
|
|
||||||
cells['encoding'])
|
cells['encoding'])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,13 +71,7 @@ def dump(model: object, path: Path, model_schema: type) -> None:
|
||||||
row = path.joinpath(model.pid.hex())
|
row = path.joinpath(model.pid.hex())
|
||||||
row.mkdir(parents=True, exist_ok=True)
|
row.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for column in [
|
for column in COLUMNS:
|
||||||
'data',
|
|
||||||
'data_hash',
|
|
||||||
'sub',
|
|
||||||
'timestamp',
|
|
||||||
'lifetime',
|
|
||||||
'encoding']:
|
|
||||||
|
|
||||||
cell = row.joinpath(column)
|
cell = row.joinpath(column)
|
||||||
cell_schema = getattr(model_schema, column)
|
cell_schema = getattr(model_schema, column)
|
||||||
|
|
@ -102,6 +99,22 @@ def init(path: Path):
|
||||||
return None
|
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):
|
def _rm_tree(pth: Path):
|
||||||
for child in pth.iterdir():
|
for child in pth.iterdir():
|
||||||
if child.is_file():
|
if child.is_file():
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,12 @@ acting as cells.
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
|
|
||||||
|
COLUMNS = [
|
||||||
|
'sub',
|
||||||
|
'key_hash',
|
||||||
|
'index',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def load(
|
def load(
|
||||||
proto: object,
|
proto: object,
|
||||||
|
|
@ -22,15 +28,17 @@ def load(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
cells = {}
|
cells = {}
|
||||||
for column in ['key_hash', 'index']:
|
for column in COLUMNS[1:]:
|
||||||
|
|
||||||
cell = row.joinpath(column)
|
cell = row.joinpath(column)
|
||||||
|
|
||||||
if getattr(model_schema, column) == bytes:
|
if not cell.exists():
|
||||||
|
cells[column] = None
|
||||||
|
elif getattr(model_schema, column) == bytes:
|
||||||
cells[column] = cell.read_bytes()
|
cells[column] = cell.read_bytes()
|
||||||
|
elif getattr(model_schema, column) == str:
|
||||||
|
cells[column] = cell.read_text()
|
||||||
else:
|
else:
|
||||||
|
|
||||||
cells[column] = literal_eval(cell.read_text())
|
cells[column] = literal_eval(cell.read_text())
|
||||||
|
|
||||||
return model_class(
|
return model_class(
|
||||||
|
|
@ -46,7 +54,7 @@ def dump(model: object, path: Path, model_schema: object) -> None:
|
||||||
row = path.joinpath(model.sub.hex())
|
row = path.joinpath(model.sub.hex())
|
||||||
row.mkdir(parents=True, exist_ok=True)
|
row.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
for column in ['key_hash', 'index']:
|
for column in COLUMNS[1:]:
|
||||||
|
|
||||||
cell = row.joinpath(column)
|
cell = row.joinpath(column)
|
||||||
|
|
||||||
|
|
@ -74,6 +82,11 @@ def init(path: Path):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(path: Path, model_class: type, model_schema: type):
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _rm_tree(pth: Path):
|
def _rm_tree(pth: Path):
|
||||||
for child in pth.iterdir():
|
for child in pth.iterdir():
|
||||||
if child.is_file():
|
if child.is_file():
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,10 @@ class User(object):
|
||||||
|
|
||||||
return user.init(self.connection)
|
return user.init(self.connection)
|
||||||
|
|
||||||
|
def sanitize(self):
|
||||||
|
|
||||||
|
return user.sanitize(self.connection, self.model_class)
|
||||||
|
|
||||||
|
|
||||||
class Paste(object):
|
class Paste(object):
|
||||||
"""SQLite paste model backend
|
"""SQLite paste model backend
|
||||||
|
|
@ -74,6 +78,10 @@ class Paste(object):
|
||||||
|
|
||||||
return paste.init(self.connection)
|
return paste.init(self.connection)
|
||||||
|
|
||||||
|
def sanitize(self):
|
||||||
|
|
||||||
|
return paste.sanitize(self.connection, self.model_class)
|
||||||
|
|
||||||
|
|
||||||
def get_connection(parameters: Parameters):
|
def get_connection(parameters: Parameters):
|
||||||
"""get an sqlite connection object
|
"""get an sqlite connection object
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
"""
|
"""
|
||||||
from os import path
|
from os import path
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
|
from time import time
|
||||||
|
from importlib.resources import open_text
|
||||||
|
|
||||||
|
|
||||||
def load(proto: object, connection: Connection, model_class: type):
|
def load(proto: object, connection: Connection, model_class: type):
|
||||||
|
|
@ -11,7 +13,7 @@ def load(proto: object, connection: Connection, model_class: type):
|
||||||
cur = connection.cursor()
|
cur = connection.cursor()
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
'SELECT pid, data, data_hash, sub, timestamp, lifetime, encoding FROM pastes WHERE pid=?',
|
'SELECT pid, data, data_hash, sub, expiration, encoding FROM pastes WHERE pid=?',
|
||||||
(proto.pid,
|
(proto.pid,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
@ -24,8 +26,7 @@ def load(proto: object, connection: Connection, model_class: type):
|
||||||
result['sub'],
|
result['sub'],
|
||||||
result['data'],
|
result['data'],
|
||||||
result['data_hash'],
|
result['data_hash'],
|
||||||
result['timestamp'],
|
result['expiration'],
|
||||||
result['lifetime'],
|
|
||||||
result['encoding'])
|
result['encoding'])
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
@ -38,14 +39,13 @@ def dump(model: object, connection: Connection):
|
||||||
cur = connection.cursor()
|
cur = connection.cursor()
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
'''INSERT INTO pastes (pid, data, data_hash, sub, timestamp, lifetime, encoding)
|
'''INSERT INTO pastes (pid, data, data_hash, sub, expiration, encoding)
|
||||||
VALUES (?,?,?,?,?,?,?)''',
|
VALUES (?,?,?,?,?,?)''',
|
||||||
(model.pid,
|
(model.pid,
|
||||||
model.data,
|
model.data,
|
||||||
model.data_hash,
|
model.data_hash,
|
||||||
model.sub,
|
model.sub,
|
||||||
model.timestamp,
|
model.expiration,
|
||||||
model.lifetime,
|
|
||||||
model.encoding))
|
model.encoding))
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
@ -64,8 +64,19 @@ def init(connection: Connection):
|
||||||
|
|
||||||
cur = connection.cursor()
|
cur = connection.cursor()
|
||||||
|
|
||||||
with open(path.join(path.dirname(__file__), 'paste.sql'), 'r') as fh:
|
with open_text('httpaste.backend.sqlite', 'paste.sql') as fh:
|
||||||
|
|
||||||
cur.execute(fh.read())
|
cur.execute(fh.read())
|
||||||
|
|
||||||
connection.commit()
|
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']))
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
CREATE TABLE IF NOT EXISTS "pastes" (
|
CREATE TABLE IF NOT EXISTS "pastes" (
|
||||||
"id" BLOB NOT NULL UNIQUE,
|
"pid" BLOB NOT NULL UNIQUE,
|
||||||
"data" BLOB NOT NULL,
|
"data" BLOB NOT NULL,
|
||||||
"data_hash" BLOB NOT NULL,
|
"data_hash" BLOB NOT NULL,
|
||||||
"sub" BLOB UNIQUE,
|
"sub" BLOB UNIQUE,
|
||||||
"timestamp" INTEGER NOT NULL,
|
"expiration" INTEGER NOT NULL,
|
||||||
"lifetime" INTEGER NOT NULL,
|
"encoding" TEXT,
|
||||||
"encoding" TEXT
|
PRIMARY KEY("pid")
|
||||||
PRIMARY KEY("id")
|
|
||||||
);
|
);
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
from os import path
|
from os import path
|
||||||
from sqlite3 import Connection
|
from sqlite3 import Connection
|
||||||
from httpaste.model import User
|
from httpaste.model import User
|
||||||
|
from importlib.resources import open_text
|
||||||
|
|
||||||
|
|
||||||
def load(proto: User, connection: Connection):
|
def load(proto: User, connection: Connection):
|
||||||
|
|
@ -48,8 +49,13 @@ def init(connection: Connection):
|
||||||
|
|
||||||
cur = connection.cursor()
|
cur = connection.cursor()
|
||||||
|
|
||||||
with open(path.join(path.dirname(__file__), 'user.sql'), 'r') as fh:
|
with open_text('httpaste.backend.sqlite', 'user.sql') as fh:
|
||||||
|
|
||||||
cur.execute(fh.read())
|
cur.execute(fh.read())
|
||||||
|
|
||||||
connection.commit()
|
connection.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize(connection: Connection, model_class) -> bool:
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
@ -71,7 +71,7 @@ def get(**kwargs):
|
||||||
config.salt, config.hmac_iterations)
|
config.salt, config.hmac_iterations)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
data, lifetime, encoding = call()
|
data, expiration, encoding = call()
|
||||||
except paste_model.LifetimeError as e:
|
except paste_model.LifetimeError as e:
|
||||||
if kwargs.get('user') is not None:
|
if kwargs.get('user') is not None:
|
||||||
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
||||||
|
|
@ -85,7 +85,7 @@ def get(**kwargs):
|
||||||
raise ForbiddenError(str(e))
|
raise ForbiddenError(str(e))
|
||||||
|
|
||||||
# burn after read
|
# burn after read
|
||||||
if lifetime < 0:
|
if expiration < 0:
|
||||||
if kwargs.get('user') is not None:
|
if kwargs.get('user') is not None:
|
||||||
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
paste_model.remove_safe(pid, sub, pkey, config.backend.paste,
|
||||||
config.salt, config.hmac_iterations)
|
config.salt, config.hmac_iterations)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
from random import choice
|
from random import choice
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
from tempfile import mkdtemp
|
||||||
|
from pathlib import Path
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
|
||||||
class DecodeError(Exception):
|
class DecodeError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
@ -29,4 +33,4 @@ def decode(data: str, encoding: str) -> bytes:
|
||||||
|
|
||||||
def join_url(base:str, url: str) -> str:
|
def join_url(base:str, url: str) -> str:
|
||||||
|
|
||||||
return urljoin(base, url, True)
|
return urljoin(base, url, True)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"""Model
|
"""Model
|
||||||
"""
|
"""
|
||||||
from typing import NamedTuple, Optional, Dict, Union
|
from typing import NamedTuple, Optional, Dict, Union, Any, TypedDict
|
||||||
|
|
||||||
|
|
||||||
class PasteDataSchema:
|
class PasteDataSchema:
|
||||||
|
|
@ -12,6 +12,7 @@ class PasteDataSchema:
|
||||||
sub = bytes
|
sub = bytes
|
||||||
timestamp = int
|
timestamp = int
|
||||||
lifetime = int
|
lifetime = int
|
||||||
|
expiration = int
|
||||||
encoding = str
|
encoding = str
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -54,6 +55,14 @@ class PasteEncoding(PasteDataSchema.encoding):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PasteExpiration(PasteDataSchema.expiration):
|
||||||
|
"""Paste Expiration
|
||||||
|
|
||||||
|
< 0: after first acccess
|
||||||
|
0: never
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class PasteLifetime(PasteDataSchema.lifetime):
|
class PasteLifetime(PasteDataSchema.lifetime):
|
||||||
"""Paste Lifetime
|
"""Paste Lifetime
|
||||||
"""
|
"""
|
||||||
|
|
@ -89,9 +98,11 @@ class Sub(UserDataSchema.sub):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Index(Dict[PasteId, PasteKey]):
|
class Index(TypedDict):
|
||||||
"""User Paste Index
|
"""User Paste Index
|
||||||
"""
|
"""
|
||||||
|
auth_expires: int
|
||||||
|
pastes: Dict[str, Dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
class SerializedIndex(UserDataSchema.index):
|
class SerializedIndex(UserDataSchema.index):
|
||||||
|
|
@ -128,8 +139,6 @@ class Paste(NamedTuple):
|
||||||
#: paste data hash
|
#: paste data hash
|
||||||
data_hash: Optional[PasteHash] = None
|
data_hash: Optional[PasteHash] = None
|
||||||
#: paste timestamp
|
#: paste timestamp
|
||||||
timestamp: Optional[PasteTimestamp] = None
|
expiration: Optional[PasteExpiration] = None
|
||||||
#: paste lifetime
|
|
||||||
lifetime: Optional[PasteLifetime] = None
|
|
||||||
#: paste encoding
|
#: paste encoding
|
||||||
encoding: Optional[PasteEncoding] = None
|
encoding: Optional[PasteEncoding] = None
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from httpaste.helper.crypto import dhash, shash, encrypt, decrypt
|
||||||
from httpaste.helper.common import generate_random_string
|
from httpaste.helper.common import generate_random_string
|
||||||
from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt,
|
from httpaste.model import (Paste, PasteId, Sub, MasterKey, PasteKey, Salt,
|
||||||
PasteData, PasteHash, PasteTimestamp, PasteSub,
|
PasteData, PasteHash, PasteTimestamp, PasteSub,
|
||||||
PasteLifetime, PasteEncoding)
|
PasteLifetime, PasteEncoding, PasteExpiration)
|
||||||
|
|
||||||
|
|
||||||
class NotFoundError(Exception):
|
class NotFoundError(Exception):
|
||||||
|
|
@ -87,8 +87,7 @@ def load(proto: Paste, backend: object) -> Optional[Paste]:
|
||||||
|
|
||||||
raise SubError('Paste not owned by user')
|
raise SubError('Paste not owned by user')
|
||||||
|
|
||||||
if model.lifetime >= 0 and model.timestamp + \
|
if model.expiration > 0 and model.expiration < int(time.time()):
|
||||||
(60 * model.lifetime) < int(time.time()):
|
|
||||||
|
|
||||||
raise LifetimeError('Paste expired')
|
raise LifetimeError('Paste expired')
|
||||||
|
|
||||||
|
|
@ -121,8 +120,7 @@ def load_safe(
|
||||||
proto.sub,
|
proto.sub,
|
||||||
data,
|
data,
|
||||||
model.data_hash,
|
model.data_hash,
|
||||||
model.timestamp,
|
model.expiration,
|
||||||
model.lifetime,
|
|
||||||
model.encoding)
|
model.encoding)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -195,6 +193,11 @@ def create(
|
||||||
sub = None
|
sub = None
|
||||||
timestamp = PasteTimestamp(int(time.time()))
|
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))
|
safe_data = PasteData(encrypt(data, pid, salt, hmac_iter))
|
||||||
|
|
||||||
model = Paste(
|
model = Paste(
|
||||||
|
|
@ -202,8 +205,7 @@ def create(
|
||||||
sub,
|
sub,
|
||||||
safe_data,
|
safe_data,
|
||||||
data_hash,
|
data_hash,
|
||||||
timestamp,
|
expiration,
|
||||||
lifetime,
|
|
||||||
encoding)
|
encoding)
|
||||||
|
|
||||||
dump(model, backend)
|
dump(model, backend)
|
||||||
|
|
@ -234,6 +236,11 @@ def create_safe(data: PasteData,
|
||||||
safe_sub = PasteSub(shash(sub, data_hash, pid))
|
safe_sub = PasteSub(shash(sub, data_hash, pid))
|
||||||
timestamp = PasteTimestamp(int(time.time()))
|
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))
|
safe_data = PasteData(encrypt(data, pkey, salt, hmac_iter))
|
||||||
|
|
||||||
dump(Paste(
|
dump(Paste(
|
||||||
|
|
@ -241,8 +248,7 @@ def create_safe(data: PasteData,
|
||||||
safe_sub,
|
safe_sub,
|
||||||
safe_data,
|
safe_data,
|
||||||
data_hash,
|
data_hash,
|
||||||
timestamp,
|
expiration,
|
||||||
lifetime,
|
|
||||||
encoding
|
encoding
|
||||||
), backend)
|
), backend)
|
||||||
|
|
||||||
|
|
@ -279,7 +285,7 @@ def get(pid: PasteId, backend: object, salt: Salt = Config.salt, hmac_iter: int
|
||||||
|
|
||||||
data = decrypt(model.data, pid, salt, hmac_iter)
|
data = decrypt(model.data, pid, salt, hmac_iter)
|
||||||
|
|
||||||
return PasteData(data), model.lifetime, model.encoding
|
return PasteData(data), model.expiration, model.encoding
|
||||||
|
|
||||||
|
|
||||||
def get_safe(
|
def get_safe(
|
||||||
|
|
@ -294,4 +300,4 @@ def get_safe(
|
||||||
|
|
||||||
model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter)
|
model = load_safe(Paste(pid, sub), pkey, backend, salt, hmac_iter)
|
||||||
|
|
||||||
return PasteData(model.data), model.lifetime, model.encoding
|
return PasteData(model.data), model.expiration, model.encoding
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
"""user model interface
|
"""user model interface
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
from time import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from httpaste import Config
|
from httpaste import Config
|
||||||
|
|
@ -34,7 +35,7 @@ class IndexError(Exception):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def load(
|
def _load(
|
||||||
proto: User,
|
proto: User,
|
||||||
master_key: str,
|
master_key: str,
|
||||||
backend: object,
|
backend: object,
|
||||||
|
|
@ -54,16 +55,19 @@ def load(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return User(
|
serialized_data = decrypt(model.index, master_key, salt, hmac_iter)
|
||||||
*model[:-1],
|
|
||||||
Index(**json.loads(decrypt(model.index, master_key, salt, hmac_iter)))
|
|
||||||
)
|
|
||||||
except DecryptionError as e:
|
except DecryptionError as e:
|
||||||
|
|
||||||
raise IndexError('unable to decrypt user index') from 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,
|
model: User,
|
||||||
key: MasterKey,
|
key: MasterKey,
|
||||||
backend: object,
|
backend: object,
|
||||||
|
|
@ -77,7 +81,7 @@ def dump(
|
||||||
:param salt: randomization salt
|
:param salt: randomization salt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not isinstance(model.index, Index):
|
if model.index is not None and not isinstance(model.index, dict):
|
||||||
|
|
||||||
raise BaseException('index serialization pre-processing not allowed.')
|
raise BaseException('index serialization pre-processing not allowed.')
|
||||||
|
|
||||||
|
|
@ -102,11 +106,13 @@ def load_paste_key(
|
||||||
:param salt: randomization salt
|
:param salt: randomization salt
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for k, v in load(User(sub), key, backend, salt, hmac_iter).index.items():
|
model = _load(User(sub), key, backend, salt, hmac_iter)
|
||||||
|
|
||||||
|
for k, v in model.index.get('pastes').items():
|
||||||
|
|
||||||
if bytes.fromhex(k) == pid:
|
if bytes.fromhex(k) == pid:
|
||||||
|
|
||||||
return PasteKey(bytes.fromhex(v))
|
return PasteKey(bytes.fromhex(v.get('key')))
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -128,12 +134,13 @@ def dump_paste_key(
|
||||||
:param backend: user model backend
|
:param backend: user model backend
|
||||||
"""
|
"""
|
||||||
|
|
||||||
model = load(User(sub), key, backend, salt, hmac_iter)
|
model = _load(User(sub), key, backend, salt, hmac_iter)
|
||||||
|
|
||||||
dump(User(*model[:-1], Index({
|
model.index.setdefault('pastes', {})[pid.hex()] = {
|
||||||
**model.index,
|
'key': pkey.hex()
|
||||||
**{pid.hex(): pkey.hex()}
|
}
|
||||||
})), key, backend, salt, hmac_iter)
|
|
||||||
|
_dump(model, key, backend, salt, hmac_iter)
|
||||||
|
|
||||||
|
|
||||||
def authenticate(
|
def authenticate(
|
||||||
|
|
@ -154,22 +161,36 @@ def authenticate(
|
||||||
|
|
||||||
proto = User(sub)
|
proto = User(sub)
|
||||||
|
|
||||||
|
bogus_decline_msg = 'unable to authenticate'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model = load(proto, key, backend, salt, hmac_iter)
|
model = _load(proto, key, backend, salt, hmac_iter)
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
raise AuthenticationError('you dun goofed')
|
raise AuthenticationError(bogus_decline_msg) from e
|
||||||
|
|
||||||
if not model:
|
if not model:
|
||||||
|
|
||||||
model = User(sub, key_hash, Index({}))
|
data = {
|
||||||
dump(model, key, backend, salt, hmac_iter)
|
'auth_expires': int(time()) + (1 * 60)
|
||||||
|
}
|
||||||
|
|
||||||
|
model = User(sub, key_hash, Index(data))
|
||||||
|
_dump(model, key, backend, salt, hmac_iter)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
if model.key_hash != key_hash:
|
if model.key_hash != key_hash:
|
||||||
|
|
||||||
raise AuthenticationError('you dun goofed')
|
raise AuthenticationError(bogus_decline_msg)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'sub': sub,
|
'sub': sub,
|
||||||
'master_key': key
|
'master_key': key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
AuthenticationError,
|
||||||
|
load_paste_key,
|
||||||
|
dump_paste_key,
|
||||||
|
authenticate
|
||||||
|
]
|
||||||
0
src/httpaste/schema/__init__.py
Normal file
0
src/httpaste/schema/__init__.py
Normal file
|
|
@ -161,6 +161,15 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"$ref": "#/components/parameters/syntax"
|
"$ref": "#/components/parameters/syntax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/format"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/linenos"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/parameters/mime"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,6 @@
|
||||||
"""
|
"""
|
||||||
from httpaste import load_config, get_flask_app, get_config_path
|
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)
|
application = get_flask_app(config, server_config)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue