diff --git a/setup.cfg b/setup.cfg index 465a26b..d8b5b0e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,4 +40,5 @@ where = src [options.package_data] * = *.json - *.sql \ No newline at end of file + *.sql + *.html \ No newline at end of file diff --git a/src/httpaste/controller/__init__.py b/src/httpaste/controller/__init__.py index f3ca075..3e923cd 100644 --- a/src/httpaste/controller/__init__.py +++ b/src/httpaste/controller/__init__.py @@ -15,4 +15,4 @@ def get(**kwargs): paste_lifetime=model.paste.default_lifetime, paste_max_lifetime=str(round(model.paste.default_max_lifetime / 60)), paste_default_encoding=model.paste.default_encoding - ), 200 + ), 302, {'Location': '/ui'} diff --git a/src/httpaste/controller/ui/__init__.py b/src/httpaste/controller/ui/__init__.py new file mode 100644 index 0000000..c1b52c2 --- /dev/null +++ b/src/httpaste/controller/ui/__init__.py @@ -0,0 +1,13 @@ +from httpaste.helper.template import views +from httpaste import __doc__ as man_page + +def search(**kwargs): + + template = views.get_template("viewport/ui/search.html") + + variables = { + 'paste_index_url': '/ui/paste', + 'man_page': man_page + } + + return template.render(**variables), 200 \ No newline at end of file diff --git a/src/httpaste/controller/ui/paste/__init__.py b/src/httpaste/controller/ui/paste/__init__.py new file mode 100644 index 0000000..771ed1a --- /dev/null +++ b/src/httpaste/controller/ui/paste/__init__.py @@ -0,0 +1,90 @@ +from io import BytesIO +from base64 import b64encode + +from connexion import request + +from httpaste.helper.template import views +from httpaste.helper.url import url_query_string, url_append_query_param +from httpaste.controller.paste import post as post_raw +from httpaste.controller.paste import get as get_raw + + +def search(**kwargs): + + template = views.get_template("viewport/ui/paste/search.html") + + variables = { + 'create_public_paste_url': '/ui/paste/public', + 'create_private_paste_url': '/ui/paste/private', + 'user': kwargs.get('user'), + 'delete_session_url': '/ui/user/session/delete' + } + + return template.render(**variables), 200 + + +def post(**kwargs): + + #rewriting strict form to mixed (as expected by cascaded controller) + data = kwargs['body'].pop('data') + kwargs = {**kwargs, **kwargs['body']} + kwargs.pop('body') + kwargs['body'] = {'data': data} + + #prepare octet stream data for cascaded controller + if kwargs.get('data').filename: + bfr = BytesIO() + kwargs.get('data').save(bfr) + bfr.seek(0) + kwargs['body']['data'] = b64encode(bfr.read()).decode('utf-8') + kwargs['encoding'] = 'base64' + + output, status_code = post_raw(**kwargs) + + #TODO: lifetime=-1 no preview handler + + url = output.strip('\n') + if kwargs.get('lifetime') and int(kwargs['lifetime']) < 0: + url = url_append_query_param(url, 'preview', 'False') + + return output, 302, {'Location': url} + + +def get(**kwargs): + + template = views.get_template("viewport/ui/paste/get.html") + + base_path = f'paste/public/{kwargs["id"]}' + + raw_paste_url = f'{request.host_url}{base_path}' + if kwargs.get('user'): + raw_paste_url = f'{request.host_url}{base_path}' + + paste_url = raw_paste_url + + paste_url_query = {} + for field in ['format', 'mime', 'syntax']: + if kwargs.get(field): + paste_url_query[field] = kwargs[field] + + if paste_url_query: + paste_url = '?'.join((paste_url, url_query_string(paste_url_query))) + + preview_url = f'/ui/{base_path}' + if kwargs.get('preview'): + paste_url_query['preview'] = kwargs['preview'] + preview_url = '?'.join((preview_url, url_query_string(paste_url_query))) + + variables = { + 'raw_paste_url': raw_paste_url, + 'paste_url': paste_url, + 'preview_url': preview_url, + 'query': { + 'format': kwargs.get('format', ''), + 'syntax': kwargs.get('syntax', ''), + 'mime': kwargs.get('mime', ''), + 'preview': kwargs.get('preview', True) + } + } + + return template.render(**variables) \ No newline at end of file diff --git a/src/httpaste/controller/ui/paste/private.py b/src/httpaste/controller/ui/paste/private.py new file mode 100644 index 0000000..4236f67 --- /dev/null +++ b/src/httpaste/controller/ui/paste/private.py @@ -0,0 +1,24 @@ +from httpaste.helper.template import views +from httpaste.controller.ui.paste import post as post_proxy +from httpaste.controller.ui.paste import get as get_proxy + +def search(**kwargs): + + template = views.get_template("viewport/ui/paste/private/search.html") + + variables = { + 'paste_form_url': '/ui/paste/private', + 'user': kwargs.get('user') + } + + return template.render(**variables), 200 + + +def post(**kwargs): + + return post_proxy(**kwargs) + + +def get(**kwargs): + + return get_proxy(**kwargs) \ No newline at end of file diff --git a/src/httpaste/controller/ui/paste/public.py b/src/httpaste/controller/ui/paste/public.py new file mode 100644 index 0000000..0659d5c --- /dev/null +++ b/src/httpaste/controller/ui/paste/public.py @@ -0,0 +1,23 @@ +from httpaste.helper.template import views +from httpaste.controller.ui.paste import post as post_proxy +from httpaste.controller.ui.paste import get as get_proxy + +def search(**kwargs): + + template = views.get_template("viewport/ui/paste/public/search.html") + + variables = { + 'paste_form_url': '/ui/paste/public' + } + + return template.render(**variables), 200 + + +def post(**kwargs): + + return post_proxy(**kwargs) + + +def get(**kwargs): + + return get_proxy(**kwargs) \ No newline at end of file diff --git a/src/httpaste/controller/ui/user/__init__.py b/src/httpaste/controller/ui/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/httpaste/controller/ui/user/session/__init__.py b/src/httpaste/controller/ui/user/session/__init__.py new file mode 100644 index 0000000..54376bc --- /dev/null +++ b/src/httpaste/controller/ui/user/session/__init__.py @@ -0,0 +1,19 @@ +from httpaste.helper.template import views +from httpaste.controller.user.session import delete as raw_delete + +from connexion import request + +def search(**kwargs): + + template = views.get_template("viewport/ui/user/session/search.html") + + print(request.path) + + variables = {'session_delete_url': request.path + '/delete'} + + return template.render(**variables), 200 + + +def delete(**kwargs): + + return raw_delete(**kwargs) \ No newline at end of file diff --git a/src/httpaste/controller/ui/user/session/delete.py b/src/httpaste/controller/ui/user/session/delete.py new file mode 100644 index 0000000..24fd99c --- /dev/null +++ b/src/httpaste/controller/ui/user/session/delete.py @@ -0,0 +1,6 @@ +from httpaste.helper.template import views +from httpaste.controller.ui.user.session import delete as proxy_delete + +def search(**kwargs): + + return proxy_delete(**kwargs) \ No newline at end of file diff --git a/src/httpaste/controller/user/session.py b/src/httpaste/controller/user/session.py index 3639b3b..1d75894 100644 --- a/src/httpaste/controller/user/session.py +++ b/src/httpaste/controller/user/session.py @@ -2,7 +2,7 @@ """ from flask import current_app -from httpaste.helper.http import ForbiddenError +from httpaste.helper.http import UnauthorizedError from httpaste.model.user import authenticate, AuthenticationError from httpaste.backend import load_backend @@ -22,4 +22,11 @@ def post(*args, **kwargs): return authenticate(user_id, password, backend.user, context) except AuthenticationError as e: - raise ForbiddenError('You shall not pass!') from e + raise UnauthorizedError('You shall not pass!') from e + + +def delete(**kwargs): + """ + """ + + raise UnauthorizedError('Authentication Rejection requested by client') \ No newline at end of file diff --git a/src/httpaste/helper/http.py b/src/httpaste/helper/http.py index 8f8fd48..050cd00 100644 --- a/src/httpaste/helper/http.py +++ b/src/httpaste/helper/http.py @@ -21,7 +21,7 @@ class UnauthorizedError(RuntimeError): return { "detail": str(error), "status": 401, - "title": "Unauthorized s", + "title": "Unauthorized", }, 401 diff --git a/src/httpaste/helper/template.py b/src/httpaste/helper/template.py new file mode 100644 index 0000000..446418a --- /dev/null +++ b/src/httpaste/helper/template.py @@ -0,0 +1,6 @@ +from jinja2 import Environment, PackageLoader, select_autoescape + +views = Environment( + loader=PackageLoader("httpaste", "views"), + autoescape=select_autoescape() +) \ No newline at end of file diff --git a/src/httpaste/helper/url.py b/src/httpaste/helper/url.py new file mode 100644 index 0000000..38528a1 --- /dev/null +++ b/src/httpaste/helper/url.py @@ -0,0 +1,20 @@ +from urllib.parse import urlparse, parse_qs + + +def url_query_string(fields:dict): + + return '&'.join([f'{k}={v}' for k,v in fields.items()]) + + +def url_append_query_param(url:str, name: str, value:str): + + urlcomps = urlparse(url) + + q = parse_qs(urlcomps.query) + + q[name] = value + + qs = url_query_string(q) + + return urlcomps._replace(query=qs).geturl() + diff --git a/src/httpaste/schema/httpaste.openapi.json b/src/httpaste/schema/httpaste.openapi.json index 8f55586..508fa5e 100644 --- a/src/httpaste/schema/httpaste.openapi.json +++ b/src/httpaste/schema/httpaste.openapi.json @@ -18,7 +18,7 @@ "get": { "description": "get description", "responses": { - "200": { + "303": { "description": "", "content": { "text/plain": { @@ -185,6 +185,258 @@ } } } + }, + "/ui": { + "get": { + "description": "create a new public paste", + "security": [ + {} + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ui/paste": { + "get": { + "description": "create a new public paste", + "security": [ + {} + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/ui/paste/public": { + "get": { + "description": "create a new public paste UI-driven", + "security": [ + {} + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "create a new public paste", + "requestBody": { + "$ref": "#/components/requestBodies/pastePost" + }, + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/lifetime" + } + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PasteURL" + } + } + } + } + } + } + }, + "/ui/paste/private": { + "get": { + "description": "create a new public paste UI-driven", + "security": [ + { + "basicAuth": [] + } + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/html": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "post": { + "description": "create a new public paste", + "requestBody": { + "$ref": "#/components/requestBodies/pastePost" + }, + "security": [ + { + "basicAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/lifetime" + }, + { + "$ref": "#/components/parameters/encoding" + } + ], + "responses": { + "200": { + "description": "paste location", + "content": { + "text/plain": { + "schema": { + "$ref": "#/components/schemas/PasteURL" + } + } + } + } + } + } + }, + "/ui/paste/public/{id}": { + "get": { + "description": "get a public paste", + "security": [ + {} + ], + "parameters": [ + { + "$ref": "#/components/parameters/id" + }, + { + "$ref": "#/components/parameters/syntax" + }, + { + "$ref": "#/components/parameters/format" + }, + { + "$ref": "#/components/parameters/linenos" + }, + { + "$ref": "#/components/parameters/mime" + }, + { + "$ref": "#/components/parameters/ui_preview" + } + ], + "responses": { + "200": { + "description": "paste data. content type may vary.", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/PasteData" + } + } + } + } + } + } + }, + "/ui/paste/private/{id}": { + "get": { + "description": "get a public paste", + "security": [ + { + "basicAuth": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/id" + }, + { + "$ref": "#/components/parameters/syntax" + }, + { + "$ref": "#/components/parameters/format" + }, + { + "$ref": "#/components/parameters/linenos" + }, + { + "$ref": "#/components/parameters/mime" + } + ], + "responses": { + "200": { + "description": "paste data. content type may vary.", + "content": { + "text/html": { + "schema": { + "$ref": "#/components/schemas/PasteData" + } + } + } + } + } + } + }, + "/ui/user/session": { + "get": { + "description": "get a public paste", + "security": [ + { + "basicAuth": [] + } + ], + "responses": { + "200": { + "description": "paste data. content type may vary.", + "content": { + "text/html": {} + } + } + } + } + }, + "/ui/user/session/delete": { + "get": { + "description": "get a public paste", + "security": [ + {} + ], + "responses": { + "200": { + "description": "paste data. content type may vary.", + "content": { + "text/html": {} + } + } + } + } } }, "components": { @@ -215,9 +467,9 @@ "type": "string", "format": "binary" }, - "rsa_public_key": { - "description": "RSA public key", - "type": "string" + "fileName": { + "type": "string", + "format": "binary" } }, "required": [ @@ -294,6 +546,15 @@ "schema": { "type": "string" } + }, + "ui_preview": { + "description": "enable preview in UI", + "name": "preview", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } } }, "securitySchemes": { diff --git a/src/httpaste/views/container/get_paste_form.html b/src/httpaste/views/container/get_paste_form.html new file mode 100644 index 0000000..d3f658a --- /dev/null +++ b/src/httpaste/views/container/get_paste_form.html @@ -0,0 +1,26 @@ +
\ No newline at end of file diff --git a/src/httpaste/views/container/post_paste_form.html b/src/httpaste/views/container/post_paste_form.html new file mode 100644 index 0000000..31ed83c --- /dev/null +++ b/src/httpaste/views/container/post_paste_form.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/src/httpaste/views/frame/base.html b/src/httpaste/views/frame/base.html new file mode 100644 index 0000000..8d6a422 --- /dev/null +++ b/src/httpaste/views/frame/base.html @@ -0,0 +1,58 @@ + + + +Preview is disabled.
+
+ This probably happened because the paste is set to expire after read.
+
+ You can still proceed to condition the paste URL.
+
+ + Flush Local HTTP Authentication Cache + +
+ + \ No newline at end of file diff --git a/src/httpaste/views/viewport/ui/search.html b/src/httpaste/views/viewport/ui/search.html new file mode 100644 index 0000000..ddfe158 --- /dev/null +++ b/src/httpaste/views/viewport/ui/search.html @@ -0,0 +1,11 @@ +{% extends 'frame/decorated.html' %} + +{% block content %} +