-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
1,077 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
__pycache__ | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
# YABin CLI Client | ||
|
||
## Installation | ||
|
||
```bash | ||
pip install yabin | ||
``` | ||
|
||
## Usage | ||
|
||
You can specify the `BASE_URL` environment variable to change the default API URL, or use the `-b/--base-url` option. | ||
|
||
### Create a paste | ||
|
||
``` | ||
usage: yabin create [-h] [--encrypted] [--password PASSWORD] [--language LANGUAGE] [--expires-after SECONDS] [--burn-after-read] FILE|stdin | ||
positional arguments: | ||
FILE|stdin Content of the paste. If it is stdin, it will be read from stdin | ||
options: | ||
-h, --help show this help message and exit | ||
--encrypted, -e Encrypt the paste on client-side. Default: False | ||
--password PASSWORD, -p PASSWORD | ||
Password to encrypt the paste with. | ||
--language LANGUAGE, -l LANGUAGE | ||
(Programming) language of the paste. Default: plaintext | ||
--expires-after SECONDS, -x SECONDS | ||
Number of seconds after which the paste will expire. | ||
--burn-after-read, -b | ||
Delete the paste after it has been read once. | ||
``` | ||
|
||
### Read a paste | ||
|
||
``` | ||
usage: yabin read [-h] [--password PASSWORD] URL | ||
positional arguments: | ||
URL Complete URL of the paste to read. | ||
options: | ||
-h, --help show this help message and exit | ||
--password PASSWORD, -p PASSWORD | ||
Password to decrypt the paste with. Only needed if password-protected. | ||
``` |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[tool.poetry] | ||
name = "yabin" | ||
version = "0.1.1" | ||
description = "CLI client for YAbin" | ||
authors = ["Soham Sen <[email protected]>"] | ||
readme = "README.md" | ||
|
||
[tool.poetry.scripts] | ||
yabin = "yabin.cli:cli" | ||
|
||
[tool.poetry.dependencies] | ||
python = "^3.11" | ||
pycryptodome = "^3.18.0" | ||
requests = "^2.31.0" | ||
|
||
[tool.poetry.group.dev.dependencies] | ||
black = "^23.3.0" | ||
|
||
[build-system] | ||
requires = ["poetry-core"] | ||
build-backend = "poetry.core.masonry.api" |
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
import argparse | ||
import os | ||
import sys | ||
|
||
from .client import PasteClient | ||
|
||
|
||
def create_arg_parser(): | ||
base_url = os.environ.get("BASE_URL", "https://bin.sohamsen.me") | ||
|
||
parser = argparse.ArgumentParser( | ||
prog="yabin", | ||
description="A command line interface for yabin, a minimalistic but feature-rich pastebin.", | ||
) | ||
|
||
base_url_group = parser.add_mutually_exclusive_group(required=not base_url) | ||
base_url_group.add_argument( | ||
"--base-url", | ||
"-b", | ||
metavar="URL", | ||
default=base_url, | ||
help=f"Server base URL. Overrides BASE_URL environment variable if provided. Default: {base_url}", | ||
) | ||
|
||
subparsers = parser.add_subparsers( | ||
title="Subcommands", help="sub-command help", dest="command" | ||
) | ||
|
||
create_parser = subparsers.add_parser("create", help="Create a new paste.") | ||
create_parser.add_argument( | ||
"--encrypted", | ||
"-e", | ||
action="store_true", | ||
help="Encrypt the paste on client-side. Default: False", | ||
) | ||
create_parser.add_argument( | ||
"--password", | ||
"-p", | ||
metavar="PASSWORD", | ||
help="Password to encrypt the paste with.", | ||
) | ||
create_parser.add_argument( | ||
"--language", | ||
"-l", | ||
metavar="LANGUAGE", | ||
default="plaintext", | ||
help="(Programming) language of the paste. Default: plaintext", | ||
) | ||
create_parser.add_argument( | ||
"--expires-after", | ||
"-x", | ||
metavar="SECONDS", | ||
type=int, | ||
help="Number of seconds after which the paste will expire.", | ||
) | ||
create_parser.add_argument( | ||
"--burn-after-read", | ||
"-b", | ||
action="store_true", | ||
help="Delete the paste after it has been read once.", | ||
) | ||
create_parser.add_argument( | ||
"file", | ||
metavar="FILE|stdin", | ||
help="Content of the paste. If it is stdin, it will be read from stdin", | ||
) | ||
|
||
read_parser = subparsers.add_parser("read", help="Read an existing paste.") | ||
read_parser.add_argument( | ||
"--password", | ||
"-p", | ||
metavar="PASSWORD", | ||
help="Password to decrypt the paste with. Only needed if password-protected.", | ||
) | ||
|
||
read_parser.add_argument( | ||
"url", | ||
metavar="URL", | ||
help="Complete URL of the paste to read.", | ||
) | ||
|
||
return parser | ||
|
||
|
||
def create_paste(client: PasteClient, args): | ||
content = None | ||
|
||
if args.file == "stdin": | ||
content = sys.stdin.read() | ||
else: | ||
print(args.file) | ||
try: | ||
with open(args.file, "r") as f: | ||
content = f.read() | ||
except Exception as e: | ||
print(f"error: {e}", file=sys.stderr) | ||
return | ||
|
||
if not content: | ||
print("error: File is empty.", file=sys.stderr) | ||
return | ||
|
||
try: | ||
paste_url = client.create( | ||
content, | ||
encrypt=args.encrypted, | ||
password=args.password, | ||
language=args.language, | ||
expires_after_seconds=args.expires_after, | ||
burn_after_read=args.burn_after_read, | ||
) | ||
print(paste_url) | ||
except Exception as e: | ||
print(f"error: {e}", file=sys.stderr) | ||
return | ||
|
||
pass | ||
|
||
|
||
def read_paste(client: PasteClient, args): | ||
url = args.url | ||
if not url.startswith(client.base_url): | ||
print( | ||
f"error: Invalid URL: {url}. Must be from {client.base_url}.", | ||
file=sys.stderr, | ||
) | ||
return | ||
|
||
try: | ||
content = client.get_from_url(url, password=args.password) | ||
print(content) | ||
except Exception as e: | ||
print(f"error: {e}", file=sys.stderr) | ||
return | ||
|
||
|
||
def cli(): | ||
parser = create_arg_parser() | ||
args = parser.parse_args() | ||
|
||
client = PasteClient(args.base_url) | ||
|
||
if args.command == "create": | ||
return create_paste(client, args) | ||
if args.command == "read": | ||
return read_paste(client, args) | ||
|
||
if args.command: | ||
print("error: Invalid command.\n", file=sys.stderr) | ||
parser.print_help(sys.stderr) | ||
|
||
|
||
if __name__ == "__main__": | ||
cli() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
from typing import Optional | ||
from urllib.parse import quote, unquote | ||
|
||
import requests | ||
|
||
from . import crypto | ||
|
||
|
||
class PasteClient: | ||
def __init__(self, base_url: str): | ||
self.base_url = base_url | ||
self.api_url = f"{self.base_url}/api" | ||
self.paste_url = f"{self.api_url}/paste" | ||
|
||
def __repr__(self) -> str: | ||
return f"<PasteClient base_url={self.base_url}>" | ||
|
||
def __str__(self) -> str: | ||
return f"<PasteClient base_url={self.base_url}>" | ||
|
||
def _make_paste_url(self, paste_id: str, key: str = None) -> str: | ||
keyStr = f"#{quote(key)}" if key else "" | ||
return f"{self.base_url}/{paste_id}{keyStr}" | ||
|
||
def create( | ||
self, | ||
content: str, | ||
*, | ||
language: str = "plaintext", | ||
encrypt: bool = False, | ||
password: Optional[str] = None, | ||
expires_after_seconds: Optional[int] = None, | ||
burn_after_read: Optional[bool] = False, | ||
) -> str: | ||
initVector = None | ||
key = None | ||
if encrypt and not password: | ||
crypt = crypto.encrypt(content) | ||
content = crypt["ciphertext"] | ||
initVector = crypt["iv"] | ||
key = crypt["key"] | ||
|
||
if password: | ||
encrypt = True | ||
crypt = crypto.encrypt_with_password(content, password) | ||
content = crypt["ciphertext"] | ||
initVector = crypt["iv"] | ||
|
||
body = { | ||
"content": content, | ||
"passwordProtected": not not password, | ||
"initVector": initVector, | ||
"config": { | ||
"language": language, | ||
"encrypted": encrypt, | ||
"expiresAfter": expires_after_seconds, | ||
"burnAfterRead": burn_after_read, | ||
}, | ||
} | ||
|
||
response = requests.post(self.paste_url, json=body).json() | ||
if not response["success"]: | ||
raise Exception(response["error"]) | ||
|
||
paste_key = response["data"]["key"] | ||
|
||
return self._make_paste_url(paste_key, key) | ||
|
||
def get( | ||
self, | ||
paste_id: str, | ||
*, | ||
key: Optional[str] = None, | ||
password: Optional[str] = None, | ||
) -> str: | ||
response = requests.get(f"{self.paste_url}?key={paste_id}").json() | ||
if not response["success"]: | ||
raise Exception(response["error"]) | ||
|
||
data = response["data"] | ||
content = data["content"] | ||
encrypted = data["encrypted"] | ||
password_protected = data["passwordProtected"] | ||
init_vector = data["initVector"] | ||
|
||
if not encrypted: | ||
return content | ||
|
||
if encrypted and not password_protected: | ||
if not key: | ||
raise ValueError("Key required") | ||
|
||
return crypto.decrypt(content, init_vector, key) | ||
|
||
if password_protected: | ||
if not password: | ||
raise ValueError("Password required") | ||
|
||
return crypto.decrypt_with_password(content, init_vector, password) | ||
|
||
raise ValueError("Failed sanity check, how did we get here?") | ||
|
||
def get_from_url(self, url: str, *, password: Optional[str] = None) -> str: | ||
if not url.startswith(self.base_url): | ||
raise ValueError("Invalid URL") | ||
|
||
key_data = url[len(self.base_url) + 1 :] | ||
if "#" in key_data: | ||
paste_id, key = key_data.split("#") | ||
key = unquote(key) | ||
else: | ||
paste_id = key_data | ||
key = None | ||
|
||
return self.get(paste_id, key=key, password=password) |
Oops, something went wrong.