added privatebin tool and tools export
This commit is contained in:
+10
-3
@@ -7,6 +7,7 @@ import traceback
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from rokuHandler import RokuWrapper, ROKU_IP
|
from rokuHandler import RokuWrapper, ROKU_IP
|
||||||
|
from privatebinHandler import PrivateBinWrapper
|
||||||
|
|
||||||
HOST = os.environ.get("HOST", "0.0.0.0")
|
HOST = os.environ.get("HOST", "0.0.0.0")
|
||||||
PORT = int(os.environ.get("PORT", "1331"))
|
PORT = int(os.environ.get("PORT", "1331"))
|
||||||
@@ -19,6 +20,7 @@ class HTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
super().setup()
|
super().setup()
|
||||||
self.rapp = RokuWrapper(self)
|
self.rapp = RokuWrapper(self)
|
||||||
|
self.pbin = PrivateBinWrapper(self)
|
||||||
|
|
||||||
def _cors(self):
|
def _cors(self):
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
@@ -53,11 +55,16 @@ class HTTPHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# specs
|
# specs
|
||||||
if path == "/roku/openapi.json":
|
|
||||||
return self._send_file(ROOT / "spec" / "roku.openapi.json", "application/json")
|
|
||||||
|
|
||||||
if path.startswith("/roku"):
|
if path.startswith("/roku"):
|
||||||
|
if path == "/roku/openapi.json":
|
||||||
|
return self._send_file(ROOT / "spec" / "roku.openapi.json", "application/json")
|
||||||
|
|
||||||
return self.rapp.run_command(path.replace("/roku/", ''))
|
return self.rapp.run_command(path.replace("/roku/", ''))
|
||||||
|
|
||||||
|
if path.startswith("/privatebin"):
|
||||||
|
if path == "/privatebin/openapi.json":
|
||||||
|
return self._send_file(ROOT / "spec" / "privatebin.openapi.json", "application/json")
|
||||||
|
return self.pbin.run_command(path.replace("/privatebin/", ''))
|
||||||
|
|
||||||
# catch-all
|
# catch-all
|
||||||
self._send(404, json.dumps({"error": "unknown endpoint"}).encode())
|
self._send(404, json.dumps({"error": "unknown endpoint"}).encode())
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from typing import Dict, Any
|
||||||
|
import json
|
||||||
|
import urllib.parse as urlparse
|
||||||
|
import privatebinapi
|
||||||
|
|
||||||
|
class PrivateBinWrapper():
|
||||||
|
def __init__(self, parent):
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
# read query params from the current request
|
||||||
|
def _qs(self) -> Dict[str, Any]:
|
||||||
|
parsed = urlparse.urlparse(self.parent.path)
|
||||||
|
|
||||||
|
# parse_qs returns lists and flatten singletons
|
||||||
|
raw = urlparse.parse_qs(parsed.query, keep_blank_values=True)
|
||||||
|
flat: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
for k, v in raw.items():
|
||||||
|
flat[k] = v[0] if len(v) == 1 else v
|
||||||
|
return flat
|
||||||
|
|
||||||
|
def _ok(self, payload: Dict[str, Any], code: int = 200):
|
||||||
|
body = json.dumps(payload).encode()
|
||||||
|
self.parent._send(code, body)
|
||||||
|
|
||||||
|
def _err(self, msg: str, code: int = 400):
|
||||||
|
self._ok({"error": msg}, code)
|
||||||
|
|
||||||
|
def _create(self, qs: Dict[str, Any]):
|
||||||
|
base_url = qs.get("base_url")
|
||||||
|
text = qs.get("text")
|
||||||
|
password = qs.get("password")
|
||||||
|
expiration = qs.get("expiration", "1day")
|
||||||
|
formatting = qs.get("formatting", "plaintext")
|
||||||
|
burn_after_reading = qs.get("burn_after_reading", "false").lower() == "true"
|
||||||
|
discussion = qs.get("discussion", "false").lower() == "true"
|
||||||
|
|
||||||
|
if not base_url:
|
||||||
|
return self._err("missing required 'base_url'")
|
||||||
|
if not text:
|
||||||
|
return self._err("missing required 'text'")
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = privatebinapi.send(
|
||||||
|
base_url,
|
||||||
|
text=text,
|
||||||
|
password=password,
|
||||||
|
expiration=expiration,
|
||||||
|
formatting=formatting,
|
||||||
|
burn_after_reading=burn_after_reading,
|
||||||
|
discussion=discussion,
|
||||||
|
)
|
||||||
|
# resp already contains: status, id, url, full_url, deletetoken, passcode?
|
||||||
|
return self._ok(resp, 200)
|
||||||
|
except Exception as e:
|
||||||
|
return self._err(f"privatebin send failed: {e}", 502)
|
||||||
|
|
||||||
|
def _read(self, qs: Dict[str, Any]):
|
||||||
|
full_url = qs.get("full_url")
|
||||||
|
password = qs.get("password")
|
||||||
|
if not full_url:
|
||||||
|
return self._err("missing required 'full_url'")
|
||||||
|
try:
|
||||||
|
resp = privatebinapi.get(full_url, password=password)
|
||||||
|
# has status, id, url, v, text, meta, attachment
|
||||||
|
return self._ok(resp, 200)
|
||||||
|
except Exception as e:
|
||||||
|
return self._err(f"privatebin get failed: {e}", 502)
|
||||||
|
|
||||||
|
def _delete(self, qs: Dict[str, Any]):
|
||||||
|
full_url = qs.get("full_url")
|
||||||
|
deletetoken = qs.get("deletetoken")
|
||||||
|
if not full_url or not deletetoken:
|
||||||
|
return self._err("missing required 'full_url' or 'deletetoken'")
|
||||||
|
try:
|
||||||
|
resp = privatebinapi.delete(full_url, deletetoken)
|
||||||
|
if resp == None:
|
||||||
|
return self._err("response is null!")
|
||||||
|
else:
|
||||||
|
return self._ok(resp, 200)
|
||||||
|
except Exception as e:
|
||||||
|
return self._err(f"privatebin delete failed: {e}", 502)
|
||||||
|
|
||||||
|
def run_command(self, cmd_path: str):
|
||||||
|
# expected forms:
|
||||||
|
# /privatebin/create?base_url=...&text=...&password=...&expiration=...
|
||||||
|
# /privatebin/read?full_url=...&password=...
|
||||||
|
# /privatebin/delete?full_url=...&deletetoken=...
|
||||||
|
cmd = (cmd_path or "").strip("/").lower()
|
||||||
|
qs = self._qs()
|
||||||
|
|
||||||
|
if cmd == "create":
|
||||||
|
return self._create(qs)
|
||||||
|
if cmd == "read":
|
||||||
|
return self._read(qs)
|
||||||
|
if cmd == "delete":
|
||||||
|
return self._delete(qs)
|
||||||
|
|
||||||
|
# unknown subpath
|
||||||
|
return self._err("unknown privatebin command", 404)
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
|
anyio==4.10.0
|
||||||
|
argcomplete==3.6.2
|
||||||
|
base58==2.1.1
|
||||||
certifi==2025.8.3
|
certifi==2025.8.3
|
||||||
charset-normalizer==3.4.3
|
charset-normalizer==3.4.3
|
||||||
|
h11==0.16.0
|
||||||
|
httpcore==1.0.9
|
||||||
|
httpx==0.28.1
|
||||||
idna==3.10
|
idna==3.10
|
||||||
|
PBinCLI==0.3.7
|
||||||
|
PrivateBinAPI==1.0.0
|
||||||
|
pycryptodome==3.23.0
|
||||||
|
PySocks==1.7.1
|
||||||
requests==2.32.5
|
requests==2.32.5
|
||||||
roku==4.1.0
|
roku==4.1.0
|
||||||
|
sjcl==0.2.1
|
||||||
|
sniffio==1.3.1
|
||||||
urllib3==2.5.0
|
urllib3==2.5.0
|
||||||
|
|||||||
@@ -0,0 +1,367 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.1.0",
|
||||||
|
"info": {
|
||||||
|
"title": "PrivateBin HTTP API",
|
||||||
|
"description": "wrapper server for privatebin API (create/read/delete) via query parameters",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"url": "http://{host}:{port}",
|
||||||
|
"variables": {
|
||||||
|
"host": {
|
||||||
|
"default": "0.0.0.0"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"default": "1331"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"/privatebin/create": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "privatebinCreate",
|
||||||
|
"summary": "create a privatebin paste",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "base_url",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"description": "root url of your privatebin instance, e.g. https://privatebin.example"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "text",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "plain text of the paste"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "optional password to protect the paste"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "expiration",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"5min",
|
||||||
|
"10min",
|
||||||
|
"1hour",
|
||||||
|
"1day",
|
||||||
|
"1week",
|
||||||
|
"1month",
|
||||||
|
"1year",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"default": "1day"
|
||||||
|
},
|
||||||
|
"description": "expire time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "formatting",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"plaintext",
|
||||||
|
"syntaxhighlighting",
|
||||||
|
"markdown"
|
||||||
|
],
|
||||||
|
"default": "plaintext"
|
||||||
|
},
|
||||||
|
"description": "one of plaintext, syntaxhighlighting, markdown"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "burn_after_reading",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"description": "delete after first read"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "discussion",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"description": "enable comments/discussion"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste created successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/CreatePasteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "invalid request parameters",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"description": "upstream privatebin send failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/privatebin/read": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "privatebinRead",
|
||||||
|
"summary": "read a privatebin paste",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "full_url",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"description": "full url of the paste (including key fragment)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "password",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "password if the paste is protected"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste fetched successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ReadPasteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "missing full_url parameter",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"description": "upstream privatebin get failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/privatebin/delete": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "privatebinDelete",
|
||||||
|
"summary": "delete a privatebin paste",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "full_url",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
"description": "full url of the paste (including key fragment)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deletetoken",
|
||||||
|
"in": "query",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "deletetoken returned upon create"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "paste deleted successfully",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/DeletePasteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "missing full_url or deletetoken",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"502": {
|
||||||
|
"description": "upstream privatebin delete failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"CreatePasteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"full_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"deletetoken": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"passcode": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
"full_url",
|
||||||
|
"deletetoken"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ReadPasteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"v": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"attachment": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true,
|
||||||
|
"description": "attachment object: includes filename and content (base64 or bytes)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"id",
|
||||||
|
"url",
|
||||||
|
"v"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"DeletePasteResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"status",
|
||||||
|
"id",
|
||||||
|
"url"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Error": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"error": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user