Skip to content

Commit

Permalink
Added README
Browse files Browse the repository at this point in the history
  • Loading branch information
Yureien committed Jun 17, 2023
1 parent 83da714 commit 042e62a
Show file tree
Hide file tree
Showing 11 changed files with 1,077 additions and 2 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,19 @@ Well, cause no pastebin I could find had ALL of the following features:

See [API.md](API.md).

## How to use
## CLI Usage

### Installation and Usage

```bash
pip install yabin
yabin create
yabin read "<URL>"
```

See [cli/README.md](cli/README.md) for detailed instructions and library usage.

## How to Host

**Requirements:** Node.js (tested on 18+, should work with 14+), and a SQL database (tested on PostgreSQL, should work with MySQL and SQLite).

Expand All @@ -41,6 +53,6 @@ yarn dev
docker run --env-file .env -it -p 3000:3000 yureien/yabin:latest
```

#### In a serverless environment (Cloudflare Workers, Netlify, Vercel, etc.)
#### In a Serverless Environment (Cloudflare Workers, Netlify, Vercel, etc.)

I have not yet tested this, but this is made with SvelteKit. Please take a look at the [SvelteKit documentation](https://kit.svelte.dev/docs/adapters) for more information. If there are any issues, please open an issue, and I will put up a proper guide on how to deploy on such environmments.
2 changes: 2 additions & 0 deletions cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
__pycache__
dist
46 changes: 46 additions & 0 deletions cli/README.md
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.
```
311 changes: 311 additions & 0 deletions cli/poetry.lock

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions cli/pyproject.toml
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 added cli/tests/__init__.py
Empty file.
Empty file added cli/yabin/__init__.py
Empty file.
154 changes: 154 additions & 0 deletions cli/yabin/cli.py
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()
115 changes: 115 additions & 0 deletions cli/yabin/client.py
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)
Loading

0 comments on commit 042e62a

Please sign in to comment.