Compare commits

..

No commits in common. "master" and "v1.6.0" have entirely different histories.

30 changed files with 118 additions and 1128 deletions

6
.bumpversion.cfg Normal file
View file

@ -0,0 +1,6 @@
[bumpversion]
files = setup.py
commit = True
tag = True
current_version = 1.5.0

View file

@ -6,6 +6,7 @@
CONTRIBUTING.rst CONTRIBUTING.rst
docker-compose.yml docker-compose.yml
Dockerfile Dockerfile
requirements.txt
tests.py tests.py
tox.ini tox.ini

View file

@ -1,8 +0,0 @@
name: "CodeQL config"
paths-ignore:
- tests.py
- 'snappass/static/bootstrap/**'
- 'snappass/static/clipboardjs/**'
- 'snappass/static/fontawesome/**'
- 'snappass/static/jquery/**'

View file

@ -9,6 +9,3 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-patch"]

View file

@ -13,15 +13,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
python-version: ['3.8', '3.9', '3.10'] python-version: ['3.7', '3.8', '3.9', '3.10']
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5 uses: actions/setup-python@v3
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- uses: actions/cache@v4 - uses: actions/cache@v3
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: ${{ runner.os }}-python-${{ matrix.python-version }}-pip-${{ hashFiles('.github/workflows/ci.yml') }} key: ${{ runner.os }}-python-${{ matrix.python-version }}-pip-${{ hashFiles('.github/workflows/ci.yml') }}

View file

@ -1,45 +0,0 @@
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
# Skip the workflow if PR only contains changes to files matching the following path patterns
paths-ignore:
- tests.py
- '**/*.md'
- '**/*.rst'
- 'snappass/static/bootstrap/**'
- 'snappass/static/clipboardjs/**'
- 'snappass/static/fontawesome/**'
- 'snappass/static/jquery/**'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'python' ]
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
config-file: ./.github/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3

5
.gitignore vendored
View file

@ -1,4 +1,3 @@
.env
.project .project
*.rdb *.rdb
junit*xml junit*xml
@ -51,7 +50,3 @@ htmlcov/
# virtualenv # virtualenv
venv/ venv/
ENV/ ENV/
# Translation catalogs
*.mo
*.pot

View file

@ -8,13 +8,9 @@ RUN groupadd -r snappass && \
WORKDIR $APP_DIR WORKDIR $APP_DIR
COPY ["setup.py", "requirements.txt", "MANIFEST.in", "README.rst", "AUTHORS.rst", "$APP_DIR/"] COPY ["setup.py", "MANIFEST.in", "README.rst", "AUTHORS.rst", "$APP_DIR/"]
COPY ["./snappass", "$APP_DIR/snappass"] COPY ["./snappass", "$APP_DIR/snappass"]
RUN pip install -r requirements.txt
RUN pybabel compile -d snappass/translations
RUN python setup.py install && \ RUN python setup.py install && \
chown -R snappass $APP_DIR && \ chown -R snappass $APP_DIR && \
chgrp -R snappass $APP_DIR chgrp -R snappass $APP_DIR

View file

@ -1,4 +1,3 @@
include *.rst LICENSE include *.rst LICENSE
recursive-include snappass/static * recursive-include snappass/static *
recursive-include snappass/templates * recursive-include snappass/templates *
recursive-include snappass/translations *

View file

@ -47,7 +47,7 @@ Requirements
------------ ------------
* `Redis`_ * `Redis`_
* Python 3.8+ * Python 2.7+ or 3.5+
.. _Redis: https://redis.io/ .. _Redis: https://redis.io/
@ -96,186 +96,6 @@ need to change this.
``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com`` ``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com``
``SNAPPASS_BIND_ADDRESS``: (optional) Used to override the default bind address of 0.0.0.0 for flask app Example: ``127.0.0.1``
``SNAPPASS_PORT``: (optional) Used to override the default port of 5000 Example: ``6000``
APIs
----
SnapPass has 2 APIs :
1. A simple API : That can be used to create passwords links, and then share them with users
2. A more REST-y API : Which facilitate programmatic interactions with SnapPass, without having to parse HTML content when retrieving the password
Simple API
^^^^^^^^^^
The advantage of using the simple API is that you can create a password and retrieve the link without having to open the web interface. This is useful if you want to embed it in a script or use it in a CI/CD pipeline.
To create a password, send a POST request to ``/api/set_password`` like so:
::
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/set_password/
This will return a JSON response with the password link:
::
{
"link": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
"ttl":1209600
}
the default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:
::
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/set_password/
REST API
^^^^^^^^
The advantage of using the REST API is that you can fully manage the lifecycle of the password stored in SnapPass without having to interact with any web user interface.
This is useful if you want to embed it in a script, use it in a CI/CD pipeline or share it between multiple client applications.
Create a password
"""""""""""""""""
To create a password, send a POST request to ``/api/v2/passwords`` like so:
::
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/v2/passwords
This will return a JSON response with a token and the password link:
::
{
"token": "snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY=",
"links": [{
"rel": "self",
"href": "http://127.0.0.1:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
},{
"rel": "web-view",
"href": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
}],
"ttl":1209600
}
The default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:
::
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/v2/passwords
If the password is null or empty, and the TTL is larger than the max TTL of the application, the API will return an error like this:
Otherwise, the API will return a 404 (Not Found) response like so:
::
{
"invalid-params": [{
"name": "password",
"reason": "The password is required and should not be null or empty."
}, {
"name": "ttl",
"reason": "The specified TTL is longer than the maximum supported."
}],
"title": "The password and/or the TTL are invalid.",
"type": "https://127.0.0.1:5000/set-password-validation-error"
}
Check if a password exists
""""""""""""""""""""""""""
To check if a password exists, send a HEAD request to ``/api/v2/passwords/<token>``, where ``<token>`` is the token of the API response when a password is created (url encoded), or simply use the `self` link:
::
$ curl --head http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
If :
- the passwork_key is valid
- the password :
- exists,
- has not been read
- is not expired
Then the API will return a 200 (OK) response like so:
::
HTTP/1.1 200 OK
Server: Werkzeug/3.0.1 Python/3.12.2
Date: Fri, 29 Mar 2024 22:15:54 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: close
Otherwise, the API will return a 404 (Not Found) response like so:
::
HTTP/1.1 404 NOT FOUND
Server: Werkzeug/3.0.1 Python/3.12.2
Date: Fri, 29 Mar 2024 22:19:29 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 0
Connection: close
Read a password
"""""""""""""""
To read a password, send a GET request to ``/api/v2/passwords/<password_key>``, where ``<password_key>`` is the token of the API response when a password is created, or simply use the `self` link:
::
$ curl -X GET http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
If :
- the token is valid
- the password :
- exists
- has not been read
- is not expired
Then the API will return a 200 (OK) with a JSON response containing the password :
::
{
"password": "foobar"
}
Otherwise, the API will return a 404 (Not Found) response like so:
::
{
"invalid-params": [{
"name": "token"
}],
"title": "The password doesn't exist.",
"type": "https://127.0.0.1:5000/get-password-error"
}
Notes on APIs
^^^^^^^^^^^^^
Notes:
- When using the APIs, you can specify any ttl, as long as it is lower than the default.
- The password is passed in the body of the request rather than in the URL. This is to prevent the password from being logged in the server logs.
- Depending on the environment you are running it, you might want to expose the ``/api`` endpoint to your internal network only, and put the web interface behind authentication.
Docker Docker
------ ------

