Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9a4079855 | ||
|
|
3965efef89 | ||
|
|
51170975c7 | ||
|
|
cc791da03c | ||
|
|
906c14fddf | ||
|
|
0e61d05c9f | ||
|
|
05c1938aa6 | ||
|
|
369738ea50 | ||
|
|
7bd3ef22b9 | ||
|
|
f05f65ea61 | ||
|
|
3c2cdb10ac | ||
|
|
28f7c09f83 | ||
|
|
b4ffc1a06b | ||
|
|
58645e1d32 | ||
|
|
82ca95f01a | ||
|
|
5589ba0437 | ||
|
|
6c9a5c95b6 | ||
|
|
505a0104ca | ||
|
|
94023a986d | ||
|
|
5909e7330b | ||
|
|
d0f23094bc | ||
|
|
86061e595c | ||
|
|
49362c70a6 | ||
|
|
70328903fa | ||
|
|
29498b9315 | ||
|
|
19f130feb1 | ||
|
|
682b47fbd3 | ||
|
|
9d82c727b6 | ||
|
|
c425d86ed6 | ||
|
|
3ed06686ab | ||
|
|
7e4fb0a8c4 | ||
|
|
ebfe0c48a0 | ||
|
|
45d854e590 | ||
|
|
18d79c8e04 | ||
|
|
92c38344e3 | ||
|
|
b596f42b7e | ||
|
|
86c5051fcf | ||
|
|
f838f8ee94 | ||
|
|
fb7a93732d | ||
|
|
181763070c | ||
|
|
c3a491ac46 | ||
|
|
641c55a6a2 | ||
|
|
432675f2e6 | ||
|
|
635c87dabd | ||
|
|
6b6c33e545 | ||
|
|
7c5ba2fdbe | ||
|
|
692335ee62 | ||
|
|
c63256c628 | ||
|
|
66659da66d | ||
|
|
32d60b5321 | ||
|
|
9e9f480075 | ||
|
|
7fc2a1a625 | ||
|
|
8aea956e77 | ||
|
|
ac58fff9ce | ||
|
|
7a59d1acd1 | ||
|
|
5c6b9611d8 | ||
|
|
dc034b1d55 | ||
|
|
5d988e01fc | ||
|
|
85cc1454ea | ||
|
|
f5ef4bbc03 | ||
|
|
8d7a9235b8 | ||
|
|
70f386193a | ||
|
|
d37e573d9e | ||
|
|
e850b5495a | ||
|
|
9390edeb79 | ||
|
|
d11beb10af | ||
|
|
4c124a33c0 | ||
|
|
5b38c532a2 | ||
|
|
f7fae450a0 | ||
|
|
1ff6e721c7 | ||
|
|
487def2b45 | ||
|
|
c27606d442 | ||
|
|
2381893b72 | ||
|
|
03b4fcc2ea | ||
|
|
587dec6b6a |
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.github/** export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
README.md export-ignore
|
||||
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "17:00"
|
||||
open-pull-requests-limit: 10
|
||||
137
.gitignore
vendored
137
.gitignore
vendored
@@ -1,2 +1,137 @@
|
||||
*.pyc
|
||||
# Gedit/other editors
|
||||
*.bak
|
||||
*~
|
||||
|
||||
# Vim.gitignore
|
||||
# https://github.com/github/gitignore/blob/master/Global/Vim.gitignore
|
||||
# Swap
|
||||
[._]*.s[a-v][a-z]
|
||||
[._]*.sw[a-p]
|
||||
[._]s[a-v][a-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
|
||||
# Temporary
|
||||
.netrwhist
|
||||
*~
|
||||
# Auto-generated tag files
|
||||
tags
|
||||
# <>
|
||||
|
||||
# Python.gitignore
|
||||
# https://github.com/github/gitignore/blob/master/Python.gitignore
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
-pbincli.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
.static_storage/
|
||||
.media/
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
# <>
|
||||
|
||||
# PyCharm project settings
|
||||
.idea
|
||||
|
||||
# PBinCLI downloaded paste text
|
||||
paste-*.txt
|
||||
|
||||
18
LICENSE
18
LICENSE
@@ -1,17 +1,7 @@
|
||||
DWTFYWWI LICENSE
|
||||
Version 1, January 2006
|
||||
Copyright 2017 © R4SAS <r4sas@i2pmail.org>
|
||||
|
||||
Copyright (C) 2017-2018 R4SAS <r4sas@i2pmail.org>
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
Preamble
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the DWTFYWWI or Do
|
||||
Whatever The Fuck You Want With It license is intended to guarantee
|
||||
your freedom to share and change the software--to make sure the
|
||||
software is free for all its users.
|
||||
|
||||
DWTFYWWI LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
0. The author grants everyone permission to do whatever the fuck they
|
||||
want with the software, whatever the fuck that may be.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
||||
include LICENSE
|
||||
include README.rst
|
||||
include requirements.txt
|
||||
151
README.md
151
README.md
@@ -1,61 +1,138 @@
|
||||
PBinCLI
|
||||
=====
|
||||
[](https://github.com/r4sas/PBinCLI/blob/master/LICENSE)
|
||||
[](https://github.com/r4sas/PBinCLI/tags/)
|
||||
[](https://www.codacy.com/gh/r4sas/PBinCLI/dashboard?utm_source=github.com&utm_medium=referral&utm_content=r4sas/PBinCLI&utm_campaign=Badge_Grade)
|
||||
|
||||
#### [PrivateBin](https://github.com/PrivateBin/PrivateBin/) CLI
|
||||
# PBinCLI
|
||||
|
||||
Installing
|
||||
-----
|
||||
PBinCLI is a command line client for [PrivateBin](https://github.com/PrivateBin/PrivateBin/) written in Python 3.
|
||||
|
||||
# Installation
|
||||
|
||||
Installing globally using pip3:
|
||||
```bash
|
||||
$ virtualenv --python=python3 venv
|
||||
$ . venv/bin/activate
|
||||
$ pip install pbincli
|
||||
pip3 install pbincli
|
||||
```
|
||||
|
||||
Usage
|
||||
-----
|
||||
By default pbincli configured to use https://paste.i2pd.xyz/ for sending and receiving pastes. No proxy used by default.
|
||||
|
||||
You can create config file with variables `server` and `proxy` in `~/.config/pbincli/pbincli.conf` to use different settings.
|
||||
|
||||
Example contents:
|
||||
|
||||
Installing with `virtualenv`:
|
||||
```bash
|
||||
python3 -m virtualenv --python=python3 venv
|
||||
. venv/bin/activate
|
||||
pip install pbincli
|
||||
```
|
||||
|
||||
*Note*: if you used `virtualenv` installation method, don't forget to activate your virtual environment before running the tool: call `. /path/to/venv/bin/activate` in terminal
|
||||
|
||||
# Configuration
|
||||
|
||||
By default PBinCLI is configured to use `https://paste.i2pd.xyz/` for sending and receiving pastes. No proxy is used by default.
|
||||
|
||||
You can always create a config file to use different settings.
|
||||
|
||||
Configuration file is expected to be found in `~/.config/pbincli/pbincli.conf`, `%APPDATA%/pbincli/pbincli.conf` (Windows) and `~/Library/Application Support/pbincli/pbincli.conf` (MacOS)
|
||||
|
||||
## Example of config file content
|
||||
|
||||
```ini
|
||||
server=https://paste.i2pd.xyz/
|
||||
proxy=http://127.0.0.1:3128
|
||||
```
|
||||
|
||||
Run inside `venv` command:
|
||||
## List of OPTIONS available
|
||||
|
||||
$ pbincli send --text "Hello!"
|
||||
| Option | Default | Possible value |
|
||||
|----------------------|-------------------------|----------------|
|
||||
| server | https://paste.i2pd.xyz/ | Domain ending with slash |
|
||||
| mirrors | None | Domains separated with comma, like `http://privatebin.ygg/,http://privatebin.i2p/` |
|
||||
| proxy | None | Proxy address starting with scheme `http://` or `socks5://` |
|
||||
| expire | 1day | 5min / 10min / 1hour / 1day / 1week / 1month / 1year / never |
|
||||
| burn | False | True / False |
|
||||
| discus | False | True / False |
|
||||
| format | plaintext | plaintext / syntaxhighlighting / markdown |
|
||||
| short | False | True / False |
|
||||
| short_api | None | `tinyurl`, `clckru`, `isgd`, `vgd`, `cuttly`, `yourls`, `custom` |
|
||||
| short_url | None | Domain name of shortener service for `yourls`, or URL (with required parameters) for `custom` |
|
||||
| short_user | None | Used only in `yourls` |
|
||||
| short_pass | None | Used only in `yourls` |
|
||||
| short_token | None | Used only in `yourls` |
|
||||
| no_check_certificate | False | True / False |
|
||||
| no_insecure_warning | False | True / False |
|
||||
| compression | zlib | zlib / none |
|
||||
|
||||
Or use stdin input to read text for paste:
|
||||
# Usage
|
||||
|
||||
$ pbincli send - <<EOF
|
||||
Hello! This is test paste!
|
||||
EOF
|
||||
PBinCLI tool is started with `pbincli` command. Detailed help on command usage is provided with `-h` option:
|
||||
```bash
|
||||
pbincli {send|get|delete} -h
|
||||
```
|
||||
|
||||
It will send string `Hello! This is test paste!` to PrivateBin.
|
||||
## Sending
|
||||
|
||||
To send file use `--file` or `-f` with filename. Example:
|
||||
* Sending text:
|
||||
```bash
|
||||
pbincli send -t "Hello! This is a test paste!"
|
||||
```
|
||||
|
||||
$ pbincli send -c "My document" -f info.pdf
|
||||
* Using stdin input to read text into a paste:
|
||||
```bash
|
||||
pbincli send - <<EOF
|
||||
Hello! This is a test paste!
|
||||
EOF
|
||||
```
|
||||
|
||||
* Sending a file with text attached into a paste:
|
||||
```bash
|
||||
pbincli send -f info.pdf -t "I'm sending my document."
|
||||
```
|
||||
|
||||
To retrieve paste from server, use `get` command with paste info.
|
||||
* Sending a file only with no text attached:
|
||||
```bash
|
||||
pbincli send -q -f info.pdf
|
||||
```
|
||||
|
||||
It must be formated like `pasteID#passphrase`. Example:
|
||||
### Other options
|
||||
|
||||
$ pbincli get 49eeb1326cfa9491#vfeortoVWaYeJlviDdhxQBtj5e0I2kArpynrtu/tnGs=
|
||||
It is also possible to set-up paste parameters such as "burn after reading", expiritaion time, formatting, enabling discussions and changing compression algorithm. Please refer to `pbincli send -h` output for more information.
|
||||
|
||||
More info you can find by typing
|
||||
## Receiving
|
||||
|
||||
$ pbincli [-h] {send, get, delete}
|
||||
To retrieve a paste from a server, you need to use `get` command with the paste info.
|
||||
|
||||
TODO
|
||||
----
|
||||
Write a more complete usage documentation.
|
||||
Paste info must be formated as `pasteID#Passphrase` or just use full URL to a paste. Example:
|
||||
```bash
|
||||
pbincli get xxx#yyy ### receive paste xxx from https://paste.i2pd.xyz/ by default
|
||||
pbincli get https://example.com/?xxx#yyy ### receive paste xxx from https://example.com/
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
This project is licensed under the DWTFYWWI license, which can be found in the file
|
||||
[LICENSE](LICENSE) in the root of the project source code.
|
||||
## Deletion
|
||||
|
||||
To delete a paste from a server, use `delete` command with required `-p` and `-t` options:
|
||||
```bash
|
||||
pbincli delete -p xxx -t deletetoken
|
||||
```
|
||||
|
||||
If you need to delete a paste on different server than the configured one, use `-s` option together with the instance URL.
|
||||
|
||||
# Additional examples
|
||||
|
||||
Here you can find additional examples.
|
||||
|
||||
## Usage with I2P enabled services
|
||||
|
||||
Change settings to set server to `http://privatebin.i2p/` and proxy to `http://127.0.0.1:4444`. Configuration file for this example is:
|
||||
```ini
|
||||
server=http://privatebin.i2p/
|
||||
proxy=http://127.0.0.1:4444
|
||||
```
|
||||
|
||||
## Using aliases
|
||||
|
||||
Example of alias to send a paste from `stdin` direclty to I2P service:
|
||||
```bash
|
||||
alias pastei2p="echo 'paste the text to stdin' && pbincli send -s http://privatebin.i2p/ -x http://127.0.0.1:4444 -"
|
||||
```
|
||||
|
||||
Call it by running `pastei2p` in terminal.
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under the MIT license, which can be found in the file [LICENSE](https://github.com/r4sas/PBinCLI/blob/master/LICENSE) in the root of the project source code.
|
||||
|
||||
218
README.rst
Normal file
218
README.rst
Normal file
@@ -0,0 +1,218 @@
|
||||
|
||||
|
||||
.. image:: https://img.shields.io/github/license/r4sas/PBinCLI.svg
|
||||
:target: https://github.com/r4sas/PBinCLI/blob/master/LICENSE
|
||||
:alt: GitHub license
|
||||
|
||||
|
||||
.. image:: https://img.shields.io/github/tag/r4sas/PBinCLI.svg
|
||||
:target: https://github.com/r4sas/PBinCLI/tags/
|
||||
:alt: GitHub tag
|
||||
|
||||
|
||||
.. image:: https://app.codacy.com/project/badge/Grade/4f24f43356a84621bbd9078c4b3f1b70
|
||||
:target: https://www.codacy.com/gh/r4sas/PBinCLI/dashboard?utm_source=github.com&utm_medium=referral&utm_content=r4sas/PBinCLI&utm_campaign=Badge_Grade
|
||||
:alt: Codacy Badge
|
||||
|
||||
|
||||
PBinCLI
|
||||
=======
|
||||
|
||||
PBinCLI is a command line client for `PrivateBin <https://github.com/PrivateBin/PrivateBin/>`_ written in Python 3.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
Installing globally using pip3:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip3 install pbincli
|
||||
|
||||
Installing with ``virtualenv``\ :
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 -m virtualenv --python=python3 venv
|
||||
. venv/bin/activate
|
||||
pip install pbincli
|
||||
|
||||
*Note*\ : if you used ``virtualenv`` installation method, don't forget to activate your virtual environment before running the tool: call ``. /path/to/venv/bin/activate`` in terminal
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
By default PBinCLI is configured to use ``https://paste.i2pd.xyz/`` for sending and receiving pastes. No proxy is used by default.
|
||||
|
||||
You can always create a config file to use different settings.
|
||||
|
||||
Configuration file is expected to be found in ``~/.config/pbincli/pbincli.conf``\ , ``%APPDATA%/pbincli/pbincli.conf`` (Windows) and ``~/Library/Application Support/pbincli/pbincli.conf`` (MacOS)
|
||||
|
||||
Example of config file content
|
||||
------------------------------
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
server=https://paste.i2pd.xyz/
|
||||
proxy=http://127.0.0.1:3128
|
||||
|
||||
List of OPTIONS available
|
||||
-------------------------
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - Option
|
||||
- Default
|
||||
- Possible value
|
||||
* - server
|
||||
- https://paste.i2pd.xyz/
|
||||
- Domain ending with slash
|
||||
* - mirrors
|
||||
- None
|
||||
- Domains separated with comma, like ``http://privatebin.ygg/,http://privatebin.i2p/``
|
||||
* - proxy
|
||||
- None
|
||||
- Proxy address starting with scheme ``http://`` or ``socks5://``
|
||||
* - expire
|
||||
- 1day
|
||||
- 5min / 10min / 1hour / 1day / 1week / 1month / 1year / never
|
||||
* - burn
|
||||
- False
|
||||
- True / False
|
||||
* - discus
|
||||
- False
|
||||
- True / False
|
||||
* - format
|
||||
- plaintext
|
||||
- plaintext / syntaxhighlighting / markdown
|
||||
* - short
|
||||
- False
|
||||
- True / False
|
||||
* - short_api
|
||||
- None
|
||||
- ``tinyurl``\ , ``clckru``\ , ``isgd``\ , ``vgd``\ , ``cuttly``\ , ``yourls``\ , ``custom``
|
||||
* - short_url
|
||||
- None
|
||||
- Domain name of shortener service for ``yourls``\ , or URL (with required parameters) for ``custom``
|
||||
* - short_user
|
||||
- None
|
||||
- Used only in ``yourls``
|
||||
* - short_pass
|
||||
- None
|
||||
- Used only in ``yourls``
|
||||
* - short_token
|
||||
- None
|
||||
- Used only in ``yourls``
|
||||
* - no_check_certificate
|
||||
- False
|
||||
- True / False
|
||||
* - no_insecure_warning
|
||||
- False
|
||||
- True / False
|
||||
* - compression
|
||||
- zlib
|
||||
- zlib / none
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
PBinCLI tool is started with ``pbincli`` command. Detailed help on command usage is provided with ``-h`` option:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli {send|get|delete} -h
|
||||
|
||||
Sending
|
||||
-------
|
||||
|
||||
|
||||
*
|
||||
Sending text:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli send -t "Hello! This is a test paste!"
|
||||
|
||||
*
|
||||
Using stdin input to read text into a paste:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli send - <<EOF
|
||||
Hello! This is a test paste!
|
||||
EOF
|
||||
|
||||
*
|
||||
Sending a file with text attached into a paste:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli send -f info.pdf -t "I'm sending my document."
|
||||
|
||||
*
|
||||
Sending a file only with no text attached:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli send -q -f info.pdf
|
||||
|
||||
Other options
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
It is also possible to set-up paste parameters such as "burn after reading", expiritaion time, formatting, enabling discussions and changing compression algorithm. Please refer to ``pbincli send -h`` output for more information.
|
||||
|
||||
Receiving
|
||||
---------
|
||||
|
||||
To retrieve a paste from a server, you need to use ``get`` command with the paste info.
|
||||
|
||||
Paste info must be formated as ``pasteID#Passphrase`` or just use full URL to a paste. Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli get xxx#yyy ### receive paste xxx from https://paste.i2pd.xyz/ by default
|
||||
pbincli get https://example.com/?xxx#yyy ### receive paste xxx from https://example.com/
|
||||
|
||||
Deletion
|
||||
--------
|
||||
|
||||
To delete a paste from a server, use ``delete`` command with required ``-p`` and ``-t`` options:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pbincli delete -p xxx -t deletetoken
|
||||
|
||||
If you need to delete a paste on different server than the configured one, use ``-s`` option together with the instance URL.
|
||||
|
||||
Additional examples
|
||||
===================
|
||||
|
||||
Here you can find additional examples.
|
||||
|
||||
Usage with I2P enabled services
|
||||
-------------------------------
|
||||
|
||||
Change settings to set server to ``http://privatebin.i2p/`` and proxy to ``http://127.0.0.1:4444``. Configuration file for this example is:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
server=http://privatebin.i2p/
|
||||
proxy=http://127.0.0.1:4444
|
||||
|
||||
Using aliases
|
||||
-------------
|
||||
|
||||
Example of alias to send a paste from ``stdin`` direclty to I2P service:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
alias pastei2p="echo 'paste the text to stdin' && pbincli send -s http://privatebin.i2p/ -x http://127.0.0.1:4444 -"
|
||||
|
||||
Call it by running ``pastei2p`` in terminal.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
This project is licensed under the MIT license, which can be found in the file `LICENSE <https://github.com/r4sas/PBinCLI/blob/master/LICENSE>`_ in the root of the project source code.
|
||||
BIN
contrib/privatebin.ico
Normal file
BIN
contrib/privatebin.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
67
pbincli.spec
Normal file
67
pbincli.spec
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
from pkg_resources import parse_version
|
||||
from PyInstaller.utils.win32.versioninfo import VSVersionInfo, FixedFileInfo, StringFileInfo, StringTable, StringStruct, VarFileInfo, VarStruct
|
||||
from pbincli.__init__ import __version__ as pbincli_version, __copyright__ as pbincli_copyright
|
||||
|
||||
pbincli_ver = parse_version(pbincli_version)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(['pbincli\\cli.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False)
|
||||
pyz = PYZ(a.pure, a.zipped_data,
|
||||
cipher=block_cipher)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='pbincli-' + pbincli_version,
|
||||
version=VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
filevers=(pbincli_ver.major, pbincli_ver.minor, pbincli_ver.micro, 0),
|
||||
prodvers=(pbincli_ver.major, pbincli_ver.minor, pbincli_ver.micro, 0),
|
||||
mask=0x3f,
|
||||
flags=0x0,
|
||||
OS=0x40004,
|
||||
fileType=0x1,
|
||||
subtype=0x0,
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo([
|
||||
StringTable(
|
||||
u'040904B0',
|
||||
[
|
||||
StringStruct(u'FileDescription', u'PrivateBin CLI'),
|
||||
StringStruct(u'FileVersion', pbincli_version),
|
||||
StringStruct(u'InternalName', u'pbincli'),
|
||||
StringStruct(u'LegalCopyright', pbincli_copyright),
|
||||
StringStruct(u'OriginalFilename', u'pbincli-' + pbincli_version + u'.exe'),
|
||||
StringStruct(u'ProductName', u'PBinCLI'),
|
||||
StringStruct(u'ProductVersion', pbincli_version)
|
||||
]
|
||||
)
|
||||
]),
|
||||
VarFileInfo([VarStruct(u'Translation', [1033, 1200])])
|
||||
]
|
||||
),
|
||||
icon=['contrib\\privatebin.ico'],
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
runtime_tmpdir=None,
|
||||
console=True)
|
||||
@@ -2,6 +2,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__author__ = "R4SAS <r4sas@i2pmail.org>"
|
||||
__version__ = "0.1"
|
||||
__version__ = "0.3.2"
|
||||
__copyright__ = "Copyright (c) R4SAS"
|
||||
__license__ = "DWTFYWWI"
|
||||
__license__ = "MIT"
|
||||
|
||||
@@ -1,219 +1,206 @@
|
||||
import json, hashlib, ntpath, os, sys, zlib
|
||||
import pbincli.actions
|
||||
from sjcl import SJCL
|
||||
from pbincli.format import Paste
|
||||
from pbincli.utils import PBinCLIError, validate_url_ending
|
||||
import signal
|
||||
|
||||
from base64 import b64encode, b64decode
|
||||
from mimetypes import guess_type
|
||||
from pbincli.utils import PBinCLIException, check_readable, check_writable, json_load_byteified
|
||||
def signal_handler(sig, frame):
|
||||
print('Keyboard interrupt received, terminating…')
|
||||
exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
|
||||
def path_leaf(path):
|
||||
head, tail = ntpath.split(path)
|
||||
return tail or ntpath.basename(head)
|
||||
def send(args, api_client, settings=None):
|
||||
from pbincli.api import Shortener
|
||||
if args.short:
|
||||
shortener = Shortener(settings)
|
||||
|
||||
|
||||
def decompress(s):
|
||||
return zlib.decompress(bytearray(map(ord, b64decode(s.encode('utf-8')).decode('utf-8'))), -zlib.MAX_WBITS)
|
||||
|
||||
def compress(s):
|
||||
co = zlib.compressobj(wbits=-zlib.MAX_WBITS)
|
||||
b = co.compress(s) + co.flush()
|
||||
|
||||
return b64encode(''.join(map(chr, b)).encode('utf-8'))
|
||||
|
||||
def send(args, api_client):
|
||||
if args.stdin:
|
||||
text = args.stdin.read()
|
||||
elif args.text:
|
||||
text = args.text
|
||||
elif args.file:
|
||||
text = "Sending a file to you!"
|
||||
if not args.notext:
|
||||
if args.text:
|
||||
text = args.text
|
||||
elif args.stdin:
|
||||
print("Reading text from stdin…")
|
||||
text = args.stdin.read()
|
||||
elif not args.file:
|
||||
PBinCLIError("Nothing to send!")
|
||||
else:
|
||||
print("Nothing to send!")
|
||||
sys.exit(1)
|
||||
text = ""
|
||||
|
||||
# Formatting request
|
||||
request = {'expire':args.expire,'formatter':args.format,'burnafterreading':int(args.burn),'opendiscussion':int(args.discus)}
|
||||
print("Preparing paste…")
|
||||
paste = Paste(args.debug)
|
||||
|
||||
passphrase = b64encode(os.urandom(32))
|
||||
if args.debug: print("Passphrase:\t{}".format(passphrase))
|
||||
if args.verbose: print("Used server: {}".format(api_client.getServer()))
|
||||
|
||||
# get from server supported paste format version and update object
|
||||
if args.verbose: print("Getting supported paste format version from server…")
|
||||
version = api_client.getVersion()
|
||||
paste.setVersion(version)
|
||||
|
||||
if args.verbose: print("Filling paste with data…")
|
||||
# set compression type, works only on v2 pastes
|
||||
if version == 2:
|
||||
paste.setCompression(args.compression)
|
||||
|
||||
# add text in paste (if it provided)
|
||||
paste.setText(text)
|
||||
|
||||
# If we set PASSWORD variable
|
||||
if args.password:
|
||||
digest = hashlib.sha256(args.password.encode("UTF-8")).hexdigest()
|
||||
password = passphrase + digest.encode("UTF-8")
|
||||
else:
|
||||
password = passphrase
|
||||
|
||||
if args.debug: print("Password:\t{}".format(password))
|
||||
|
||||
# Encrypting text
|
||||
cipher = SJCL().encrypt(compress(text.encode('utf-8')), password, mode='gcm')
|
||||
|
||||
# TODO: should be implemented in upstream
|
||||
for k in ['salt', 'iv', 'ct']: cipher[k] = cipher[k].decode()
|
||||
|
||||
request['data'] = json.dumps(cipher, ensure_ascii=False).replace(' ','')
|
||||
paste.setPassword(args.password)
|
||||
|
||||
# If we set FILE variable
|
||||
if args.file:
|
||||
check_readable(args.file)
|
||||
with open(args.file, "rb") as f:
|
||||
contents = f.read()
|
||||
f.close()
|
||||
mime = guess_type(args.file)
|
||||
if args.debug: print("Filename:\t{}\nMIME-type:\t{}".format(path_leaf(args.file), mime[0]))
|
||||
paste.setAttachment(args.file)
|
||||
|
||||
file = "data:" + mime[0] + ";base64," + b64encode(contents).decode()
|
||||
filename = path_leaf(args.file)
|
||||
if args.verbose: print("Encrypting paste…")
|
||||
paste.encrypt(
|
||||
formatter = args.format,
|
||||
burnafterreading = args.burn,
|
||||
discussion = args.discus,
|
||||
expiration = args.expire)
|
||||
|
||||
cipherfile = SJCL().encrypt(compress(file.encode('utf-8')), password, mode='gcm')
|
||||
# TODO: should be implemented in upstream
|
||||
for k in ['salt', 'iv', 'ct']: cipherfile[k] = cipherfile[k].decode()
|
||||
cipherfilename = SJCL().encrypt(compress(filename.encode('utf-8')), password, mode='gcm')
|
||||
for k in ['salt', 'iv', 'ct']: cipherfilename[k] = cipherfilename[k].decode()
|
||||
if args.verbose: print("Sending request to server…")
|
||||
request = paste.getJSON()
|
||||
|
||||
request['attachment'] = json.dumps(cipherfile, ensure_ascii=False).replace(' ','')
|
||||
request['attachmentname'] = json.dumps(cipherfilename, ensure_ascii=False).replace(' ','')
|
||||
|
||||
if args.debug: print("Request:\t{}".format(request))
|
||||
if args.debug: print("Passphrase:\t{}\nRequest:\t{}".format(paste.getHash(), request))
|
||||
|
||||
# If we use dry option, exit now
|
||||
if args.dry: sys.exit(0)
|
||||
if args.dry: exit(0)
|
||||
|
||||
print("Uploading paste…")
|
||||
result = api_client.post(request)
|
||||
|
||||
if args.debug: print("Response:\t{}\n".format(result))
|
||||
|
||||
# Paste was sent. Checking for returned status code
|
||||
if not result['status']: # return code is zero
|
||||
passphrase = paste.getHash()
|
||||
|
||||
# Paste information
|
||||
print("Paste uploaded!\nPasteID:\t{}\nPassword:\t{}\nDelete token:\t{}".format(
|
||||
result['id'],
|
||||
passphrase,
|
||||
result['deletetoken']))
|
||||
|
||||
# Paste link
|
||||
print("\nLink:\t\t{}?{}#{}".format(
|
||||
settings['server'],
|
||||
result['id'],
|
||||
passphrase))
|
||||
|
||||
# Paste deletion link
|
||||
print("Delete Link:\t{}?pasteid={}&deletetoken={}".format(
|
||||
settings['server'],
|
||||
result['id'],
|
||||
result['deletetoken']))
|
||||
|
||||
# Print links to mirrors if present
|
||||
if settings['mirrors']:
|
||||
print("\nMirrors:")
|
||||
urls = settings['mirrors'].split(',')
|
||||
for x in urls:
|
||||
print("\t\t{}?{}#{}".format(
|
||||
validate_url_ending(x),
|
||||
result['id'],
|
||||
passphrase))
|
||||
|
||||
elif result['status']: # return code is other then zero
|
||||
PBinCLIError("Something went wrong…\nError:\t\t{}".format(result['message']))
|
||||
else: # or here no status field in response or it is empty
|
||||
PBinCLIError("Something went wrong…\nError: Empty response.")
|
||||
|
||||
if args.short:
|
||||
print("\nQuerying URL shortening service…")
|
||||
shortener.getlink("{}?{}#{}".format(
|
||||
settings['server'],
|
||||
result['id'],
|
||||
passphrase))
|
||||
|
||||
|
||||
def get(args, api_client, settings=None):
|
||||
from pbincli.utils import check_writable, json_encode, uri_validator
|
||||
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except ValueError as e:
|
||||
print("PBinCLI Error: {}".format(e))
|
||||
sys.exit(1)
|
||||
|
||||
if 'status' in result and not result['status']:
|
||||
print("Paste uploaded!\nPasteID:\t{}\nPassword:\t{}\nDelete token:\t{}\n\nLink:\t\t{}?{}#{}".format(result['id'], passphrase.decode(), result['deletetoken'], api_client.server, result['id'], passphrase.decode()))
|
||||
elif 'status' in result and result['status']:
|
||||
print("Something went wrong...\nError:\t\t{}".format(result['message']))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Something went wrong...\nError: Empty response.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get(args, api_client):
|
||||
pasteid, passphrase = args.pasteinfo.split("#")
|
||||
|
||||
if pasteid and passphrase:
|
||||
if args.debug: print("PasteID:\t{}\nPassphrase:\t{}".format(pasteid, passphrase))
|
||||
|
||||
if args.password:
|
||||
digest = hashlib.sha256(args.password.encode("UTF-8")).hexdigest()
|
||||
password = passphrase + digest.encode("UTF-8")
|
||||
if uri_validator(args.pasteinfo):
|
||||
api_client.server, pasteinfo = args.pasteinfo.split("?")
|
||||
pasteid, passphrase = pasteinfo.split("#")
|
||||
else:
|
||||
password = passphrase
|
||||
pasteid, passphrase = args.pasteinfo.split("#")
|
||||
except ValueError:
|
||||
PBinCLIError("Provided info hasn't contain valid PasteID#Passphrase string")
|
||||
|
||||
if args.debug: print("Password:\t{}".format(password))
|
||||
if not (pasteid and passphrase):
|
||||
PBinCLIError("Incorrect request")
|
||||
|
||||
result = api_client.get(pasteid)
|
||||
else:
|
||||
print("PBinCLI error: Incorrect request")
|
||||
sys.exit(1)
|
||||
if args.verbose: print("Used server: {}".format(api_client.getServer()))
|
||||
if args.debug: print("PasteID:\t{}\nPassphrase:\t{}".format(pasteid, passphrase))
|
||||
|
||||
paste = Paste(args.debug)
|
||||
|
||||
if args.password:
|
||||
paste.setPassword(args.password)
|
||||
if args.debug: print("Password:\t{}".format(args.password))
|
||||
|
||||
if args.verbose: print("Requesting paste from server…")
|
||||
result = api_client.get(pasteid)
|
||||
|
||||
if args.debug: print("Response:\t{}\n".format(result))
|
||||
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except ValueError as e:
|
||||
print("PBinCLI Error: {}".format(e))
|
||||
sys.exit(1)
|
||||
# Paste was received. Checking received status code
|
||||
if not result['status']: # return code is zero
|
||||
print("Paste received! Decoding…")
|
||||
|
||||
if 'status' in result and not result['status']:
|
||||
print("Paste received! Text inside:")
|
||||
data = json.loads(result['data'])
|
||||
version = result['v'] if 'v' in result else 1
|
||||
paste.setVersion(version)
|
||||
|
||||
if args.debug: print("Text:\t{}\n".format(data))
|
||||
if version == 2:
|
||||
if args.debug: print("Authentication data:\t{}".format(result['adata']))
|
||||
|
||||
text = SJCL().decrypt(data, password)
|
||||
print("{}\n".format(decompress(text.decode())))
|
||||
paste.setHash(passphrase)
|
||||
paste.loadJSON(result)
|
||||
paste.decrypt()
|
||||
|
||||
check_writable("paste.txt")
|
||||
with open("paste.txt", "wb") as f:
|
||||
f.write(decompress(text.decode()))
|
||||
f.close
|
||||
text = paste.getText()
|
||||
|
||||
if 'attachment' in result and 'attachmentname' in result:
|
||||
print("Found file, attached to paste. Decoding it and saving")
|
||||
if args.debug: print("Decoded text size: {}\n".format(len(text)))
|
||||
|
||||
cipherfile = json.loads(result['attachment'])
|
||||
cipherfilename = json.loads(result['attachmentname'])
|
||||
|
||||
if args.debug: print("Name:\t{}\nData:\t{}".format(cipherfilename, cipherfile))
|
||||
|
||||
attachmentf = SJCL().decrypt(cipherfile, password)
|
||||
attachmentname = SJCL().decrypt(cipherfilename, password)
|
||||
|
||||
attachment = decompress(attachmentf.decode('utf-8')).decode('utf-8').split(',', 1)[1]
|
||||
file = b64decode(attachment)
|
||||
filename = decompress(attachmentname.decode('utf-8')).decode('utf-8')
|
||||
|
||||
print("Filename:\t{}\n".format(filename))
|
||||
if len(text):
|
||||
if args.debug: print("{}\n".format(text.decode()))
|
||||
filename = "paste-" + pasteid + ".txt"
|
||||
print("Found text in paste. Saving it to {}".format(filename))
|
||||
|
||||
check_writable(filename)
|
||||
with open(filename, "wb") as f:
|
||||
f.write(file)
|
||||
f.close
|
||||
f.write(text)
|
||||
f.close()
|
||||
|
||||
if 'burnafterreading' in result['meta'] and result['meta']['burnafterreading']:
|
||||
print("Burn afrer reading flag found. Deleting paste...")
|
||||
result = api_client.delete(pasteid, 'burnafterreading')
|
||||
attachment, attachment_name = paste.getAttachment()
|
||||
|
||||
if args.debug: print("Delete response:\t{}\n".format(result))
|
||||
if attachment:
|
||||
print("Found file, attached to paste. Saving it to {}\n".format(attachment_name))
|
||||
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except ValueError as e:
|
||||
print("PBinCLI Error: {}".format(e))
|
||||
sys.exit(1)
|
||||
check_writable(attachment_name)
|
||||
with open(attachment_name, "wb") as f:
|
||||
f.write(attachment)
|
||||
f.close()
|
||||
|
||||
if 'status' in result and not result['status']:
|
||||
print("Paste successfully deleted!")
|
||||
elif 'status' in result and result['status']:
|
||||
print("Something went wrong...\nError:\t\t{}".format(result['message']))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Something went wrong...\nError: Empty response.")
|
||||
sys.exit(1)
|
||||
if version == 1 and 'meta' in result and 'burnafterreading' in result['meta'] and result['meta']['burnafterreading']:
|
||||
print("Burn afrer reading flag found. Deleting paste…")
|
||||
api_client.delete(json_encode({'pasteid':pasteid,'deletetoken':'burnafterreading'}))
|
||||
|
||||
elif 'status' in result and result['status']:
|
||||
print("Something went wrong...\nError:\t\t{}".format(result['message']))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Something went wrong...\nError: Empty response.")
|
||||
sys.exit(1)
|
||||
elif result['status']: # return code is other then zero
|
||||
PBinCLIError("Something went wrong…\nError:\t\t{}".format(result['message']))
|
||||
else: # or here no status field in response or it is empty
|
||||
PBinCLIError("Something went wrong…\nError: Empty response.")
|
||||
|
||||
|
||||
def delete(args, api_client):
|
||||
def delete(args, api_client, settings=None):
|
||||
from pbincli.utils import json_encode
|
||||
|
||||
pasteid = args.paste
|
||||
token = args.token
|
||||
|
||||
if args.verbose: print("Used server: {}".format(api_client.getServer()))
|
||||
if args.debug: print("PasteID:\t{}\nToken:\t\t{}".format(pasteid, token))
|
||||
|
||||
result = api_client.delete(pasteid, token)
|
||||
|
||||
if args.debug: print("Response:\t{}\n".format(result))
|
||||
|
||||
try:
|
||||
result = json.loads(result)
|
||||
except ValueError as e:
|
||||
print("PBinCLI Error: {}".format(e))
|
||||
sys.exit(1)
|
||||
|
||||
if 'status' in result and not result['status']:
|
||||
print("Paste successfully deleted!")
|
||||
elif 'status' in result and result['status']:
|
||||
print("Something went wrong...\nError:\t\t{}".format(result['message']))
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Something went wrong...\nError: Empty response.")
|
||||
sys.exit(1)
|
||||
print("Requesting paste deletion…")
|
||||
api_client.delete(json_encode({'pasteid':pasteid,'deletetoken':token}))
|
||||
|
||||
278
pbincli/api.py
278
pbincli/api.py
@@ -1,27 +1,275 @@
|
||||
import requests
|
||||
from requests import HTTPError
|
||||
from pbincli.utils import PBinCLIError
|
||||
|
||||
def _config_requests(settings=None):
|
||||
if settings['proxy']:
|
||||
proxy = {settings['proxy'].split('://')[0]: settings['proxy']}
|
||||
else:
|
||||
proxy = {}
|
||||
|
||||
if settings['no_insecure_warning']:
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
||||
|
||||
session = requests.Session()
|
||||
session.verify = not settings['no_check_certificate']
|
||||
|
||||
return session, proxy
|
||||
|
||||
|
||||
class PrivateBin:
|
||||
def __init__(self, server, proxy=None):
|
||||
self.server = server
|
||||
def __init__(self, settings=None):
|
||||
self.server = settings['server']
|
||||
self.headers = {'X-Requested-With': 'JSONHttpRequest'}
|
||||
if proxy:
|
||||
self.proxy = {proxy.split('://')[0]: proxy}
|
||||
else:
|
||||
self.proxy = {}
|
||||
|
||||
self.session, self.proxy = _config_requests(settings)
|
||||
|
||||
def post(self, request):
|
||||
r = requests.post(url = self.server, headers = self.headers, proxies = self.proxy, data = request)
|
||||
return r.text
|
||||
result = self.session.post(
|
||||
url = self.server,
|
||||
headers = self.headers,
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
|
||||
try:
|
||||
return result.json()
|
||||
except ValueError:
|
||||
PBinCLIError("Unable parse response as json. Received (size = {}):\n{}".format(len(result.text), result.text))
|
||||
|
||||
|
||||
def get(self, request):
|
||||
url = self.server + "?" + request
|
||||
r = requests.get(url = url, headers = self.headers, proxies = self.proxy)
|
||||
return r.text
|
||||
return self.session.get(
|
||||
url = self.server + "?" + request,
|
||||
headers = self.headers,
|
||||
proxies = self.proxy).json()
|
||||
|
||||
|
||||
def delete(self, pasteid, token):
|
||||
request = {'pasteid':pasteid,'deletetoken':token}
|
||||
r = requests.post(url = self.server, headers = self.headers, proxies = self.proxy, data = request)
|
||||
return r.text
|
||||
def delete(self, request):
|
||||
# using try as workaround for versions < 1.3 due to we cant detect
|
||||
# if server used version 1.2, where auto-deletion is added
|
||||
try:
|
||||
result = self.session.post(
|
||||
url = self.server,
|
||||
headers = self.headers,
|
||||
proxies = self.proxy,
|
||||
data = request).json()
|
||||
except ValueError:
|
||||
# unable parse response as json because it can be empty (1.2), so simulate correct answer
|
||||
print("NOTICE: Received empty response. We interpret that as our paste has already been deleted.")
|
||||
from json import loads as json_loads
|
||||
result = json_loads('{"status":0}')
|
||||
|
||||
if not result['status']:
|
||||
print("Paste successfully deleted!")
|
||||
elif result['status']:
|
||||
PBinCLIError("Something went wrong...\nError:\t\t{}".format(result['message']))
|
||||
else:
|
||||
PBinCLIError("Something went wrong...\nError: Empty response.")
|
||||
|
||||
|
||||
def getVersion(self):
|
||||
jsonldSchema = self.session.get(
|
||||
url = self.server + '?jsonld=paste',
|
||||
proxies = self.proxy).json()
|
||||
return jsonldSchema['@context']['v']['@value'] \
|
||||
if ('@context' in jsonldSchema and
|
||||
'v' in jsonldSchema['@context'] and
|
||||
'@value' in jsonldSchema['@context']['v']) \
|
||||
else 1
|
||||
|
||||
def getServer(self):
|
||||
return self.server
|
||||
|
||||
|
||||
class Shortener:
|
||||
"""Some parts of this class was taken from
|
||||
python-yourls (https://github.com/tflink/python-yourls/) library
|
||||
"""
|
||||
def __init__(self, settings=None):
|
||||
self.api = settings['short_api']
|
||||
|
||||
if self.api is None:
|
||||
PBinCLIError("Unable to activate link shortener without short_api.")
|
||||
|
||||
# we checking which service is used, because some services doesn't require
|
||||
# any authentication, or have only one domain on which it working
|
||||
if self.api == 'yourls':
|
||||
self._yourls_init(settings)
|
||||
elif self.api == 'isgd' or self.api == 'vgd':
|
||||
self._gd_init()
|
||||
elif self.api == 'custom':
|
||||
self.apiurl = settings['short_url']
|
||||
|
||||
self.session, self.proxy = _config_requests(settings)
|
||||
|
||||
|
||||
def _yourls_init(self, settings):
|
||||
if not settings['short_url']:
|
||||
PBinCLIError("YOURLS: An API URL is required")
|
||||
|
||||
# setting API URL
|
||||
apiurl = settings['short_url']
|
||||
if apiurl.endswith('/yourls-api.php'):
|
||||
self.apiurl = apiurl
|
||||
elif apiurl.endswith('/'):
|
||||
self.apiurl = apiurl + 'yourls-api.php'
|
||||
else:
|
||||
PBinCLIError("YOURLS: Incorrect URL is provided.\n" +
|
||||
"It must contain full address to 'yourls-api.php' script (like https://example.com/yourls-api.php)\n" +
|
||||
"or just contain instance URL with '/' at the end (like https://example.com/)")
|
||||
|
||||
# validating for required credentials
|
||||
if settings['short_user'] and settings['short_pass'] and settings['short_token'] is None:
|
||||
self.auth_args = {'username': settings['short_user'], 'password': settings['short_pass']}
|
||||
elif settings['short_user'] is None and settings['short_pass'] is None and settings['short_token']:
|
||||
self.auth_args = {'signature': settings['short_token']}
|
||||
elif settings['short_user'] is None and settings['short_pass'] is None and settings['short_token'] is None:
|
||||
self.auth_args = {}
|
||||
else:
|
||||
PBinCLIError("YOURLS: either username and password or token are required. Otherwise set to default (None)")
|
||||
|
||||
|
||||
def _gd_init(self):
|
||||
if self.api == 'isgd':
|
||||
self.apiurl = 'https://is.gd/'
|
||||
else:
|
||||
self.apiurl = 'https://v.gd/'
|
||||
self.useragent = 'Mozilla/5.0 (compatible; pbincli - https://github.com/r4sas/pbincli/)'
|
||||
|
||||
|
||||
def getlink(self, url):
|
||||
# that is api -> function mapper for running service-related function when getlink() used
|
||||
servicesList = {
|
||||
'yourls': self._yourls,
|
||||
'clckru': self._clckru,
|
||||
'tinyurl': self._tinyurl,
|
||||
'isgd': self._gd,
|
||||
'vgd': self._gd,
|
||||
'cuttly': self._cuttly,
|
||||
'custom': self._custom
|
||||
}
|
||||
# run function selected by choosen API
|
||||
servicesList[self.api](url)
|
||||
|
||||
|
||||
def _yourls(self,url):
|
||||
request = {'action': 'shorturl', 'format': 'json', 'url': url}
|
||||
request.update(self.auth_args)
|
||||
|
||||
result = self.session.post(
|
||||
url = self.apiurl,
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
|
||||
try:
|
||||
result.raise_for_status()
|
||||
except HTTPError:
|
||||
try:
|
||||
response = result.json()
|
||||
except ValueError:
|
||||
PBinCLIError("YOURLS: Unable parse response. Received (size = {}):\n{}".format(len(result.text), result.text))
|
||||
else:
|
||||
PBinCLIError("YOURLS: Received error from API: {} with JSON {}".format(result, response))
|
||||
else:
|
||||
response = result.json()
|
||||
|
||||
if {'status', 'statusCode', 'message'} <= set(response.keys()):
|
||||
if response['status'] == 'fail':
|
||||
PBinCLIError("YOURLS: Received error from API: {}".format(response['message']))
|
||||
if not 'shorturl' in response:
|
||||
PBinCLIError("YOURLS: Unknown error: {}".format(response['message']))
|
||||
else:
|
||||
print("Short Link:\t{}".format(response['shorturl']))
|
||||
else:
|
||||
PBinCLIError("YOURLS: No status, statusCode or message fields in response! Received:\n{}".format(response))
|
||||
|
||||
|
||||
def _clckru(self, url):
|
||||
request = {'url': url}
|
||||
|
||||
try:
|
||||
result = self.session.post(
|
||||
url = "https://clck.ru/--",
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
print("Short Link:\t{}".format(result.text))
|
||||
except Exception as ex:
|
||||
PBinCLIError("clck.ru: unexcepted behavior: {}".format(ex))
|
||||
|
||||
|
||||
def _tinyurl(self, url):
|
||||
request = {'url': url}
|
||||
|
||||
try:
|
||||
result = self.session.post(
|
||||
url = "https://tinyurl.com/api-create.php",
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
print("Short Link:\t{}".format(result.text))
|
||||
except Exception as ex:
|
||||
PBinCLIError("TinyURL: unexcepted behavior: {}".format(ex))
|
||||
|
||||
|
||||
def _gd(self, url):
|
||||
request = {
|
||||
'format': 'json',
|
||||
'url': url,
|
||||
'logstats': 0 # we don't want use any statistics
|
||||
}
|
||||
headers = { 'User-Agent': self.useragent}
|
||||
|
||||
try:
|
||||
result = self.session.post(
|
||||
url = self.apiurl + "create.php",
|
||||
headers = headers,
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
|
||||
response = result.json()
|
||||
|
||||
if 'shorturl' in response:
|
||||
print("Short Link:\t{}".format(response['shorturl']))
|
||||
else:
|
||||
PBinCLIError("{}: got error {} from API: {}".format(
|
||||
"is.gd" if self.api == 'isgd' else 'v.gd',
|
||||
response['errorcode'],
|
||||
response['errormessage']))
|
||||
|
||||
except Exception as ex:
|
||||
PBinCLIError("{}: unexcepted behavior: {}".format(
|
||||
"is.gd" if self.api == 'isgd' else 'v.gd',
|
||||
ex))
|
||||
|
||||
|
||||
def _cuttly(self, url):
|
||||
request = {
|
||||
'url': url,
|
||||
'domain': 0
|
||||
}
|
||||
|
||||
try:
|
||||
result = self.session.post(
|
||||
url = "https://cutt.ly/scripts/shortenUrl.php",
|
||||
proxies = self.proxy,
|
||||
data = request)
|
||||
print("Short Link:\t{}".format(result.text))
|
||||
except Exception as ex:
|
||||
PBinCLIError("cutt.ly: unexcepted behavior: {}".format(ex))
|
||||
|
||||
|
||||
def _custom(self, url):
|
||||
if self.apiurl is None:
|
||||
PBinCLIError("No short_url specified - link will not be shortened.")
|
||||
|
||||
from urllib.parse import quote
|
||||
qUrl = quote(url, safe="") # urlencoded paste url
|
||||
rUrl = self.apiurl.replace("{{url}}", qUrl)
|
||||
|
||||
try:
|
||||
result = self.session.get(
|
||||
url = rUrl,
|
||||
proxies = self.proxy)
|
||||
print("Short Link:\t{}".format(result.text))
|
||||
except Exception as ex:
|
||||
PBinCLIError("Shorter: unexcepted behavior: {}".format(ex))
|
||||
|
||||
138
pbincli/cli.py
138
pbincli/cli.py
@@ -3,66 +3,130 @@ import os, sys, argparse
|
||||
|
||||
import pbincli.actions
|
||||
from pbincli.api import PrivateBin
|
||||
from pbincli.utils import PBinCLIException
|
||||
from pbincli.utils import PBinCLIException, PBinCLIError, validate_url_ending
|
||||
|
||||
CONFIG_PATHS = [os.path.join(".", "pbincli.conf", ),
|
||||
os.path.join(os.getenv("HOME") or "~", ".config", "pbincli", "pbincli.conf") ]
|
||||
CONFIG_PATHS = [
|
||||
os.path.join(".", "pbincli.conf", ),
|
||||
os.path.join(os.getenv("HOME") or "~", ".config", "pbincli", "pbincli.conf")
|
||||
]
|
||||
|
||||
if sys.platform == "win32":
|
||||
CONFIG_PATHS.append(os.path.join(os.getenv("APPDATA"), "pbincli", "pbincli.conf"))
|
||||
elif sys.platform == "darwin":
|
||||
CONFIG_PATHS.append(os.path.join(os.getenv("HOME") or "~", "Library", "Application Support", "pbincli", "pbincli.conf"))
|
||||
|
||||
def read_config(filename):
|
||||
"""Read config variables from a file"""
|
||||
settings = {}
|
||||
with open(filename) as f:
|
||||
for l in f.readlines():
|
||||
key, value = l.strip().split("=")
|
||||
settings[key.strip()] = value.strip()
|
||||
if len(l.strip()) == 0:
|
||||
continue
|
||||
try:
|
||||
key, value = l.strip().split("=", 1)
|
||||
settings[key.strip()] = value.strip()
|
||||
except ValueError:
|
||||
PBinCLIError("Unable to parse config file, please check it for errors.")
|
||||
|
||||
return settings
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(description='Full-featured PrivateBin command-line client')
|
||||
subparsers = parser.add_subparsers(title="actions", help="List of commands")
|
||||
|
||||
# a send command
|
||||
send_parser = subparsers.add_parser("send", description="Send data to PrivateBin instance", usage="""
|
||||
%(prog)s --burn --discus --expire 1day --format plaintext \\
|
||||
--text "My file" --password mypass image.txt"""
|
||||
)
|
||||
send_parser.add_argument("-B", "--burn", default=False, action="store_true", help="burn sent paste after reading")
|
||||
send_parser.add_argument("-D", "--discus", default=False, action="store_true", help="open discussion of sent paste")
|
||||
send_parser = subparsers.add_parser("send", description="Send data to PrivateBin instance")
|
||||
send_parser.add_argument("-t", "--text", help="text in quotes. Ignored if used stdin. If not used, forcefully used stdin")
|
||||
send_parser.add_argument("-f", "--file", help="example: image.jpg or full path to file")
|
||||
send_parser.add_argument("-p", "--password", help="password for encrypting paste")
|
||||
send_parser.add_argument("-E", "--expire", default="1day", action="store",
|
||||
choices=["5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never"], help="expiration of paste (default: 1day)")
|
||||
choices=["5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never"], help="paste lifetime (default: 1day)")
|
||||
send_parser.add_argument("-B", "--burn", default=False, action="store_true", help="burn sent paste after reading")
|
||||
send_parser.add_argument("-D", "--discus", default=False, action="store_true", help="open discussion for sent paste")
|
||||
send_parser.add_argument("-F", "--format", default="plaintext", action="store",
|
||||
choices=["plaintext", "syntaxhighlighting", "markdown"], help="format of text (default: plaintext)")
|
||||
send_parser.add_argument("-t", "--text", help="comment in quotes. Ignored if used stdin")
|
||||
send_parser.add_argument("-p", "--password", help="password for encrypting paste")
|
||||
send_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
|
||||
send_parser.add_argument("-q", "--notext", default=False, action="store_true", help="don't send text in paste")
|
||||
send_parser.add_argument("-c", "--compression", default="zlib", action="store",
|
||||
choices=["zlib", "none"], help="set compression for paste (default: zlib). Note: works only on v2 paste format")
|
||||
## URL shortener
|
||||
send_parser.add_argument("-S", "--short", default=False, action="store_true", help="use URL shortener")
|
||||
send_parser.add_argument("--short-api", default=argparse.SUPPRESS, action="store",
|
||||
choices=["tinyurl", "clckru", "isgd", "vgd", "cuttly", "yourls", "custom"], help="API used by shortener service")
|
||||
send_parser.add_argument("--short-url", default=argparse.SUPPRESS, help="URL of shortener service API")
|
||||
send_parser.add_argument("--short-user", default=argparse.SUPPRESS, help="Shortener username")
|
||||
send_parser.add_argument("--short-pass", default=argparse.SUPPRESS, help="Shortener password")
|
||||
send_parser.add_argument("--short-token", default=argparse.SUPPRESS, help="Shortener token")
|
||||
## Connection options
|
||||
send_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="Instance URL (default: https://paste.i2pd.xyz/)")
|
||||
send_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
|
||||
send_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
|
||||
send_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
|
||||
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
|
||||
##
|
||||
send_parser.add_argument("-L", "--mirrors", default=argparse.SUPPRESS, help="Comma-separated list of mirrors of service with scheme (default: None)")
|
||||
send_parser.add_argument("-v", "--verbose", default=False, action="store_true", help="enable verbose output")
|
||||
send_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug output")
|
||||
send_parser.add_argument("--dry", default=False, action="store_true", help="invoke dry run")
|
||||
send_parser.add_argument("-f", "--file", help="example: image.jpg or full path to file")
|
||||
|
||||
send_parser.add_argument("stdin", help="input paste text from stdin", nargs="?", type=argparse.FileType("r"), default=sys.stdin)
|
||||
send_parser.set_defaults(func=pbincli.actions.send)
|
||||
|
||||
get_parser = subparsers.add_parser("get", description="Get data from PrivateBin instance", usage="""
|
||||
%(prog)s pasteid#password"""
|
||||
)
|
||||
get_parser.add_argument("pasteinfo", help="example: aabb#cccddd")
|
||||
get_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
|
||||
# a get command
|
||||
get_parser = subparsers.add_parser("get", description="Get data from PrivateBin instance")
|
||||
get_parser.add_argument("pasteinfo", help="\"PasteID#Passphrase\" or full URL")
|
||||
get_parser.add_argument("-p", "--password", help="password for decrypting paste")
|
||||
## Connection options
|
||||
get_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="Instance URL (default: https://paste.i2pd.xyz/, ignored if URL used in pasteinfo)")
|
||||
get_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
|
||||
get_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
|
||||
get_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
|
||||
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
|
||||
##
|
||||
get_parser.add_argument("-v", "--verbose", default=False, action="store_true", help="enable verbose output")
|
||||
get_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug output")
|
||||
get_parser.set_defaults(func=pbincli.actions.get)
|
||||
|
||||
delete_parser = subparsers.add_parser("delete", description="Delete paste from PrivateBin instance using token", usage="""
|
||||
%(prog)s --paste aabb --token aabbcc"""
|
||||
)
|
||||
# a delete command
|
||||
delete_parser = subparsers.add_parser("delete", description="Delete paste from PrivateBin instance using token")
|
||||
delete_parser.add_argument("-p", "--paste", required=True, help="paste id")
|
||||
delete_parser.add_argument("-t", "--token", required=True, help="delete token")
|
||||
delete_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug")
|
||||
delete_parser.add_argument("-t", "--token", required=True, help="paste deletion token")
|
||||
## Connection options
|
||||
delete_parser.add_argument("-s", "--server", default=argparse.SUPPRESS, help="Instance URL (default: https://paste.i2pd.xyz/)")
|
||||
delete_parser.add_argument("-x", "--proxy", default=argparse.SUPPRESS, help="Proxy server address (default: None)")
|
||||
delete_parser.add_argument("--no-check-certificate", default=False, action="store_true", help="disable certificate validation")
|
||||
delete_parser.add_argument("--no-insecure-warning", default=False, action="store_true",
|
||||
help="suppress InsecureRequestWarning (only with --no-check-certificate)")
|
||||
##
|
||||
delete_parser.add_argument("-v", "--verbose", default=False, action="store_true", help="enable verbose output")
|
||||
delete_parser.add_argument("-d", "--debug", default=False, action="store_true", help="enable debug output")
|
||||
delete_parser.set_defaults(func=pbincli.actions.delete)
|
||||
|
||||
# parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
CONFIG = {"server": "https://paste.i2pd.xyz/",
|
||||
"proxy": None}
|
||||
CONFIG = {
|
||||
'server': 'https://paste.i2pd.xyz/',
|
||||
'mirrors': None,
|
||||
'proxy': None,
|
||||
'expire': None,
|
||||
'burn': False,
|
||||
'discus': False,
|
||||
'format': None,
|
||||
'short': False,
|
||||
'short_api': None,
|
||||
'short_url': None,
|
||||
'short_user': None,
|
||||
'short_pass': None,
|
||||
'short_token': None,
|
||||
'no_check_certificate': False,
|
||||
'no_insecure_warning': False,
|
||||
'compression': None
|
||||
}
|
||||
|
||||
# Configuration preference order:
|
||||
# 1. Command line switches
|
||||
# 2. Environment variables
|
||||
# 3. Configuration file
|
||||
# 4. Default values below
|
||||
|
||||
for p in CONFIG_PATHS:
|
||||
if os.path.exists(p):
|
||||
@@ -72,15 +136,21 @@ def main():
|
||||
for key in CONFIG.keys():
|
||||
var = "PRIVATEBIN_{}".format(key.upper())
|
||||
if var in os.environ: CONFIG[key] = os.getenv(var)
|
||||
# values from command line switches are preferred
|
||||
args_var = vars(args)
|
||||
if key in args_var:
|
||||
CONFIG[key] = args_var[key]
|
||||
|
||||
api_client = PrivateBin(CONFIG["server"], proxy=CONFIG["proxy"])
|
||||
# Re-validate PrivateBin instance URL
|
||||
CONFIG['server'] = validate_url_ending(CONFIG['server'])
|
||||
|
||||
api_client = PrivateBin(CONFIG)
|
||||
|
||||
if hasattr(args, "func"):
|
||||
try:
|
||||
args.func(args, api_client)
|
||||
args.func(args, api_client, settings=CONFIG)
|
||||
except PBinCLIException as pe:
|
||||
print("PBinCLI error: {}".format(pe))
|
||||
sys.exit(1)
|
||||
raise PBinCLIException("error: {}".format(pe))
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
||||
327
pbincli/format.py
Normal file
327
pbincli/format.py
Normal file
@@ -0,0 +1,327 @@
|
||||
from base64 import b64encode, b64decode
|
||||
from pbincli.utils import PBinCLIError
|
||||
import zlib
|
||||
|
||||
# try import AES cipher and check if it has GCM mode (prevent usage of pycrypto)
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
if not hasattr(AES, 'MODE_GCM'):
|
||||
try:
|
||||
from Cryptodome.Cipher import AES
|
||||
from Cryptodome.Random import get_random_bytes
|
||||
except ImportError:
|
||||
PBinCLIError("AES GCM mode is not found in imported crypto module.\n" +
|
||||
"That can happen if you have installed pycrypto.\n\n" +
|
||||
"We tried to import pycryptodomex but it is not available.\n" +
|
||||
"Please install it via pip, if you still need pycrypto, by running:\n" +
|
||||
"\tpip install pycryptodomex\n" +
|
||||
"... otherwise use separate python environment or uninstall pycrypto:\n" +
|
||||
"\tpip uninstall pycrypto")
|
||||
else:
|
||||
from Crypto.Random import get_random_bytes
|
||||
except ImportError:
|
||||
PBinCLIError("Unable import pycryptodome")
|
||||
|
||||
|
||||
CIPHER_ITERATION_COUNT = 100000
|
||||
CIPHER_SALT_BYTES = 8
|
||||
CIPHER_BLOCK_BITS = 256
|
||||
CIPHER_TAG_BITS = 128
|
||||
|
||||
|
||||
class Paste:
|
||||
def __init__(self, debug=False):
|
||||
self._version = 2
|
||||
self._compression = 'zlib'
|
||||
self._data = ''
|
||||
self._text = ''
|
||||
self._attachment = ''
|
||||
self._attachment_name = ''
|
||||
self._password = ''
|
||||
self._debug = debug
|
||||
self._iteration_count = CIPHER_ITERATION_COUNT
|
||||
self._salt_bytes = CIPHER_SALT_BYTES
|
||||
self._block_bits = CIPHER_BLOCK_BITS
|
||||
self._tag_bits = CIPHER_TAG_BITS
|
||||
self._key = get_random_bytes(int(self._block_bits / 8))
|
||||
|
||||
|
||||
def setVersion(self, version):
|
||||
if self._debug: print("Set paste version to {}".format(version))
|
||||
self._version = version
|
||||
|
||||
|
||||
def setPassword(self, password):
|
||||
self._password = password
|
||||
|
||||
|
||||
def setText(self, text):
|
||||
self._text = text
|
||||
|
||||
|
||||
def setAttachment(self, path):
|
||||
from pbincli.utils import check_readable, path_leaf
|
||||
from mimetypes import guess_type
|
||||
|
||||
check_readable(path)
|
||||
with open(path, 'rb') as f:
|
||||
contents = f.read()
|
||||
f.close()
|
||||
mime = guess_type(path, strict=False)[0]
|
||||
|
||||
# MIME fallback
|
||||
if not mime: mime = 'application/octet-stream'
|
||||
|
||||
if self._debug: print("Filename:\t{}\nMIME-type:\t{}".format(path_leaf(path), mime))
|
||||
|
||||
self._attachment = 'data:' + mime + ';base64,' + b64encode(contents).decode()
|
||||
self._attachment_name = path_leaf(path)
|
||||
|
||||
def setCompression(self, comp):
|
||||
self._compression = comp
|
||||
|
||||
|
||||
def getText(self):
|
||||
return self._text
|
||||
|
||||
|
||||
def getAttachment(self):
|
||||
return [b64decode(self._attachment.split(',', 1)[1]), self._attachment_name] \
|
||||
if self._attachment \
|
||||
else [False,False]
|
||||
|
||||
|
||||
def getJSON(self):
|
||||
if self._version == 2:
|
||||
from pbincli.utils import json_encode
|
||||
return json_encode(self._data).decode()
|
||||
else:
|
||||
return self._data
|
||||
|
||||
|
||||
def loadJSON(self, data):
|
||||
self._data = data
|
||||
|
||||
|
||||
def getHash(self):
|
||||
if self._version == 2:
|
||||
from base58 import b58encode
|
||||
return b58encode(self._key).decode()
|
||||
else:
|
||||
return b64encode(self._key).decode()
|
||||
|
||||
|
||||
def setHash(self, passphrase):
|
||||
if self._version == 2:
|
||||
from base58 import b58decode
|
||||
self._key = b58decode(passphrase)
|
||||
else:
|
||||
self._key = b64decode(passphrase)
|
||||
|
||||
|
||||
def __deriveKey(self, salt):
|
||||
from Crypto.Protocol.KDF import PBKDF2
|
||||
from Crypto.Hash import HMAC, SHA256
|
||||
|
||||
# Key derivation, using PBKDF2 and SHA256 HMAC
|
||||
return PBKDF2(
|
||||
self._key + self._password.encode(),
|
||||
salt,
|
||||
dkLen = int(self._block_bits / 8),
|
||||
count = self._iteration_count,
|
||||
prf = lambda password, salt: HMAC.new(
|
||||
password,
|
||||
salt,
|
||||
SHA256
|
||||
).digest())
|
||||
|
||||
|
||||
@classmethod
|
||||
def __initializeCipher(self, key, iv, adata, tagsize):
|
||||
from pbincli.utils import json_encode
|
||||
|
||||
cipher = AES.new(key, AES.MODE_GCM, nonce=iv, mac_len=tagsize)
|
||||
cipher.update(json_encode(adata))
|
||||
return cipher
|
||||
|
||||
|
||||
def __preparePassKey(self):
|
||||
from hashlib import sha256
|
||||
|
||||
if self._password:
|
||||
digest = sha256(self._password.encode("UTF-8")).hexdigest()
|
||||
return b64encode(self._key) + digest.encode("UTF-8")
|
||||
else:
|
||||
return b64encode(self._key)
|
||||
|
||||
|
||||
def __decompress(self, s):
|
||||
if self._version == 2 and self._compression == 'zlib':
|
||||
# decompress data
|
||||
return zlib.decompress(s, -zlib.MAX_WBITS)
|
||||
elif self._version == 2 and self._compression == 'none':
|
||||
# nothing to do, just return original data
|
||||
return s
|
||||
elif self._version == 1:
|
||||
return zlib.decompress(bytearray(map(lambda c:ord(c)&255, b64decode(s.encode('utf-8')).decode('utf-8'))), -zlib.MAX_WBITS)
|
||||
else:
|
||||
PBinCLIError('Unknown compression type provided in paste!')
|
||||
|
||||
|
||||
def __compress(self, s):
|
||||
if self._version == 2 and self._compression == 'zlib':
|
||||
# using compressobj as compress doesn't let us specify wbits
|
||||
# needed to get the raw stream without headers
|
||||
co = zlib.compressobj(wbits=-zlib.MAX_WBITS)
|
||||
return co.compress(s) + co.flush()
|
||||
elif self._version == 2 and self._compression == 'none':
|
||||
# nothing to do, just return original data
|
||||
return s
|
||||
elif self._version == 1:
|
||||
co = zlib.compressobj(wbits=-zlib.MAX_WBITS)
|
||||
b = co.compress(s) + co.flush()
|
||||
return b64encode(''.join(map(chr, b)).encode('utf-8'))
|
||||
else:
|
||||
PBinCLIError('Unknown compression type provided!')
|
||||
|
||||
|
||||
def decrypt(self):
|
||||
# that is wrapper which running needed function regrading to paste version
|
||||
if self._version == 2: self._decryptV2()
|
||||
else: self._decryptV1()
|
||||
|
||||
|
||||
def _decryptV2(self):
|
||||
from json import loads as json_decode
|
||||
iv = b64decode(self._data['adata'][0][0])
|
||||
salt = b64decode(self._data['adata'][0][1])
|
||||
|
||||
self._iteration_count = self._data['adata'][0][2]
|
||||
self._block_bits = self._data['adata'][0][3]
|
||||
self._tag_bits = self._data['adata'][0][4]
|
||||
cipher_tag_bytes = int(self._tag_bits / 8)
|
||||
|
||||
key = self.__deriveKey(salt)
|
||||
|
||||
# Get compression type from received paste
|
||||
self._compression = self._data['adata'][0][7]
|
||||
|
||||
cipher = self.__initializeCipher(key, iv, self._data['adata'], cipher_tag_bytes)
|
||||
# Cut the cipher text into message and tag
|
||||
cipher_text_tag = b64decode(self._data['ct'])
|
||||
cipher_text = cipher_text_tag[:-cipher_tag_bytes]
|
||||
cipher_tag = cipher_text_tag[-cipher_tag_bytes:]
|
||||
cipher_message = json_decode(self.__decompress(cipher.decrypt_and_verify(cipher_text, cipher_tag)).decode())
|
||||
|
||||
self._text = cipher_message['paste'].encode()
|
||||
|
||||
if 'attachment' in cipher_message and 'attachment_name' in cipher_message:
|
||||
self._attachment = cipher_message['attachment']
|
||||
self._attachment_name = cipher_message['attachment_name']
|
||||
|
||||
|
||||
def _decryptV1(self):
|
||||
from sjcl import SJCL
|
||||
from json import loads as json_decode
|
||||
|
||||
password = self.__preparePassKey()
|
||||
cipher_text = json_decode(self._data['data'])
|
||||
if self._debug: print("Text:\t{}\n".format(cipher_text))
|
||||
|
||||
text = SJCL().decrypt(cipher_text, password)
|
||||
|
||||
if len(text):
|
||||
if self._debug: print("Decoded Text:\t{}\n".format(text))
|
||||
self._text = self.__decompress(text.decode())
|
||||
|
||||
if 'attachment' in self._data and 'attachmentname' in self._data:
|
||||
cipherfile = json_decode(self._data['attachment'])
|
||||
cipherfilename = json_decode(self._data['attachmentname'])
|
||||
|
||||
if self._debug: print("Name:\t{}\nData:\t{}".format(cipherfilename, cipherfile))
|
||||
|
||||
attachment = SJCL().decrypt(cipherfile, password)
|
||||
attachmentname = SJCL().decrypt(cipherfilename, password)
|
||||
|
||||
self._attachment = self.__decompress(attachment.decode('utf-8')).decode('utf-8')
|
||||
self._attachment_name = self.__decompress(attachmentname.decode('utf-8')).decode('utf-8')
|
||||
|
||||
|
||||
def encrypt(self, formatter, burnafterreading, discussion, expiration):
|
||||
# that is wrapper which running needed function regrading to paste version
|
||||
self._formatter = formatter
|
||||
self._burnafterreading = burnafterreading
|
||||
self._discussion = discussion
|
||||
self._expiration = expiration
|
||||
|
||||
if self._debug: print("[Enc] Starting encyptor…")
|
||||
if self._version == 2: self._encryptV2()
|
||||
else: self._encryptV1()
|
||||
|
||||
|
||||
def _encryptV2(self):
|
||||
from pbincli.utils import json_encode
|
||||
|
||||
if self._debug: print("[Enc] Preparing IV, Salt…")
|
||||
iv = get_random_bytes(int(self._tag_bits / 8))
|
||||
salt = get_random_bytes(self._salt_bytes)
|
||||
if self._debug: print("[Enc] Deriving Key…")
|
||||
key = self.__deriveKey(salt)
|
||||
|
||||
if self._debug: print("[Enc] Preparing aData and message…")
|
||||
# prepare encryption authenticated data and message
|
||||
adata = [
|
||||
[
|
||||
b64encode(iv).decode(),
|
||||
b64encode(salt).decode(),
|
||||
self._iteration_count,
|
||||
self._block_bits,
|
||||
self._tag_bits,
|
||||
'aes',
|
||||
'gcm',
|
||||
self._compression
|
||||
],
|
||||
self._formatter,
|
||||
int(self._discussion),
|
||||
int(self._burnafterreading)
|
||||
]
|
||||
cipher_message = {'paste':self._text}
|
||||
if self._attachment:
|
||||
cipher_message['attachment'] = self._attachment
|
||||
cipher_message['attachment_name'] = self._attachment_name
|
||||
|
||||
if self._debug: print("[Enc] Encrypting message…")
|
||||
cipher = self.__initializeCipher(key, iv, adata, int(self._tag_bits /8 ))
|
||||
ciphertext, tag = cipher.encrypt_and_digest(self.__compress(json_encode(cipher_message)))
|
||||
|
||||
if self._debug: print("PBKDF2 Key:\t{}\nCipherText:\t{}\nCipherTag:\t{}"
|
||||
.format(b64encode(key), b64encode(ciphertext), b64encode(tag)))
|
||||
|
||||
self._data = {'v':2,'adata':adata,'ct':b64encode(ciphertext + tag).decode(),'meta':{'expire':self._expiration}}
|
||||
|
||||
|
||||
def _encryptV1(self):
|
||||
from sjcl import SJCL
|
||||
from pbincli.utils import json_encode
|
||||
|
||||
self._data = {'expire':self._expiration,'formatter':self._formatter,
|
||||
'burnafterreading':int(self._burnafterreading),'opendiscussion':int(self._discussion)}
|
||||
|
||||
password = self.__preparePassKey()
|
||||
if self._debug: print("Password:\t{}".format(password))
|
||||
|
||||
# Encrypting text
|
||||
cipher = SJCL().encrypt(self.__compress(self._text.encode('utf-8')), password, mode='gcm')
|
||||
for k in ['salt', 'iv', 'ct']: cipher[k] = cipher[k].decode()
|
||||
|
||||
self._data['data'] = json_encode(cipher)
|
||||
|
||||
if self._attachment:
|
||||
cipherfile = SJCL().encrypt(self.__compress(self._attachment.encode('utf-8')), password, mode='gcm')
|
||||
for k in ['salt', 'iv', 'ct']: cipherfile[k] = cipherfile[k].decode()
|
||||
|
||||
cipherfilename = SJCL().encrypt(self.__compress(self._attachment_name.encode('utf-8')), password, mode='gcm')
|
||||
for k in ['salt', 'iv', 'ct']: cipherfilename[k] = cipherfilename[k].decode()
|
||||
|
||||
self._data['attachment'] = json_encode(cipherfile)
|
||||
self._data['attachmentname'] = json_encode(cipherfilename)
|
||||
@@ -1,50 +1,44 @@
|
||||
import json, os
|
||||
|
||||
import json, ntpath, os, sys
|
||||
|
||||
class PBinCLIException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def PBinCLIError(message):
|
||||
print("PBinCLI Error: {}".format(message), file=sys.stderr)
|
||||
exit(1)
|
||||
|
||||
|
||||
def path_leaf(path):
|
||||
head, tail = ntpath.split(path)
|
||||
return tail or ntpath.basename(head)
|
||||
|
||||
|
||||
def check_readable(f):
|
||||
# Checks if path exists and readable
|
||||
if not os.path.exists(f) or not os.access(f, os.R_OK):
|
||||
raise PBinCLIException("Error accessing path: {}".format(f))
|
||||
PBinCLIError("Error accessing path: {}".format(f))
|
||||
|
||||
|
||||
def check_writable(f):
|
||||
# Checks if path is writable
|
||||
if not os.access(os.path.dirname(f) or ".", os.W_OK):
|
||||
raise PBinCLIException("Path is not writable: {}".format(f))
|
||||
PBinCLIError("Path is not writable: {}".format(f))
|
||||
|
||||
|
||||
# http://stackoverflow.com/a/33571117
|
||||
def json_load_byteified(file_handle):
|
||||
return _byteify(
|
||||
json.load(file_handle, object_hook=_byteify),
|
||||
ignore_dicts=True
|
||||
)
|
||||
def json_encode(s):
|
||||
return json.dumps(s, separators=(',',':')).encode()
|
||||
|
||||
|
||||
def json_loads_byteified(json_text):
|
||||
return _byteify(
|
||||
json.loads(json_text, object_hook=_byteify),
|
||||
ignore_dicts=True
|
||||
)
|
||||
def validate_url_ending(s):
|
||||
if not s.endswith('/'):
|
||||
s = s + "/"
|
||||
return s
|
||||
|
||||
|
||||
def _byteify(data, ignore_dicts = False):
|
||||
# if this is a unicode string, return its string representation
|
||||
if isinstance(data, str):
|
||||
return data.encode('utf-8')
|
||||
# if this is a list of values, return list of byteified values
|
||||
if isinstance(data, list):
|
||||
return [ _byteify(item, ignore_dicts=True) for item in data ]
|
||||
# if this is a dictionary, return dictionary of byteified keys and values
|
||||
# but only if we haven't already byteified it
|
||||
if isinstance(data, dict) and not ignore_dicts:
|
||||
return {
|
||||
_byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True)
|
||||
for key, value in data.iteritems()
|
||||
}
|
||||
# if it's anything else, return it in its original form
|
||||
return data
|
||||
def uri_validator(x):
|
||||
from urllib.parse import urlparse
|
||||
try:
|
||||
result = urlparse(x)
|
||||
return all([result.scheme, result.netloc])
|
||||
except ValueError:
|
||||
return False
|
||||
@@ -1,3 +1,4 @@
|
||||
pycryptodome
|
||||
sjcl
|
||||
base58
|
||||
requests
|
||||
|
||||
28
setup.py
28
setup.py
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from setuptools import setup
|
||||
from pbincli.__init__ import __version__ as pbincli_version
|
||||
|
||||
with open("README.md") as readme:
|
||||
with open("README.rst") as readme:
|
||||
long_description = readme.read()
|
||||
|
||||
with open("requirements.txt") as f:
|
||||
@@ -10,25 +11,34 @@ with open("requirements.txt") as f:
|
||||
|
||||
setup(
|
||||
name='PBinCLI',
|
||||
version='0.1',
|
||||
version=pbincli_version,
|
||||
description='PrivateBin client for command line',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/x-rst',
|
||||
author='R4SAS',
|
||||
author_email='r4sas@i2pmail.org',
|
||||
url='https://github.com/r4sas/PBinCLI',
|
||||
keywords='privatebin',
|
||||
license='DWTFYWWI',
|
||||
url='https://github.com/r4sas/PBinCLI/',
|
||||
keywords='privatebin cryptography security',
|
||||
license='MIT',
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'License :: Other/Proprietary License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Topic :: Security :: Cryptography',
|
||||
'Topic :: Utilities',
|
||||
],
|
||||
packages=['pbincli'],
|
||||
install_requires=install_requires,
|
||||
python_requires='>=3',
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pbincli=pbincli.cli:main',
|
||||
],
|
||||
}
|
||||
},
|
||||
project_urls={
|
||||
'Bug Reports': 'https://github.com/r4sas/PBinCLI/issues',
|
||||
'Source': 'https://github.com/r4sas/PBinCLI/',
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user