Merged in feature/HTTPASTE-46/view (pull request #48)

Feature/HTTPASTE-46/view
This commit is contained in:
Tiara Rodney 2022-04-16 20:34:17 +00:00
commit afafbd6459
29 changed files with 387 additions and 231 deletions

66
Pipfile.lock generated
View file

@ -292,6 +292,50 @@
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pillow": {
"hashes": [
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
],
"markers": "python_version >= '3.7'",
"version": "==9.1.0"
},
"protobuf": {
"hashes": [
"sha256:001c2160c03b6349c04de39cf1a58e342750da3632f6978a1634a3dcca1ec10e",
@ -339,11 +383,11 @@
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
},
"pyrsistent": {
"hashes": [
@ -501,11 +545,11 @@
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
},
"six": {
"hashes": [
@ -533,11 +577,11 @@
},
"virtualenv": {
"hashes": [
"sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66",
"sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"
"sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a",
"sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.14.0"
"version": "==20.14.1"
}
}
}

View file

@ -23,6 +23,7 @@ install_requires =
connexion>=2.13.0,<3
cryptography>=36.0.2,<37
pygments>=2.11.2,<3
Pillow>=9.1.0,<10
zip_safe = true
package_dir =
=src

View file

@ -7,7 +7,10 @@ def search(**kwargs):
variables = {
'paste_index_url': '/ui/paste',
'man_page': man_page
'user_index_url': '/ui/user',
'man_page': man_page,
'user': kwargs.get('user'),
'delete_session_url': '/ui/user/session/delete'
}
return template.render(**variables), 200

View file

@ -5,6 +5,9 @@ from connexion import request
from httpaste.helper.template import views
from httpaste.helper.url import url_query_string, url_append_query_param
from httpaste.helper.syntax import syntax_shortnames, format_shortnames
from httpaste.helper.http import mime_types
from httpaste.controller.paste import post as post_raw
from httpaste.controller.paste import get as get_raw
@ -54,8 +57,6 @@ def get(**kwargs):
template = views.get_template("viewport/ui/paste/get.html")
base_path = f'paste/public/{kwargs["id"]}'
if kwargs.get('user'):
@ -86,7 +87,10 @@ def get(**kwargs):
'syntax': kwargs.get('syntax', ''),
'mime': kwargs.get('mime', ''),
'preview': kwargs.get('preview', True)
}
},
'syntax_shortnames': syntax_shortnames(),
'format_shortnames': format_shortnames(),
'mime_types': mime_types()
}
return template.render(**variables)

View file

@ -0,0 +1,12 @@
from httpaste.helper.template import views
from httpaste import __doc__ as man_page
def search(**kwargs):
template = views.get_template("viewport/ui/user/search.html")
variables = {
'delete_session_url': '/ui/user/session/delete'
}
return template.render(**variables), 200

View file

@ -1,3 +1,4 @@
from mimetypes import types_map as mime_types_map
class BadRequestError(RuntimeError):
def __init__(self, msg=None):
@ -62,3 +63,11 @@ class NotFoundError(RuntimeError):
"status": 404,
"title": "Not Found",
}, 404
def mime_types():
types = list(set(mime_types_map.values()))
types.sort()
return types

View file

