248 lines
7 KiB
Python
Executable file
248 lines
7 KiB
Python
Executable file
#!/usr/bin/env python3
|
||
"""PSEUDO-MAN-PAGE
|
||
|
||
NAME
|
||
|
||
httpaste - versatile HTTP pastebin
|
||
|
||
SYNOPSIS
|
||
|
||
HTTP [POST|PUT|DELETE|GET] {url}paste/[public|private]
|
||
|
||
DESCRIPTION
|
||
|
||
This program offers an HTTP interface for storing public and private data
|
||
(a.k.a. pastes). 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 with an HMAC derived key using
|
||
{hmac_iterations} iterations 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 databases as keyed and salted BLAKE2 hashes.
|
||
|
||
Default Paste Encoding: {paste_default_encoding}
|
||
Default Paste Lifetime: {paste_lifetime} minutes
|
||
Minimum Paste Lifetime: 1 minute
|
||
Maximum Paste Lifetime: {paste_max_lifetime} hours
|
||
|
||
EXAMPLES
|
||
|
||
POST Public Paste
|
||
|
||
$ echo '#My public paste' | curl {url}paste/public \\
|
||
-F 'data=<-'
|
||
{url}/paste/private/I0ah7fyA
|
||
|
||
|
||
GET Public Paste
|
||
|
||
$ curl {url}/paste/public/I0ah7fyA
|
||
#My public paste
|
||
|
||
|
||
POST Private Paste
|
||
|
||
$ echo '#My private paste' | curl {url}paste/private \\
|
||
-F 'data=<-' \\
|
||
-u myusername:mypassword
|
||
{url}paste/private/4FtNL75g
|
||
|
||
|
||
GET Private Paste
|
||
|
||
$ curl {url}paste/private -u myusername:mypassword
|
||
#My private paste
|
||
|
||
|
||
POST Paste (with non-default expiration)
|
||
|
||
$ echo '#My paste expires in 20 minutes' | curl \\
|
||
{url}paste/public?lifetime=20 \\
|
||
-F 'data=<-' \\
|
||
{url}paste/public/xMxEmNi8
|
||
|
||
|
||
POST Paste (with expiration after first read)
|
||
|
||
$ echo '#My paste expires after first read' | curl \\
|
||
{url}paste/public?lifetime=-1 \\
|
||
-F 'data=<-' \\
|
||
{url}paste/public/4FtNL75g
|
||
|
||
|
||
POST Paste with binary data
|
||
|
||
$ cat my.pdf | base64 | curl \\
|
||
"{url}paste/public?encoding=base64" \\
|
||
-F "data=<-"
|
||
{url}paste/public/zYWpEzXU
|
||
|
||
|
||
GET Paste (with shell syntax highlighting)
|
||
|
||
$ curl "{url}paste/public/I0ah7fyA?syntax=json"
|
||
[38;5;66;03m#My public paste[39;00m
|
||
|
||
|
||
GET Paste (with line numbers)
|
||
|
||
$ curl "{url}paste/public/I0ah7fyA?syntax=shell&linenos=true"
|
||
0001: [38;5;66;03m#My public paste[39;00m
|
||
0002:
|
||
|
||
|
||
GET Paste (with formatted output)
|
||
|
||
$ curl "{url}paste/public/I0ah7fyA?syntax=shell&format=html"
|
||
--e.g. HTML OUTPUT WITH INLINE CSS--
|
||
|
||
|
||
GET Paste (and set HTTP response content type)
|
||
|
||
$ curl "{url}paste/public/zYWpEzXU?mime=application/pdf"
|
||
--e.g. BINARY--
|
||
|
||
SEE ALSO
|
||
|
||
Documentation <https://victorykit.bitbucket.io/httpaste>
|
||
|
||
Sources <https://bitbucket.org/victorykit/httpaste>
|
||
|
||
Host (HTTP) <http://httpaste.it>
|
||
(Onion) <http://paste77ubkwxy4fqezffsmthxdh3xerwi72tlsw2mch7ecjhw2xn7iyd.onion>
|
||
|
||
NOTES
|
||
|
||
THIS PROGRAM IS FREE SOFTWARE.
|
||
|
||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||
THE PROGRAM AS PERMITTED, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||
SUCH DAMAGES.
|
||
|
||
"""
|
||
from typing import NamedTuple
|
||
from configparser import ConfigParser
|
||
from importlib.resources import path as resource_path
|
||
from pathlib import Path
|
||
|
||
from connexion import FlaskApp
|
||
from connexion.resolver import RestyResolver
|
||
|
||
from httpaste.server import get_server_config
|
||
from httpaste.server import Config as ServerConfig
|
||
from httpaste.context import get_context_config
|
||
from httpaste.context import Config as ContextConfig
|
||
from httpaste.model import get_model_config
|
||
from httpaste.model import Config as ModelConfig
|
||
from httpaste.backend import get_backend_config
|
||
from httpaste.backend import Config as BackendConfig
|
||
from httpaste.helper.config import get_configparser, CONFIGPATH_ENVIRON
|
||
from httpaste.helper.http import (
|
||
BadRequestError,
|
||
ForbiddenError,
|
||
GoneError,
|
||
NotFoundError,
|
||
UnauthorizedError)
|
||
|
||
|
||
class Config(NamedTuple):
|
||
"""
|
||
"""
|
||
context: ContextConfig
|
||
server: ServerConfig
|
||
model: ModelConfig
|
||
backend: BackendConfig
|
||
|
||
|
||
def get_config(configIni: ConfigParser, path: Path):
|
||
"""
|
||
"""
|
||
|
||
from httpaste.model import Config as ModelConfig
|
||
|
||
context_config = get_context_config(configIni)
|
||
server_config = get_server_config(configIni)
|
||
model_config = get_model_config(configIni, path)
|
||
backend_config = get_backend_config(configIni, path)
|
||
|
||
return Config(
|
||
context=context_config,
|
||
server=server_config,
|
||
model=model_config,
|
||
backend=backend_config
|
||
)
|
||
|
||
|
||
def load_config(path: str = None, var_name: str = CONFIGPATH_ENVIRON):
|
||
"""
|
||
"""
|
||
|
||
configIni, path = get_configparser(path, var_name)
|
||
|
||
return get_config(configIni, Path(path).resolve().parent)
|
||
|
||
|
||
def get_flask_app(config: Config) -> FlaskApp:
|
||
"""get a flask app object
|
||
"""
|
||
|
||
print(config.server.swagger_ui)
|
||
|
||
options = {"swagger_ui": config.server.swagger_ui}
|
||
|
||
#context manager returns a pathlib.Path object
|
||
with resource_path('httpaste.schema', 'httpaste.openapi.json') as path:
|
||
|
||
application = FlaskApp(__name__, specification_dir=path.parent)
|
||
|
||
application.add_api(
|
||
path.name,
|
||
options=options,
|
||
resolver=RestyResolver('httpaste.controller')
|
||
)
|
||
|
||
for err_cls in [
|
||
BadRequestError,
|
||
ForbiddenError,
|
||
GoneError,
|
||
NotFoundError,
|
||
UnauthorizedError
|
||
]:
|
||
application.add_error_handler(
|
||
err_cls, getattr(err_cls, 'render')
|
||
)
|
||
|
||
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
|
||
|
||
|
||
__all__ = [
|
||
Config,
|
||
load_config,
|
||
get_flask_app
|
||
]
|