diff --git a/README.md b/README.md index 0a4d7d4..471699d 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,18 @@ A Pastebin for Tor ## What is this? -TorPaste is a simple Pastebin service written in Python using the Flask framework. -It is targeted to users inside Tor and can be easily setup as a Hidden Service. -As of version v0.4 TorPaste supports multiple backends for storage of data, however -currently only the local filesystem backend is implemented. TorPaste has been designed -in order to need no cookies or JavaScript and to run without problems in the Tor Browser -with the Security and Privacy Settings set to Maximum. +TorPaste is a simple Pastebin service written in Python using the Flask +framework. It is targeted to users inside Tor and can be easily setup as a +Hidden Service. As of version v0.4 TorPaste supports multiple backends for +storage of data, however currently only the local filesystem and Redis backends +are implemented. TorPaste has been designed in order to need no cookies or +JavaScript and to run without problems in the Tor Browser with the Security and +Privacy Settings set to Maximum. Unfortunately, the lack of client-side code means all the pastes are stored in -**plaintext** format, readable by anyone, including the server. For this reason, -all pastes are indexed and available publicly by default to anyone to see as well. -Do not use this service for sensitive data. +**plaintext** format, readable by anyone, including the server. For this +reason, all pastes are indexed and available publicly by default to anyone to +see as well. Do not use this service for sensitive data. ## How to run this? @@ -58,9 +59,10 @@ docker run -d -p 80:80 daknob/torpaste If you want to run TorPaste in production ( [don't worry, you're not alone](https://paste.daknob.net) ), consider using a specific tag such as `daknob/torpaste:v0.3`. The `latest` tag is synchronized -automatically with the `master` branch of this repo and therefore is the bleeding -edge version. In both cases, don't forget to update your version of TorPaste for -bug fixes, security patches, and new features. This can be done by running: +automatically with the `master` branch of this repo and therefore is the +bleeding edge version. In both cases, don't forget to update your version of +TorPaste for bug fixes, security patches, and new features. This can be done by +running: ```bash docker pull daknob/torpaste @@ -73,69 +75,88 @@ or of course, for a specific version: docker pull daknob/torpaste:v0.3 ``` -and then stop the previous container and start a new one. It is important to use -the same settings when launching a new container, so any `-p` / `-e` / `-v` arguments -need to be specified again. +and then stop the previous container and start a new one. It is important to +use the same settings when launching a new container, so any `-p` / `-e` / `-v` +arguments need to be specified again. -If you're using Docker and you need the pastes to persist, you can mount the paste -directory to the local filesystem. This will store all pastes in the host and not -inside the container. This can be done as such: +If you're using Docker and you need the pastes to persist, you can mount the +paste directory to the local filesystem. This will store all pastes in the host +and not inside the container. This can be done as such: ```bash docker run -d -p 80:80 -v /path/to/host/:/torpaste/pastes daknob/torpaste ``` ## Backends -TorPaste is extensible and supports multiple backends for storage of its data. As -of now, the only one implemented is the `filesystem` backend, which stores all data -as files in the local filesystem. If you're interested in writting a backend, please -see [Issue #15](https://github.com/DaKnOb/TorPaste/issues/15) for some ideas. For -more information in the development of new backends, there's an `example.py` file -inside the `backends` folder which you can copy and start editing right away. The -file includes a lot of useful information and design documentation for your new -backend, but if you still want to look at an example, the `backends/filesystem.py` -is there as well to have a look. +TorPaste is extensible and supports multiple backends for storage of its data. +As of now, the only ones implemented are the `filesystem` and `redis` backends. +The first stores all data as files in the local filesystem while the second +stores everything in a Redis instance. If you're interested in writting a +backend, please see [Issue #15](https://github.com/DaKnOb/TorPaste/issues/15) +for some ideas. For more information in the development of new backends, +there's an `example.py` file inside the `backends` folder which you can copy +and start editing right away. The file includes a lot of useful information and +design documentation for your new backend, but if you still want to look at an +example, the `backends/filesystem.py` is there as well to have a look. ### filesystem -This is the first backend available for TorPaste and stores everything in the local -filesystem. TorPaste versions prior to v0.4 had this backend hardcoded and therefore -this is an improved implementation so we can maintain backwards compatibility without -running any migration scripts. It is also the simplest backend and it is used by -default. +This is the first backend available for TorPaste and stores everything in the +local filesystem. TorPaste versions prior to v0.4 had this backend hardcoded +and therefore this is an improved implementation so we can maintain backwards +compatibility without running any migration scripts. It is also the simplest +backend and it is used by default. + +### redis +This is the second backend ever to be added to TorPaste. This backend requires +a Redis instance to save data to. It uses a single Redis database for all data +storage and is much faster than the `filesystem` backend according to some +initial benchmarks. In addition to that, it can be set up in a HA environment +by using Redis' HA features. Please note that Redis may store data only into +memory so if you want your data to persist accross reboots / crashes you have +to enable persistence according to the Redis documentation. ## Configuration -TorPaste can be configured by using `ENV`ironment Variables. The list of available -variables as well as their actions is below so you can use them to parameterize your -installation of the software. Please note that all these variables have a default -value which may not work well for you, but makes them all optional. +TorPaste can be configured by using `ENV`ironment Variables. The list of +available variables as well as their actions is below so you can use them to +parameterize your installation of the software. Please note that all these +variables have a default value which may not work well for you, but makes them +all optional. ### Available ENV Variables -* `TP_WEBSITE_TITLE` : Use this variable to set the TorPaste Title inside the HTML -`` tags. *Default:* `Tor Paste` +* `TP_WEBSITE_TITLE` : Use this variable to set the TorPaste Title inside the +HTML `` tags. *Default:* `Tor Paste` * `TP_BACKEND` : Use this variable to select a backend for TorPaste to use. The -available backends for each version are included in the `COMPATIBLE_BACKENDS` variable -inside `torpaste.py`. *Default:* `filesystem` -* `TP_PASTE_MAX_SIZE` : Use this variable to set the maximum paste size, in bytes. The -possible values are formatted as ` `, for example `10 M`, or `128 B`, -or `16 k`. Any value that starts with `0` changes this limit to unlimited. *Default:* -`0` -* `TP_PASTE_LIST_ACTIVE` : Use this variable to enable or disable the paste listing -available in the `Pastes` menu. *Default:* `True` -* `TP_CSP_REPORT_URI` : Use this variable to set a `report-uri` for the Content Security -Policy of TorPaste. If this variable is not set, no `report-uri` is added, which is the -default behavior. -* `TP_ENABLED_PASTE_VISIBILITIES` : Use this variable to select the available paste -visibilities, separated by a comma. Example: "public,unlisted". The available backends -for each version are included in the `AVAILABLE_VISIBILITIES` variable inside -`torpaste.py`. *Default:* `public`. +available backends for each version are included in the `COMPATIBLE_BACKENDS` +variable inside `torpaste.py`. *Default:* `filesystem` +* `TP_PASTE_MAX_SIZE` : Use this variable to set the maximum paste size, in +bytes. The possible values are formatted as ` `, for example +`10 M`, or `128 B`, or `16 k`. Any value that starts with `0` changes this +limit to unlimited. *Default:* `0` +* `TP_PASTE_LIST_ACTIVE` : Use this variable to enable or disable the paste +listing available in the `Pastes` menu. *Default:* `True` +* `TP_CSP_REPORT_URI` : Use this variable to set a `report-uri` for the Content +Security Policy of TorPaste. If this variable is not set, no `report-uri` is +added, which is the default behavior. +* `TP_ENABLED_PASTE_VISIBILITIES` : Use this variable to select the available +paste visibilities, separated by a comma. Example: "public,unlisted". The +available backends for each version are included in the +`AVAILABLE_VISIBILITIES` variable inside `torpaste.py`. *Default:* `public`. ### Backend ENV Variables -Each backend may need one or more additional `ENV` variables to work. For example, -a MySQL backend may need the `HOST`, `PORT`, `USERNAME`, and `PASSWORD` to connect -to the database. To prevent conflicts, all these variables will be available as -`TP_BACKEND_BACKENDNAME_VARIABLE` where `BACKENDNAME` is the name of the backend, -such as `MYSQL` and the `VARIABLE` will be the name of the variable, such as `HOST`. - -Currently there are no used backend `ENV` variables. When there are, you will find -a list of all backends and their variables here. +Each backend may need one or more additional `ENV` variables to work. For +example, a MySQL backend may need the `HOST`, `PORT`, `USERNAME`, and +`PASSWORD` to connect to the database. To prevent conflicts, all these +variables will be available as `TP_BACKEND_BACKENDNAME_VARIABLE` where +`BACKENDNAME` is the name of the backend, such as `MYSQL` and the `VARIABLE` +will be the name of the variable, such as `HOST`. + +#### redis +* `TP_BACKEND_REDIS_HOST` : The Redis server running to connect to. *Default:* +`127.0.0.1` +* `TP_BACKEND_REDIS_PORT` : The Redis server port to connect to. *Default:* +`6379` +* `TP_BACKEND_REDIS_PASSWORD` : The Redis server password needed for +authentication. *Default: none* +* `TP_BACKEND_REDIS_DB_INDEX` : The database index to connect to such as `0` or +`1`. *Default:* `1`. diff --git a/backends/redis.py b/backends/redis.py new file mode 100644 index 0000000..4f32a54 --- /dev/null +++ b/backends/redis.py @@ -0,0 +1,183 @@ +#!../bin/python + +import backends.exceptions as e +import redis +from os import getenv + + +def initialize_backend(): + """ + This method is called when the Flask application starts. + Here you can do any initialization that may be required, + such as connecting to a remote database or making sure a file exists. + """ + global rb + + RedisHost = getenv("TP_BACKEND_REDIS_HOST") or "127.0.0.1" + RedisPort = int(getenv("TP_BACKEND_REDIS_PORT") or "6379") + RedisPass = getenv("TP_BACKEND_REDIS_PASSWORD") or None + RedisDBI = int(getenv("TP_BACKEND_REDIS_DB_INDEX") or "1") + + rb = redis.StrictRedis( + host=RedisHost, + port=RedisPort, + password=RedisPass, + db=RedisDBI + ) + + if rb.ping() is not True: + raise e.ErrorException( + "Could not connect to the backend database. Please try again" + + "later. If the error persists, please contact a system" + + "administrator." + ) + + rb.save() + + return + + +def new_paste(paste_id, paste_content): + """ + This method is called when the Flask application wants to create a new + paste. The arguments given are the Paste ID, which is a paste identifier + that is not necessarily unique, as well as the paste content. Please note + that the paste must be retrievable given the above Paste ID as well as + he fact that paste_id is (typically) an ASCII string while paste_content + can (and will) contain UTF-8 characters. + :param paste_id: a not necessarily unique id of the paste + :param paste_content: content of the paste (utf-8 encoded) + :return: + """ + + rb.set(paste_id, paste_content) + + rb.save() + + return + + +def update_paste_metadata(paste_id, metadata): + """ + This method is called by the Flask application to update a paste's + metadata. For this to happen, the application passes the Paste ID, + which is typically an ASCII string, as well as a Python dictionary + that contains the new metadata. This method must overwrite any and + all metadata with the passed dictionary. For example, if a paste has + the keys a and b and this method is called with only keys b and c, + the final metadata must be b and c only, and not a. + :param paste_id: ASCII coded id of the paste + :param metadata: dictionary containing the metadata + :return: + """ + + for md in rb.keys(paste_id + ".*"): + rb.delete(md) + for md in metadata: + rb.set(paste_id + "." + md, metadata[md]) + + rb.save() + + return + + +def does_paste_exist(paste_id): + """ + This method is called when the Flask application wants to check if a + paste with a given Paste ID exists. The Paste ID is (typically) an + ASCII string and your method must return True if a paste with this ID + exists, or False if it doesn't. + :param paste_id: ASCII string which represents the ID of the paste + :return: True if paste with given ID exists, false otherwise + """ + + return rb.exists(paste_id) + + +def get_paste_contents(paste_id): + """ + This method must return all the paste contents in UTF-8 encoding for + a given Paste ID. The Paste ID is typically in ASCII, and it is + guaranteed that this Paste ID exists. + :param paste_id: ASCII string which represents the ID of the paste + :return: the content of the paste in UTF-8 encoding + """ + + return rb.get(paste_id).decode("utf-8") + + +def get_paste_metadata(paste_id): + """ + This method must return a Python Dictionary with all the currently + stored metadata for the paste with the given Paste ID. All keys of + the dictionary are typically in ASCII, while all values are in + UTF-8. It is guaranteed that the Paste ID exists. + :param paste_id: ASCII string which represents the ID of the paste + :return: a dictionary with the metadata of a given paste + """ + + ret = {} + + f = rb.keys(paste_id + ".*") + for md in f: + ret[md.split(".")[1]] = rb.get(md).decode("utf-8") + + return str(ret) + + +def get_paste_metadata_value(paste_id, key): + """ + This method must return the value of the metadata key provided for + the paste whose Paste ID is provided. If the key is not set, the + method should return None. You can assume that a paste with this + Paste ID exists, and you can also assume that both parameters + passed are typically ASCII. + :param paste_id: ASCII string which represents the ID of the paste + :param key: key of the metadata + :return: value of the metadata key provided for the given ID, None if + the key wasn't set + """ + + if rb.exists(paste_id + "." + key) is not True: + return None + else: + return rb.get(paste_id + "." + key).decode("utf-8") + + +def get_all_paste_ids(filters={}, fdefaults={}): + """ + This method must return a Python list containing the ASCII ID of all + pastes which match the (optional) filters provided. The order does not + matter so it can be the same or it can be different every time this + function is called. In the case of no pastes, the method must return a + Python list with a single item, whose content must be equal to 'none'. + :param filters: a dictionary of filters + :param fdefaults: a dictionary with the default value for each filter + if it's not present + :return: a list containing all paste IDs + """ + + ak = rb.keys() + ap = [] + for k in ak: + if b"." not in k: + ap.append(k.decode("utf-8")) + filt = [] + for p in ap: + keep = True + + for k, v in filters.items(): + gpmv = get_paste_metadata_value(p, k) + if gpmv is None: + gpmv = fdefaults[k] + if gpmv != v: + keep = False + break + + if keep: + filt.append(p) + + if len(filt) == 0: + filt = ['none'] + + return filt diff --git a/requirements.txt b/requirements.txt index e3e9a71..1fb0bd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Flask +redis diff --git a/torpaste.py b/torpaste.py index 3b54153..ef40e37 100755 --- a/torpaste.py +++ b/torpaste.py @@ -17,7 +17,7 @@ VERSION = check_output(["git", "describe"]).decode("utf-8").replace("\n", "") # Compatible Backends List -COMPATIBLE_BACKENDS = ["filesystem"] +COMPATIBLE_BACKENDS = ["filesystem", "redis"] # Available list of paste visibilities # public: can be viewed by all, is listed in /list