@ -1,5 +1,5 @@
from pygments.lexers import get_lexer_by_name, find_lexer_class_by_name
from pygments.formatters import find_formatter_class, HtmlFormatter
from pygments.lexers import (get_lexer_by_name, find_lexer_class_by_name, get_all_lexers)
from pygments.formatters import (find_formatter_class, HtmlFormatter, get_all_formatters)
def highlight(
@ -18,3 +18,13 @@ def highlight(
formatter = find_formatter_class(format_alias)(linenos=linenos)
return highlight(data, get_lexer_by_name(lexer_alias), formatter)
def syntax_shortnames():
return {l[0]:l[1][0] for l in get_all_lexers() if len(l[1]) > 0}
def format_shortnames():
return [f.aliases[0] for f in get_all_formatters()]

View file

@ -1,6 +1,6 @@
from jinja2 import Environment, PackageLoader, select_autoescape
views = Environment(
loader=PackageLoader("httpaste", "views"),
loader=PackageLoader("httpaste", "view"),
autoescape=select_autoescape()
)

View file

@ -388,6 +388,9 @@
},
{
"$ref": "#/components/parameters/mime"
},
{
"$ref": "#/components/parameters/ui_preview"
}
],
"responses": {
@ -422,6 +425,22 @@
}
}
},
"/ui/user": {
"get": {
"description": "get a public paste",
"security": [
{}
],
"responses": {
"200": {
"description": "paste data. content type may vary.",
"content": {
"text/html": {}
}
}
}
}
},
"/ui/user/session/delete": {
"get": {
"description": "get a public paste",

View file

@ -0,0 +1,39 @@
<form class="color-theme font-theme" action="{{preview_url}}" method="get" enctype="multipart/form-data">
<details>
<summary>
<label for="syntax">Syntax</label>
<!--<input type="text" name="syntax" value="{{ query['syntax'] }}"/>-->
<select name="syntax">
<option {{ 'selected=""'if not query['syntax'] }} value>Disabled</option>
{% for name, shortname in syntax_shortnames.items() %}
<option value="{{shortname}}" {{ 'selected=""'if query['syntax'] == shortname }}>{{ name }}</option>
{% endfor %}</select>
</summary>
<small>language to highlight syntax for</small>
</details>
<details>
<summary>
<label for="format">Format</label>
<select name="format">
<option {{ 'selected=""'if not query['format'] }} value>Disabled</option>
{% for shortname in format_shortnames %}
<option value="{{shortname}}" {{ 'selected=""'if query['format'] == shortname }}>{{ shortname }}</option>
{% endfor %}</select>
</summary>
<small>output format of highlighted syntax</small>
</details>
<details>
<summary>
<label for="mime">Content-Type (MIME)</label>
<select name="mime">
<option {{ 'selected=""'if not query['mime'] }} value>Disabled</option>
{% for mime in mime_types %}
<option value="{{mime}}" {{ 'selected=""'if query['mime'] == mime }}>{{ mime }}</option>
{% endfor %}</select>
</summary>
<small>content-type Header the server should return</small>
</details>
<input type="text" name="preview" value="{{ query['preview'] }}" style="display: none"/>
<input type="submit" value="☝ Refresh"/>
</form>

View file

@ -0,0 +1,26 @@
<form class="color-theme font-theme" action="{{paste_form_url}}" method="post" enctype="multipart/form-data" target="_top">
<details>
<summary>
<label for="data">Data
<textarea name="data" cols="60" rows="10" style="width:100%"></textarea><br/>
<input type="file" id="myfile" name="data"/>
</label>
</summary>
<small>
Either supply a past text, or upload a file.
</small>
</details>
<details>
<summary>
<label for="lifetime">Lifetime
<input type="number" id="lname" name="lifetime" value="5">
</label>
</summary>
<small>
Set a pastes lifetime to make it expire after a specified amount of time.
The lifetime must be provided in minutes and cannot be less than 1 (, unless lesser than 0).
A lifetime of 0 will evaluate to a lifetime 1. A lifetime of less than 0 will make the paste expire after first read.
</small>
</details>
<input type="submit" value="☝ Paste"/>
</form>

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
</style>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {
background-color: #DDD;
padding: 0 1em 0 1em;
font-size: 100%;
}
form > *, menu > li {
margin-bottom: 1em;
}
</style>
</head>
<body>
<header>
{% block header %}{% endblock %}
</header>
<hr/>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -0,0 +1,36 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Conditioner</h1>
<p>
Preview and conditon an existing paste
</p>
{% endblock %}
{% block content %}
{% if query['preview'] %}
Preview
<iframe src="{{paste_url}}" title="as" style="resize:both; width: 100%"></iframe>
{% else %}
<p><b>Preview is disabled.</b>
<br/>
This probably happened because the paste is set to expire after read.
<br/>
<i>You can still proceed to condition the paste URL.</i>
</p>
{% endif %}
{% include 'container/get_paste_form.html' %}
<hr/>
<figure>
<figcaption><strong>URLs</strong></figcaption>
<dl>
<dt>Formatted</dt>
<dd>
<a href="{{paste_url}}" target="_blank">{{paste_url}}</a>
</dd>
<dt>Raw</dt>
<dd>
<a href="{{raw_paste_url}}" target="_blank">{{raw_paste_url}}</a>
</dd>
</dl>
</figure>
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Private</h1>
<p>
Private pastes are authenticated
</p>
{% endblock %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - <a href="/ui/paste">Paste</a> - Public</h1>
<p>
Public pastes are not indexed and can only be accessed by knowing their respective
paste id.
</p>
{% endblock %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - Paste</h1>
<p>
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 pastes passwords are derived from their ids. Private pastes 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 a HTTP basic authentication act as the master password.
Paste ids, usernames, and any other identifiable attributes are only stored
inside the storage backend as keyed and salted BLAKE2 hashes.
</p>
<p><strong>Note:</strong>
The initial creation of a private paste will prompt for login credentials.
If the login credentials are not known, they will be created automatically.
If it is required to authenticate with other credentials, clear your local
HTTP authentication cache.
</p>
{% endblock %}
{% block content %}
<figure>
<figcaption><strong>Navigation</strong></figcaption>
<menu>
<li>
<a href="{{create_private_paste_url}}">Create a Private Paste</a>
</li>
<li>
<a href="{{create_public_paste_url}}">Create a Public Paste</a>
</li>
</menu>
</figure>
{% endblock %}

View file

@ -0,0 +1,41 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - versatile HTTP pastebin</h1>
<p>
This is the user interface of the hosted version of <a href="https://victorykit.bitbucket.io/httpaste" target="_blank">httpaste</a>,
a program which 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
<a href='http://sprunge.us' target='_blank'>sprunge.us</a> and <a href='http://ix.io' target="_blank">ix.io</a>.
It aims for a higher degree of privacy control than available commercial pastebin products.
</p>
<p>
httpaste features include:
<ul>
<li>Authenticated private and unlisted public pasting</li>
<li>Lifetime control (including Burn-After-Read expiration)</li>
<li>Encoded and Binary Data Upload</li>
<li>Syntax Higlighting</li>
<li>Output Formatting</li>
<li>Output Content-Type Control</li>
</ul>
</p>
<p>
A pseudo man page for CLI usage is available via HTTP GET of this host's root document.
(e.g. `<code>$ curl httpaste.it</code>`)
</p>
{% endblock %}
{% block content %}
<figure>
<figcaption><strong>Navigation</strong></figcaption>
<menu>
<li>
<a href="{{paste_index_url}}">Paste</a>
</li>
<li>
<a href="{{user_index_url}}">User</a>
</li>
</menu>
</figure>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends 'frame/decorated.html' %}
{% block header %}
<h1><a href="/">httpaste</a> - User</h1>
{% endblock %}
{% block content %}
<figure>
<figcaption><strong>Navigation</strong></figcaption>
<menu>
<li>
<a href="{{delete_session_url}}" target="_blank">Clear Local HTTP Authentication Cache</a>
</li>
</menu>
</figure>
{% endblock %}

View file

@ -1,26 +0,0 @@
<form class="color-theme font-theme" action="{{preview_url}}" method="get" enctype="multipart/form-data">
<details>
<summary>
<label for="syntax">Syntax</label>
<input type="text" name="syntax" value="{{ query['syntax'] }}"/>
</summary>
<a href="https://pygments.org/docs/lexers/" target="_blank">Pygments lexer short name</a> <i>(e.g. 'terraform', 'python')</i>
</details>
<details>
<summary>
<label for="format">Format</label>
<input type="text" name="format" value="{{ query['format'] }}"/>
</summary>
<a href="https://pygments.org/docs/formatters/" target="_blank">Pygments formatter short name</a> <i>(e.g. 'html', 'terminal256')</i>
</details>
<details>
<summary>
<label for="mime">Content-Type (MIME)</label>
<input type="text" name="mime" value="{{ query['mime'] }}"/>
</summary>
Content-Type Header the server should return
</details>
<input type="text" name="preview" value="{{ query['preview'] }}" class="hidden"/>
<input type="submit" value="☝ Refresh"/>
</form>

View file

@ -1,21 +0,0 @@
<form class="color-theme font-theme" action="{{paste_form_url}}" method="post" enctype="multipart/form-data" target="_top">
<details>
<summary>
<label for="data">Data</label>
<div><textarea name="data" cols="60" rows="10" style="width:100%"></textarea><br/>
<input type="file" id="myfile" name="data"/>
</div>
</summary>
Either supply a past text, or upload a file.
</details>
<details>
<summary>
<label for="lifetime">Lifetime</label>
<input type="number" id="lname" name="lifetime" value="5">
</summary>
Set a pastes lifetime to make it expire after a specified amount of time.<br/>
The lifetime must be provided in minutes and cannot be less than 1<br/>(, unless lesser than 0).<br/>
A lifetime of 0 will evaluate to a lifetime 1.
</details>
<input type="submit" value="☝ Paste"/>
</form>

View file

@ -1,58 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
form {
display: flex;
flex-direction: column;
justify-content: center;
align-items: left;
}
.hidden {
display: none;
}
.color-theme details {
background-color: #EEE;
}
.font-theme details {
font-style: italic;
}
.color-theme details > summary {
background-color: white;
}
.font-theme details > summary {
font-style: normal;
}
details {
margin-bottom: 1em;
}
details > summary {
list-style-type: '?';
}
</style>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
margin: 0;
height: 100%;
width: 100%;
display: flex;
}
main {
width: 100%;
display: flex;
flex-direction: column;
}
.iframe {
width: 100%;
flex-grow: 1;
border: none;
}
</style>
</head>
<body>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>

View file

@ -1,27 +0,0 @@
{% extends 'frame/base.html' %}
{% block content %}
<a href="/">Return</a>
<h1>Paste Conditioner</h1>
{% if query['preview'] %}
Preview
<iframe src="{{paste_url}}" title="as" style="resize:both; width: 100%"></iframe>
<hr/>
{% else %}
<p><b>Preview is disabled.</b>
<br/>
This probably happened because the paste is set to expire after read.
<br/>
<i>You can still proceed to condition the paste URL.</i>
</p>
{% endif %}
{% include 'container/get_paste_form.html' %}
<div style="justify-content: center">
<hr/>
<h3>Paste URLs</h3>
Formatted: <a href="{{paste_url}}" target="_top">{{paste_url}}</a>
<br/>
Raw: <a href="{{raw_paste_url}}" target="_top">{{raw_paste_url}}</a>
</div>
{% endblock %}

View file

@ -1,5 +0,0 @@
{% extends 'frame/base.html' %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -1,5 +0,0 @@
{% extends 'frame/base.html' %}
{% block content %}
{% include 'container/post_paste_form.html' %}
{% endblock %}

View file

@ -1,29 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title></title>
<style>
html, body {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
</style>
</head>
<body>
<p>
<a href="{{create_private_paste_url}}">Create a Private Paste</a>
</p>
<p>
<a href="{{create_public_paste_url}}">Create a Public Paste</a>
</p>
<p>
<a href="{{ delete_session_url }}" target="_blank" style="text-align: center">
Flush Local HTTP Authentication Cache
</a>
</p>
</body>
</html>

View file

@ -1,11 +0,0 @@
{% extends 'frame/decorated.html' %}
{% block content %}
<header style="display: flex; justify-content: center">
<details>
<summary style="text-align: center"><a href="/">httpaste - versatile HTTP pastebin (User Interface)</a></summary>
<textarea readonly="true" style="width: 100%">{{ man_page }}</textarea>
</details>
</header>
<iframe src="{{ paste_index_url }}" title="" class="iframe"></iframe>
{% endblock %}