View file

@ -1,10 +0,0 @@
# Update Translations:
# (venv) $ pybabel extract -F babel.cfg -o messages.pot .
# (venv) $ pybabel update -i messages.pot -d snappass/translations
# (venv) $ pybabel compile -d snappass/translations
# Add a new language:
# (venv) $ pybabel extract -F babel.cfg -o messages.pot .
# (venv) $ pybabel init -i messages.pot -d snappass/translations -l <language_code>
[python: snappass/**.py]
[jinja2: snappass/templates/**.html]

View file

@ -1,9 +1,9 @@
coverage==7.6.0 coverage==4.5.1
fakeredis==2.24.1 fakeredis==0.7.0
flake8==7.1.1 flake8==3.5.0
freezegun==1.5.1 freezegun==0.3.15
pytest==8.3.2 mock==2.0.0
pytest-cov==5.0.0 nose==1.3.7
tox==4.18.0 pytest-cov==2.5.1
bumpversion==0.6.0 pytest==3.6.3
wheel==0.44.0 tox==3.1.2

View file

@ -4,42 +4,15 @@ services:
snappass: snappass:
build: . build: .
#image: pinterest/snappass image: pinterest/snappass
#ports: ports:
# - "5000:5000" - "5000:5000"
stop_signal: SIGINT stop_signal: SIGINT
environment: environment:
- REDIS_HOST=redis - REDIS_HOST=redis
- NO_SSL=false - NO_SSL=True
- SECRET_KEY=${SECRET_KEY}
- VIRTUAL_HOST=share.brothertec.eu
- VIRTUAL_PORT=5000
- LETSENCRYPT_HOST=share.brothertec.eu
- LETSENCRYPT_EMAIL=admin@brothertec.eu
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=false"
networks:
- default
- proxy
- edge-tier
depends_on: depends_on:
- redis - redis
redis: redis:
image: "redis:latest" image: "redis:latest"
restart: unless-stopped
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
proxy:
name: nginx-proxy
external: true
edge-tier:
name: edge
external: true

View file

@ -1,8 +1,13 @@
cryptography==43.0.1 asn1crypto==1.3.0
Flask==3.0.0 cffi==1.14.0
itsdangerous==2.2.0 click==7.1.2
Jinja2==3.1.4 cryptography==3.3.2
MarkupSafe==2.1.1 Flask==1.0.2
redis==5.0.1 idna==2.9
Werkzeug==3.0.3 itsdangerous==0.24
flask-babel Jinja2==2.11.3
MarkupSafe==1.1.1
pycparser==2.20
redis==2.10.6
six==1.14.0
Werkzeug==0.15.6

View file

@ -1,9 +1,10 @@
[bumpversion] [bumpversion]
current_version = 1.6.2 current_version = 1.6.0
commit = True commit = True
tag = True tag = True
files = setup.py files = setup.py snappass/__init__.py
[flake8] [flake8]
show-source = True show-source = True
max-line-length = 120 max-line-length = 120

View file

@ -2,7 +2,7 @@ from setuptools import setup
setup( setup(
name='snappass', name='snappass',
version='1.6.2', version='1.6.0',
description="It's like SnapChat... for Passwords.", description="It's like SnapChat... for Passwords.",
long_description=(open('README.rst').read() + '\n\n' + long_description=(open('README.rst').read() + '\n\n' +
open('AUTHORS.rst').read()), open('AUTHORS.rst').read()),
@ -18,7 +18,7 @@ setup(
], ],
}, },
include_package_data=True, include_package_data=True,
python_requires='>=3.8, <4', python_requires='>=3.7, <4',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
@ -27,6 +27,7 @@ setup(
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',

View file

@ -5,20 +5,18 @@ import uuid
import redis import redis
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from flask import abort, Flask, render_template, request, jsonify, make_response from flask import abort, Flask, render_template, request, jsonify
from redis.exceptions import ConnectionError from redis.exceptions import ConnectionError
from urllib.parse import quote_plus from werkzeug.urls import url_quote_plus
from urllib.parse import unquote_plus from werkzeug.urls import url_unquote_plus
from urllib.parse import urljoin
from distutils.util import strtobool from distutils.util import strtobool
# _ is required to get the Jinja templates translated
from flask_babel import Babel, _ # noqa: F401
NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False'))) NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False')))
URL_PREFIX = os.environ.get('URL_PREFIX', None) URL_PREFIX = os.environ.get('URL_PREFIX', None)
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None) HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
TOKEN_SEPARATOR = '~' TOKEN_SEPARATOR = '~'
# Initialize Flask Application # Initialize Flask Application
app = Flask(__name__) app = Flask(__name__)
if os.environ.get('DEBUG'): if os.environ.get('DEBUG'):
@ -27,18 +25,9 @@ app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key')
app.config.update( app.config.update(
dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) dict(STATIC_URL=os.environ.get('STATIC_URL', 'static')))
# Set up Babel
def get_locale():
return request.accept_languages.best_match(['en', 'es', 'de', 'nl'])
babel = Babel(app, locale_selector=get_locale)
# Initialize Redis # Initialize Redis
if os.environ.get('MOCK_REDIS'): if os.environ.get('MOCK_REDIS'):
from fakeredis import FakeStrictRedis from fakeredis import FakeStrictRedis
redis_client = FakeStrictRedis() redis_client = FakeStrictRedis()
elif os.environ.get('REDIS_URL'): elif os.environ.get('REDIS_URL'):
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL')) redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
@ -50,10 +39,7 @@ else:
host=redis_host, port=redis_port, db=redis_db) host=redis_host, port=redis_port, db=redis_db)
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass') REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400, TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400, 'hour': 3600}
'hour': 3600}
DEFAULT_API_TTL = 1209600
MAX_TTL = DEFAULT_API_TTL
def check_redis_alive(fn): def check_redis_alive(fn):
@ -68,7 +54,6 @@ def check_redis_alive(fn):
sys.exit(0) sys.exit(0)
else: else:
return abort(500) return abort(500)
return inner return inner
@ -104,37 +89,6 @@ def parse_token(token):
return storage_key, decryption_key return storage_key, decryption_key
def as_validation_problem(request, problem_type, problem_title, invalid_params):
base_url = set_base_url(request)
problem = {
"type": base_url + problem_type,
"title": problem_title,
"invalid-params": invalid_params
}
return as_problem_response(problem)
def as_not_found_problem(request, problem_type, problem_title, invalid_params):
base_url = set_base_url(request)
problem = {
"type": base_url + problem_type,
"title": problem_title,
"invalid-params": invalid_params
}
return as_problem_response(problem, 404)
def as_problem_response(problem, status_code=None):
if not isinstance(status_code, int) or not status_code:
status_code = 400
response = make_response(jsonify(problem), status_code)
response.headers['Content-Type'] = 'application/problem+json'
return response
@check_redis_alive @check_redis_alive
def set_password(password, ttl): def set_password(password, ttl):
""" """
@ -200,22 +154,6 @@ def clean_input():
return TIME_CONVERSION[time_period], request.form['password'] return TIME_CONVERSION[time_period], request.form['password']
def set_base_url(req):
if NO_SSL:
if HOST_OVERRIDE:
base_url = f'http://{HOST_OVERRIDE}/'
else:
base_url = req.url_root
else:
if HOST_OVERRIDE:
base_url = f'https://{HOST_OVERRIDE}/'
else:
base_url = req.url_root.replace("http://", "https://")
if URL_PREFIX:
base_url = base_url + URL_PREFIX.strip("/") + "/"
return base_url
@app.route('/', methods=['GET']) @app.route('/', methods=['GET'])
def index(): def index():
return render_template('set_password.html') return render_template('set_password.html')
@ -223,113 +161,31 @@ def index():
@app.route('/', methods=['POST']) @app.route('/', methods=['POST'])
def handle_password(): def handle_password():
password = request.form.get('password') ttl, password = clean_input()
ttl = request.form.get('ttl')
if clean_input():
ttl = TIME_CONVERSION[ttl.lower()]
token = set_password(password, ttl) token = set_password(password, ttl)
base_url = set_base_url(request)
link = base_url + quote_plus(token) if NO_SSL:
if request.accept_mimetypes.accept_json and not \ if HOST_OVERRIDE:
request.accept_mimetypes.accept_html: base_url = f'http://{HOST_OVERRIDE}/'
else:
base_url = request.url_root
else:
if HOST_OVERRIDE:
base_url = f'https://{HOST_OVERRIDE}/'
else:
base_url = request.url_root.replace("http://", "https://")
if URL_PREFIX:
base_url = base_url + URL_PREFIX.strip("/") + "/"
link = base_url + url_quote_plus(token)
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
return jsonify(link=link, ttl=ttl) return jsonify(link=link, ttl=ttl)
else: else:
return render_template('confirm.html', password_link=link) return render_template('confirm.html', password_link=link)
else:
abort(500)
@app.route('/api/set_password/', methods=['POST'])
def api_handle_password():
password = request.json.get('password')
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
if password and isinstance(ttl, int) and ttl <= MAX_TTL:
token = set_password(password, ttl)
base_url = set_base_url(request)
link = base_url + quote_plus(token)
return jsonify(link=link, ttl=ttl)
else:
abort(500)
@app.route('/api/v2/passwords', methods=['POST'])
def api_v2_set_password():
password = request.json.get('password')
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
invalid_params = []
if not password:
invalid_params.append({
"name": "password",
"reason": "The password is required and should not be null or empty."
})
if not isinstance(ttl, int) or ttl > MAX_TTL:
invalid_params.append({
"name": "ttl",
"reason": "The specified TTL is longer than the maximum supported."
})
if len(invalid_params) > 0:
# Return a ProblemDetails expliciting issue with Password and/or TTL
return as_validation_problem(
request,
"set-password-validation-error",
"The password and/or the TTL are invalid.",
invalid_params
)
token = set_password(password, ttl)
url_token = quote_plus(token)
base_url = set_base_url(request)
api_link = urljoin(base_url, request.path + "/" + url_token)
web_link = urljoin(base_url, url_token)
response_content = {
"token": token,
"links": [{
"rel": "self",
"href": api_link
}, {
"rel": "web-view",
"href": web_link
}],
"ttl": ttl
}
return jsonify(response_content)
@app.route('/api/v2/passwords/<token>', methods=['HEAD'])
def api_v2_check_password(token):
token = unquote_plus(token)
if not password_exists(token):
# Return NotFound, to indicate that password does not exists (anymore or at all)
return ('', 404)
else:
# Return OK, to indicate that password still exists
return ('', 200)
@app.route('/api/v2/passwords/<token>', methods=['GET'])
def api_v2_retrieve_password(token):
token = unquote_plus(token)
password = get_password(token)
if not password:
# Return NotFound, to indicate that password does not exists (anymore or at all)
return as_not_found_problem(
request,
"get-password-error",
"The password doesn't exist.",
[{"name": "token"}]
)
else:
# Return OK and the password in JSON message
return jsonify(password=password)
@app.route('/<password_key>', methods=['GET']) @app.route('/<password_key>', methods=['GET'])
def preview_password(password_key): def preview_password(password_key):
password_key = unquote_plus(password_key) password_key = url_unquote_plus(password_key)
if not password_exists(password_key): if not password_exists(password_key):
return render_template('expired.html'), 404 return render_template('expired.html'), 404
@ -338,7 +194,7 @@ def preview_password(password_key):
@app.route('/<password_key>', methods=['POST']) @app.route('/<password_key>', methods=['POST'])
def show_password(password_key): def show_password(password_key):
password_key = unquote_plus(password_key) password_key = url_unquote_plus(password_key)
password = get_password(password_key) password = get_password(password_key)
if not password: if not password:
return render_template('expired.html'), 404 return render_template('expired.html'), 404
@ -346,16 +202,9 @@ def show_password(password_key):
return render_template('password.html', password=password) return render_template('password.html', password=password)
@app.route('/_/_/health', methods=['GET'])
@check_redis_alive
def health_check():
return {}
@check_redis_alive @check_redis_alive
def main(): def main():
app.run(host=os.environ.get('SNAPPASS_BIND_ADDRESS', '0.0.0.0'), app.run(host='0.0.0.0')
port=os.environ.get('SNAPPASS_PORT', 5000))
if __name__ == '__main__': if __name__ == '__main__':

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ _('en') }}"> <html lang="en">
<head> <head>
<title>{{ _('Snappass - Share Secrets') }}</title> <title>Snappass - Share Secrets</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -13,22 +13,14 @@
<nav class="navbar navbar-default navbar-static-top"> <nav class="navbar navbar-default navbar-static-top">
<div class="container"> <div class="container">
<div class="navbar-header"> <div class="navbar-header">
<a class="navbar-brand" href="/">{{ _('Share Secret') }}</a> <a class="navbar-brand" href="/">Share Secret</a>
</div> </div>
</div> </div>
</nav> </nav>
{% block content %}{% endblock %} {% block content %}{% endblock %}
<script src="{{ config.STATIC_URL }}/jquery/jquery-3.6.0.min.js"></script> <script src="{{ config.STATIC_URL }}/jquery/jquery-1.12.4.min.js"></script>
<script src="{{ config.STATIC_URL }}/bootstrap/js/bootstrap.min.js"></script> <script src="{{ config.STATIC_URL }}/bootstrap/js/bootstrap.min.js"></script>
{% block js %}{% endblock %} {% block js %}{% endblock %}
<div class="container">
<p>
<p><strong>no tracking | no logging | no advertising</strong></p>
<p>proudly presented by <a href="https://brothertec.eu/" target="_blank">brothertec.eu</a> | <a href="https://datenschutz.brothertec.eu/impressum/" target="_blank">Impressum</a> | <a href="https://datenschutz.brothertec.eu/datenschutzerkl%C3%A4rung/" target="_blank">Datenschutzhinweis</a> | <a href="https://github.com/pinterest/snappass" target="_blank">Code</a></p>
</p>
</div>
</body> </body>
</html> </html>

View file

@ -3,15 +3,15 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<section> <section>
<div class="page-header"><h1>{{ _('Share Secret Link') }}</h1></div> <div class="page-header"><h1>Share Secret Link</h1></div>
<p>{{ _('The secret has been temporarily saved. Send the following URL to your intended recipient.') }}</p> <p>The secret has been temporarily saved. Send the following URL to your intended recipient.</p>
<div class="row"> <div class="row">
<div class="col-sm-6 margin-bottom-10"> <div class="col-sm-6 margin-bottom-10">
<input type="text" class="form-control" id="password-link" value="{{ password_link }}" readonly="readonly"> <input type="text" class="form-control" id="password-link" value="{{ password_link }}" readonly="readonly">
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<button title="{{ _('Copy to clipboard') }}" type="button" class="btn btn-primary copy-clipboard-btn" <button title="Copy to clipboard" type="button" class="btn btn-primary copy-clipboard-btn"
id="copy-clipboard-btn" data-clipboard-target="#password-link" id="copy-clipboard-btn" data-clipboard-target="#password-link"
data-placement='bottom'> data-placement='bottom'>
<i class="fa fa-clipboard"></i> <i class="fa fa-clipboard"></i>

View file

@ -3,9 +3,9 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<section> <section>
<div class="page-header"><h1>{{ _('Secret not found') }}</h1></div> <div class="page-header"><h1>Secret not found</h1></div>
<p class="lead">{{ _('The requested URL was not found on the server. This could be because this URL never contained a secret, or because it expired or was revealed earlier.') }}</p> <p class="lead">The requested URL was not found on the server. This could be because this URL never contained a secret, or because it expired or was revealed earlier.</p>
<p class="lead">{{ _('If this URL was sent to you by someone, make sure to check your spelling or ask the person who sent it to you to send a new secret.') }}</p> <p class="lead">If this URL was sent to you by someone, make sure to check your spelling or ask the person who sent it to you to send a new secret.</p>
</section> </section>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -3,22 +3,22 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<section> <section>
<div class="page-header"><h1>{{ _('Secret') }}</h1></div> <div class="page-header"><h1>Secret</h1></div>
<p>{{ _('Save the following secret to a secure location.') }}</p> <p>Save the following secret to a secure location.</p>
<div class="row"> <div class="row">
<div class="col-sm-6 margin-bottom-10"> <div class="col-sm-6 margin-bottom-10">
<textarea class="form-control" rows="10" cols="50" id="password-text" name="password-text" readonly="readonly">{{ password }}</textarea> <textarea class="form-control" rows="10" cols="50" id="password-text" name="password-text" readonly="readonly">{{ password }}</textarea>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<button title="{{ _('Copy to clipboard') }}" type="button" class="btn btn-primary copy-clipboard-btn" <button title="Copy to clipboard" type="button" class="btn btn-primary copy-clipboard-btn"
id="copy-clipboard-btn" data-clipboard-target="#password-text" id="copy-clipboard-btn" data-clipboard-target="#password-text"
data-placement='bottom'> data-placement='bottom'>
<i class="fa fa-clipboard"></i> <i class="fa fa-clipboard"></i>
</button> </button>
</div> </div>
</div> </div>
<p>{{ _('The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.') }}</p> <p>The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.</p>
</section> </section>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -4,12 +4,12 @@
<div class="container"> <div class="container">
<section> <section>
<div class="page-header"> <div class="page-header">
<h1>{{ _('Secret') }}</h1> <h1>Secret</h1>
</div> </div>
<p class="lead">{{ _('You can only reveal the secret once!') }}</p> <p class="lead">You can only reveal the secret once!</p>
<div class="row"> <div class="row">
<div class="col-sm-6 margin-bottom-10"> <div class="col-sm-6 margin-bottom-10">
<button id="revealSecret" type="button" class="btn-lg btn-primary">{{ _('Reveal secret') }}</button> <button id="revealSecret" type="button" class="btn-lg btn-primary">Reveal secret</button>
</div> </div>
</div> </div>
</section> </section>

View file

@ -3,27 +3,27 @@
{% block content %} {% block content %}
<div class="container"> <div class="container">
<section> <section>
<div class="page-header"><h1>{{ _('Set Secret') }}</h1></div> <div class="page-header"><h1>Set Secret</h1></div>
<div class="row"> <div class="row">
<form role="form" id="password_create" method="post" autocomplete="off"> <form role="form" id="password_create" method="post" autocomplete="off">
<div class="col-sm-6 margin-bottom-10"> <div class="col-sm-6 margin-bottom-10">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span> <span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span>
<textarea rows="10" cols="50" id="password" name="password" autofocus="true" class="form-control" placeholder="{{ _('SnapPass allows you to share secrets in a secure, ephemeral way. Input a single or multi-line secret, its expiration time, and click Generate URL. Share the one-time use URL with your intended recipient.') }}" aria-describedby="basic-addon1" autocomplete="off" required></textarea> <textarea rows="10" cols="50" id="password" name="password" autofocus="true" class="form-control" placeholder="SnapPass allows you to share secrets in a secure, ephemeral way. Input a single or multi-line secret, its expiration time, and click Generate URL. Share the one-time use URL with your intended recipient." aria-describedby="basic-addon1" autocomplete="off" required></textarea>
</div> </div>
</div> </div>
<div class="col-sm-2 margin-bottom-10"> <div class="col-sm-2 margin-bottom-10">
<select class="form-control" name="ttl"> <select class="form-control" name="ttl">
<option value="Two Weeks">{{ _('Two Weeks') }}</option> <option value="Two Weeks">Two Weeks</option>
<option value="Week" selected="selected">{{ _('Week') }}</option> <option value="Week" selected="selected">Week</option>
<option value="Day">{{ _('Day') }}</option> <option value="Day">Day</option>
<option value="Hour">{{ _('Hour') }}</option> <option value="Hour">Hour</option>
</select> </select>
</div> </div>
<div class="col-sm-4"> <div class="col-sm-4">
<button type="submit" class="btn btn-primary" id="submit">{{ _('Generate URL') }}</button> <button type="submit" class="btn btn-primary" id="submit">Generate URL</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -1,131 +0,0 @@
# German translations for SNAPPASS.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# systeembeheerder <systeembeheerder@users.noreply.github.com>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-22 11:01+0100\n"
"PO-Revision-Date: 2024-02-16 09:29+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: de\n"
"Language-Team: de <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: snappass/templates/base.html:2
msgid "en"
msgstr "de"
#: snappass/templates/base.html:4
msgid "Snappass - Share Secrets"
msgstr "Snappass - Passwort teilen"
#: snappass/templates/base.html:16
msgid "Share Secret"
msgstr "Passwort teilen"
#: snappass/templates/confirm.html:6
msgid "Share Secret Link"
msgstr "Geheimen Link teilen"
#: snappass/templates/confirm.html:7
msgid ""
"The secret has been temporarily saved. Send the following URL to your "
"intended recipient."
msgstr ""
"Das Geheimnis wurde vorübergehend gespeichert. Senden Sie die folgende "
"URL an Ihre gewünschten Empfänger."
#: snappass/templates/confirm.html:14 snappass/templates/password.html:14
msgid "Copy to clipboard"
msgstr "In Zwischenablage kopieren"
#: snappass/templates/expired.html:6
msgid "Secret not found"
msgstr "Passwort nicht gefunden"
#: snappass/templates/expired.html:7
msgid ""
"The requested URL was not found on the server. This could be because this"
" URL never contained a secret, or because it expired or was revealed "
"earlier."
msgstr ""
"Die angeforderte URL wurde auf dem Server nicht gefunden. Dies könnte "
"daran liegen, dass diesDie URL enthielt nie ein Passwort, oder weil sie "
"abgelaufen ist oder offengelegt wurde "
#: snappass/templates/expired.html:8
msgid ""
"If this URL was sent to you by someone, make sure to check your spelling "
"or ask the person who sent it to you to send a new secret."
msgstr ""
"Wenn Ihnen diese URL von jemandem gesendet wurde, überprüfen Sie "
"unbedingt Ihre Rechtschreibung oder bitten Sie die Person, die es Ihnen "
"geschickt hat, ein neues Passwort zu senden."
#: snappass/templates/password.html:6 snappass/templates/preview.html:7
msgid "Secret"
msgstr "Geheim"
#: snappass/templates/password.html:7
msgid "Save the following secret to a secure location."
msgstr "Speichern Sie dass folgende Passwort an einem sicheren Ort."
#: snappass/templates/password.html:21
msgid ""
"The secret has now been permanently deleted from the system, and the URL "
"will no longer work. Refresh this page to verify."
msgstr ""
" Dass Passwort wurde nun endgültig aus dem System gelöscht, und die URL "
"funktioniert nicht mehr. Aktualisieren Sie diese Seite, um dies zu "
"überprüfen."
#: snappass/templates/preview.html:9
msgid "You can only reveal the secret once!"
msgstr "Du kannst das Passwort nur einmal lüften!"
#: snappass/templates/preview.html:12
msgid "Reveal secret"
msgstr "Passwort lüften"
#: snappass/templates/set_password.html:6
msgid "Set Secret"
msgstr "Geheimen Schlüssel festlegen"
#: snappass/templates/set_password.html:12
msgid ""
"SnapPass allows you to share secrets in a secure, ephemeral way. Input a "
"single or multi-line secret, its expiration time, and click Generate URL."
" Share the one-time use URL with your intended recipient."
msgstr ""
"SnapPass ermöglicht es Ihnen, Passwörter auf sichere, kurzlebige Weise zu"
" teilen. Input a ein- oder mehrzeiliges Passwort, die Ablaufzeit und "
"klicken Sie auf URL generieren.Teilen Sie die URL für den einmaligen "
"Gebrauch mit dem beabsichtigten Empfänger."
#: snappass/templates/set_password.html:18
msgid "Two Weeks"
msgstr "Zwei Wochen"
#: snappass/templates/set_password.html:19
msgid "Week"
msgstr "Woche"
#: snappass/templates/set_password.html:20
msgid "Day"
msgstr "Tag"
#: snappass/templates/set_password.html:21
msgid "Hour"
msgstr "Stunde"
#: snappass/templates/set_password.html:26
msgid "Generate URL"
msgstr "URL generieren"

View file

@ -1,129 +0,0 @@
# Spanish translations for SNAPPASS.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# systeembeheerder <systeembeheerder@users.noreply.github.com>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-22 11:01+0100\n"
"PO-Revision-Date: 2024-02-16 09:29+0100\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es\n"
"Language-Team: es <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: snappass/templates/base.html:2
msgid "en"
msgstr "es"
#: snappass/templates/base.html:4
msgid "Snappass - Share Secrets"
msgstr "Snappass - Compartir secretos"
#: snappass/templates/base.html:16
msgid "Share Secret"
msgstr "Compartir secretos"
#: snappass/templates/confirm.html:6
msgid "Share Secret Link"
msgstr "Compartir enlace secreto"
#: snappass/templates/confirm.html:7
msgid ""
"The secret has been temporarily saved. Send the following URL to your "
"intended recipient."
msgstr ""
"El secreto se ha guardado temporalmente. Envíe la siguiente URL a "
"sudestinatario previsto."
#: snappass/templates/confirm.html:14 snappass/templates/password.html:14
msgid "Copy to clipboard"
msgstr "Copiar en el portapapeles"
#: snappass/templates/expired.html:6
msgid "Secret not found"
msgstr "Secreto no encontrado"
#: snappass/templates/expired.html:7
msgid ""
"The requested URL was not found on the server. This could be because this"
" URL never contained a secret, or because it expired or was revealed "
"earlier."
msgstr ""
"La URL solicitada no se encontró en el servidor. Esto podría deberse a "
"estoLa URL nunca contenía un secreto, o porque caducó o fue revelado "
"Antes."
#: snappass/templates/expired.html:8
msgid ""
"If this URL was sent to you by someone, make sure to check your spelling "
"or ask the person who sent it to you to send a new secret."
msgstr ""
"Si alguien te envió esta URL, asegúrate de revisar tu ortografíaO pídele "
"a la persona que te lo envió que te envíe un nuevo secreto."
#: snappass/templates/password.html:6 snappass/templates/preview.html:7
msgid "Secret"
msgstr "Secreto"
#: snappass/templates/password.html:7
msgid "Save the following secret to a secure location."
msgstr "Guarda el siguiente secreto en un lugar seguro."
#: snappass/templates/password.html:21
msgid ""
"The secret has now been permanently deleted from the system, and the URL "
"will no longer work. Refresh this page to verify."
msgstr ""
"El secreto ahora se ha eliminado permanentemente del sistema, y la URL Ya"
" no funcionará. Actualiza esta página para verificarlo."
#: snappass/templates/preview.html:9
msgid "You can only reveal the secret once!"
msgstr "¡Solo puedes revelar el secreto una vez!"
#: snappass/templates/preview.html:12
msgid "Reveal secret"
msgstr "Revelar secreto"
#: snappass/templates/set_password.html:6
msgid "Set Secret"
msgstr "Establecer secreto"
#: snappass/templates/set_password.html:12
msgid ""
"SnapPass allows you to share secrets in a secure, ephemeral way. Input a "
"single or multi-line secret, its expiration time, and click Generate URL."
" Share the one-time use URL with your intended recipient."
msgstr ""
"SnapPass te permite compartir secretos de forma segura y efímera. "
"Introduzca un secreto de una o varias líneas, su tiempo de caducidad y "
"haga clic en Generar URL.Comparta la URL de un solo uso con el "
"destinatario previsto\""
#: snappass/templates/set_password.html:18
msgid "Two Weeks"
msgstr "Dos semanas"
#: snappass/templates/set_password.html:19
msgid "Week"
msgstr "Semana"
#: snappass/templates/set_password.html:20
msgid "Day"
msgstr "Día"
#: snappass/templates/set_password.html:21
msgid "Hour"
msgstr "Hora"
#: snappass/templates/set_password.html:26
msgid "Generate URL"
msgstr "Generar URL"

View file

@ -1,128 +0,0 @@
# Dutch translations for SNAPPASS.
# Copyright (C) 2024 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# systeembeheerder <systeembeheerder@users.noreply.github.com>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2024-02-22 11:01+0100\n"
"PO-Revision-Date: 2024-02-14 21:16+0100\n"
"Last-Translator: \n"
"Language: nl\n"
"Language-Team: nl <LL@li.org>\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.14.0\n"
#: snappass/templates/base.html:2
msgid "en"
msgstr "nl"
#: snappass/templates/base.html:4
msgid "Snappass - Share Secrets"
msgstr "Snappass - Deel Wachtwoorden"
#: snappass/templates/base.html:16
msgid "Share Secret"
msgstr "Stel wachtwoord in"
#: snappass/templates/confirm.html:6
msgid "Share Secret Link"
msgstr "Deel wachtwoord link"
#: snappass/templates/confirm.html:7
msgid ""
"The secret has been temporarily saved. Send the following URL to your "
"intended recipient."
msgstr ""
"Het wachtwoord is tijdelijk opgeslagen. Deel de volgende URL aan de "
"bedoelde ontvanger."
#: snappass/templates/confirm.html:14 snappass/templates/password.html:14
msgid "Copy to clipboard"
msgstr "Kopieer naar het klembord"
#: snappass/templates/expired.html:6
msgid "Secret not found"
msgstr "Wachtwoord niet gevonden"
#: snappass/templates/expired.html:7
msgid ""
"The requested URL was not found on the server. This could be because this"
" URL never contained a secret, or because it expired or was revealed "
"earlier."
msgstr ""
"De gevraagde URL is niet gevonden op de server. Dat kan omdat deze geen "
"wachtwoord bevat, het is verlopen of het al eerder getoond is."
#: snappass/templates/expired.html:8
msgid ""
"If this URL was sent to you by someone, make sure to check your spelling "
"or ask the person who sent it to you to send a new secret."
msgstr ""
"Als deze URL naar u is toegestuurd, controleer de spelling of vraag de "
"verzender om een nieuw wachtwoord link te versturen."
#: snappass/templates/password.html:6 snappass/templates/preview.html:7
msgid "Secret"
msgstr "Wachtwoord"
#: snappass/templates/password.html:7
msgid "Save the following secret to a secure location."
msgstr "Bewaar het wachtwoord op een veilige plek."
#: snappass/templates/password.html:21
msgid ""
"The secret has now been permanently deleted from the system, and the URL "
"will no longer work. Refresh this page to verify."
msgstr ""
"Het wachtwoord is permanent verwijderd van het systeem, de URL werkt niet"
" meer. Herlaad deze pagina ter verificatie"
#: snappass/templates/preview.html:9
msgid "You can only reveal the secret once!"
msgstr "Het wachtwoord wordt slechts eenmaal getoond!"
#: snappass/templates/preview.html:12
msgid "Reveal secret"
msgstr "Onthul wachtwoord"
#: snappass/templates/set_password.html:6
msgid "Set Secret"
msgstr "Stel wachtwoord in"
#: snappass/templates/set_password.html:12
msgid ""
"SnapPass allows you to share secrets in a secure, ephemeral way. Input a "
"single or multi-line secret, its expiration time, and click Generate URL."
" Share the one-time use URL with your intended recipient."
msgstr ""
"We stellen je in staat om wachtwoorden op een veilige, tijdelijke manier "
"te delen. Voer een enkel- of meerregelig wachwoord in, stel de vervaltijd"
" in, en klik op 'URL genereren'. Deel de eenmalig te gebruiken URL met de"
" beoogde ontvanger."
#: snappass/templates/set_password.html:18
msgid "Two Weeks"
msgstr "Twee weken"
#: snappass/templates/set_password.html:19
msgid "Week"
msgstr "Week"
#: snappass/templates/set_password.html:20
msgid "Day"
msgstr "Dag"
#: snappass/templates/set_password.html:21
msgid "Hour"
msgstr "Uur"
#: snappass/templates/set_password.html:26
msgid "Generate URL"
msgstr "URL genereren"

207
tests.py
View file

@ -1,16 +1,16 @@
from mock import patch
import re import re
import time import time
import unittest import unittest
import uuid import uuid
import json
from unittest import TestCase from unittest import TestCase
from unittest import mock
from urllib.parse import quote
from urllib.parse import unquote
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
from freezegun import freeze_time from freezegun import freeze_time
from werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from fakeredis import FakeStrictRedis from fakeredis import FakeStrictRedis
from six.moves.urllib.parse import unquote
# noinspection PyPep8Naming # noinspection PyPep8Naming
import snappass.main as snappass import snappass.main as snappass
@ -20,7 +20,7 @@ __author__ = 'davedash'
class SnapPassTestCase(TestCase): class SnapPassTestCase(TestCase):
@mock.patch('redis.client.StrictRedis', FakeStrictRedis) @patch('redis.client.StrictRedis', FakeStrictRedis)
def test_get_password(self): def test_get_password(self):
password = "melatonin overdose 1337!$" password = "melatonin overdose 1337!$"
key = snappass.set_password(password, 30) key = snappass.set_password(password, 30)
@ -107,11 +107,6 @@ class SnapPassRoutesTestCase(TestCase):
snappass.app.config['TESTING'] = True snappass.app.config['TESTING'] = True
self.app = snappass.app.test_client() self.app = snappass.app.test_client()
def test_health_check(self):
response = self.app.get('/_/_/health')
self.assertEqual('200 OK', response.status)
self.assertEqual('{}', response.get_data(as_text=True).strip())
def test_preview_password(self): def test_preview_password(self):
password = "I like novelty kitten statues!" password = "I like novelty kitten statues!"
key = snappass.set_password(password, 30) key = snappass.set_password(password, 30)
@ -148,11 +143,7 @@ class SnapPassRoutesTestCase(TestCase):
def test_set_password_json(self): def test_set_password_json(self):
with freeze_time("2020-05-08 12:00:00") as frozen_time: with freeze_time("2020-05-08 12:00:00") as frozen_time:
password = 'my name is my passport. verify me.' password = 'my name is my passport. verify me.'
rv = self.app.post( rv = self.app.post('/', headers={'Accept': 'application/json'}, data={'password': password, 'ttl': 'two weeks'})
'/',
headers={'Accept': 'application/json'},
data={'password': password, 'ttl': 'two weeks'},
)
json_content = rv.get_json() json_content = rv.get_json()
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1) key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
@ -164,194 +155,6 @@ class SnapPassRoutesTestCase(TestCase):
frozen_time.move_to("2020-05-22 12:00:00") frozen_time.move_to("2020-05-22 12:00:00")
self.assertIsNone(snappass.get_password(key)) self.assertIsNone(snappass.get_password(key))
def test_set_password_api(self):
with freeze_time("2020-05-08 12:00:00") as frozen_time:
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/set_password/',
headers={'Accept': 'application/json'},
json={'password': password, 'ttl': '1209600'},
)
json_content = rv.get_json()
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
key = unquote(key)
frozen_time.move_to("2020-05-22 11:59:59")
self.assertEqual(snappass.get_password(key), password)
frozen_time.move_to("2020-05-22 12:00:00")
self.assertIsNone(snappass.get_password(key))
def test_set_password_api_default_ttl(self):
with freeze_time("2020-05-08 12:00:00") as frozen_time:
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/set_password/',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
key = unquote(key)
frozen_time.move_to("2020-05-22 11:59:59")
self.assertEqual(snappass.get_password(key), password)
frozen_time.move_to("2020-05-22 12:00:00")
self.assertIsNone(snappass.get_password(key))
def test_set_password_api_v2(self):
with freeze_time("2020-05-08 12:00:00") as frozen_time:
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password, 'ttl': '1209600'},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
frozen_time.move_to("2020-05-22 11:59:59")
self.assertEqual(snappass.get_password(key), password)
frozen_time.move_to("2020-05-22 12:00:00")
self.assertIsNone(snappass.get_password(key))
def test_set_password_api_v2_default_ttl(self):
with freeze_time("2020-05-08 12:00:00") as frozen_time:
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
frozen_time.move_to("2020-05-22 11:59:59")
self.assertEqual(snappass.get_password(key), password)
frozen_time.move_to("2020-05-22 12:00:00")
self.assertIsNone(snappass.get_password(key))
def test_set_password_api_v2_no_password(self):
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': ''},
)
self.assertEqual(rv.status_code, 400)
json_content = rv.get_json()
invalid_params = json_content['invalid-params']
self.assertEqual(len(invalid_params), 1)
bad_password = invalid_params[0]
self.assertEqual(bad_password['name'], 'password')
def test_set_password_api_v2_too_big_ttl(self):
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password, 'ttl': '1209600000'},
)
self.assertEqual(rv.status_code, 400)
json_content = rv.get_json()
invalid_params = json_content['invalid-params']
self.assertEqual(len(invalid_params), 1)
bad_ttl = invalid_params[0]
self.assertEqual(bad_ttl['name'], 'ttl')
def test_set_password_api_v2_no_password_and_too_big_ttl(self):
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': '', 'ttl': '1209600000'},
)
self.assertEqual(rv.status_code, 400)
json_content = rv.get_json()
invalid_params = json_content['invalid-params']
self.assertEqual(len(invalid_params), 2)
bad_password = invalid_params[0]
self.assertEqual(bad_password['name'], 'password')
bad_ttl = invalid_params[1]
self.assertEqual(bad_ttl['name'], 'ttl')
def test_check_password_api_v2(self):
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
rvc = self.app.head('/api/v2/passwords/' + quote(key))
self.assertEqual(rvc.status_code, 200)
def test_check_password_api_v2_bad_keys(self):
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
rvc = self.app.head('/api/v2/passwords/' + quote(key[::-1]))
self.assertEqual(rvc.status_code, 404)
def test_retrieve_password_api_v2(self):
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
rvc = self.app.get('/api/v2/passwords/' + quote(key))
self.assertEqual(rv.status_code, 200)
json_content_retrieved = rvc.get_json()
retrieved_password = json_content_retrieved['password']
self.assertEqual(retrieved_password, password)
def test_retrieve_password_api_v2_bad_keys(self):
password = 'my name is my passport. verify me.'
rv = self.app.post(
'/api/v2/passwords',
headers={'Accept': 'application/json'},
json={'password': password},
)
json_content = rv.get_json()
key = unquote(json_content['token'])
rvc = self.app.get('/api/v2/passwords/' + quote(key[::-1]))
self.assertEqual(rvc.status_code, 404)
json_content_retrieved = rvc.get_json()
invalid_params = json_content_retrieved['invalid-params']
self.assertEqual(len(invalid_params), 1)
bad_token = invalid_params[0]
self.assertEqual(bad_token['name'], 'token')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py38, py39, py310, flake8 envlist = py37, py38, py39, py310, flake8
[testenv] [testenv]
setenv = setenv =
@ -7,9 +7,9 @@ setenv =
commands = commands =
pip install -r requirements.txt pip install -r requirements.txt
pip install -r dev-requirements.txt pip install -r dev-requirements.txt
pytest --cov=snappass --cov-report=term-missing tests.py pytest --junitxml=junit-{envname}.xml --cov-report xml tests.py
[testenv:flake8] [testenv:flake8]
commands = commands =
pip install -r dev-requirements.txt pip install flake8
flake8 flake8 snappass/