Compare commits

..

1 commit

Author SHA1 Message Date
Nicholas Charriere
afe3558640 Bump version: 1.1.0 → 1.1.1 2017-05-16 10:10:35 -07:00
47 changed files with 189 additions and 4140 deletions

5
.bumpversion.cfg Normal file
View file

@ -0,0 +1,5 @@
[bumpversion]
files = setup.py
commit = True
tag = True
current_version = 0.1.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

@ -1,14 +0,0 @@
---
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-patch"]

View file

@ -1,37 +0,0 @@
name: CI
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-python-${{ matrix.python-version }}-pip-${{ hashFiles('.github/workflows/ci.yml') }}
restore-keys: ${{ runner.os }}-python-${{ matrix.python-version }}-pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install tox tox-gh-actions
- name: Lint
if: matrix.python-version == '3.10'
run: tox -e flake8
- name: Tests
run: tox

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

6
.gitignore vendored
View file

@ -1,4 +1,3 @@
.env
.project .project
*.rdb *.rdb
junit*xml junit*xml
@ -46,12 +45,7 @@ htmlcov/
.coverage .coverage
.coverage.* .coverage.*
.cache .cache
.pytest_cache/
# virtualenv # virtualenv
venv/ venv/
ENV/ ENV/
# Translation catalogs
*.mo
*.pot

15
.travis.yml Normal file
View file

@ -0,0 +1,15 @@
language: python
python:
- "2.6"
- "2.7"
- "3.3"
- "3.4"
- "3.5"
- "3.6"
sudo: false
install:
- pip install tox-travis
script:
- tox
services:
- redis-server

View file

@ -1,9 +0,0 @@
# Adopters
This is an alphabetical list of people and organizations who are using this
project. If you'd like to be included here, please send a Pull Request that
adds your information to this file.
- [Pinterest](https://www.pinterest.com/)
- [Ookla/Speedtest.net](https://www.ookla.com/)
- [VSHN](https://www.vshn.ch/)

View file

@ -19,4 +19,3 @@ Thanks a lot for the contributions of:
* Donny Winston * Donny Winston
* James Barclay * James Barclay
* Thomas Decaux * Thomas Decaux
* Lauri Lubi

View file

@ -1,49 +0,0 @@
Version 1.6.0
-------------
* Drop support for officially unsupported Python versions (< Python 3.7)
Version 1.5.1
-------------
* The ``HOST_OVERRIDE`` environment variable can be used to override the base URL. Useful when behind a reverseproxy.
* Upgrade to Jinja 2.11.3
* Upgrade to cryptography 3.3.2
* Returning json if request-mimetype is "application/json"
* Return template if password is expired (instead of 404)
Version 1.5.0
-------------
* Added support for "2 week" secret lifetimes.
* The ``NO_SSL`` environment variable is now propertly parsed.
* The ``URL_PREFIX`` environment variable can be used to add a prefix to URLs,
which is useful when running behind a reverse proxy like nginx.
* Prevent prefetching bots from destroying secrets.
* Replaced mockredis with fakeredis in the unit test environment.
* Added support for Python 3.8.
Version 1.4.2
-------------
* Various minor README and documentation improvements
* Upgrade to Jinja 2.10.1
* Fix autocomplete bug where hitting "back" would allow to autocomplete the password
Version 1.4.1
-------------
* Switch to local (non-CDN) Font Awesome assets
* Upgraded cryptography to 2.3.1 (for CVE-2018-10903, although snappass is
unaffected because it doesn't use the vulnerable ``finalize_with_tag`` API)
Version 1.4.0
-------------
*You will lose stored passwords during the upgrade to this version*
* Added a prefix in redis in front of the storage keys, making the redis safer to share with other applications
* Small test and syntax improvements
Version 1.3.0
-------------
* Quote urls to fix bug with ending in '='
* Mock redis
* Drop support for python 2.6 and python 3.3
Version 1.2.0
-------------
* Added Fernet cryptography to the stored keys, prevent access to full text passwords if someone has access to Redis

View file

@ -1,40 +0,0 @@
# Code of Conduct
At Pinterest, we work hard to ensure that our work environment is welcoming
and inclusive to as many people as possible. We are committed to creating this
environment for everyone involved in our open source projects as well. We
welcome all participants regardless of ability, age, ethnicity, identified
gender, religion (or lack there of), sexual orientation and socioeconomic
status.
This code of conduct details our expectations for upholding these values.
## Good behavior
We expect members of our community to exhibit good behavior including (but of
course not limited to):
- Using intentional and empathetic language.
- Focusing on resolving instead of escalating conflict.
- Providing constructive feedback.
## Unacceptable behavior
Some examples of unacceptable behavior (again, this is not an exhaustive
list):
- Harassment, publicly or in private.
- Trolling.
- Sexual advances (this isnt the place for it).
- Publishing others personal information.
- Any behavior which would be deemed unacceptable in a professional environment.
## Recourse
If you are witness to or the target of unacceptable behavior, it should be
reported to Pinterest at opensource-policy@pinterest.com. All reporters will
be kept confidential and an appropriate response for each incident will be
evaluated.
If the snappass maintainers do not uphold and enforce this code of conduct in
good faith, community leadership will hold them accountable.

View file

@ -63,9 +63,9 @@ If you are proposing a feature:
Setting Up the Code for Local Development Setting Up the Code for Local Development
----------------------------------------- -----------------------------------------
Here's how to set up ``snappass`` for local development. Here's how to set up `snappass` for local development.
1. Fork the ``snappass`` repo on GitHub. 1. Fork the `snappass` repo on GitHub.
2. Clone your fork locally:: 2. Clone your fork locally::
$ git clone git@github.com:your_name_here/snappass.git $ git clone git@github.com:your_name_here/snappass.git
@ -77,7 +77,7 @@ Here's how to set up ``snappass`` for local development.
$ mkvirtualenv snappass $ mkvirtualenv snappass
$ cd snappass/ $ cd snappass/
$ python setup.py develop $ python setup.py develop
$ make dev $ pip install -r dev-requirements.txt
4. Create a branch for local development:: 4. Create a branch for local development::
@ -85,36 +85,35 @@ Here's how to set up ``snappass`` for local development.
Now you can make your changes locally. Now you can make your changes locally.
5. You run a development server with debug and autoreload to manually verify:: 5. You can test your changes in a development server with debug and autoreload::
$ docker run -d --name redis-server -p 6379:6379 redis $ docker run -d --name redis-server -p 6379:6379 redis
$ make run $ export FLASK_DEBUG=1 && \
export FLASK_APP=snappass.main && \
export NO_SSL=True
$ flask run
You now have a running instance on localhost:5000/ You now have a running instance on localhost:5000/
6. Please add some tests to tests.py and run tests:: 6. When you're done making changes, check that your changes pass the tests and
$ make test
7. When you're done making changes, check that your changes pass the tests and
flake8:: flake8::
$ flake8 snappass tests.py setup.py $ flake8 snappass tests.py setup.py
$ tox $ tox
7. Commit your changes and push your branch to GitHub::
$ git add .
$ git commit -m "Your detailed description of your changes."
$ git push origin name-of-your-bugfix-or-feature
8. Check that the test coverage hasn't dropped:: 8. Check that the test coverage hasn't dropped::
$ coverage run --source snappass tests.py $ coverage run --source snappass tests.py
$ coverage report -m $ coverage report -m
$ coverage html $ coverage html
9. Commit your changes and push your branch to GitHub:: 9. Submit a pull request through the GitHub website.
$ git add .
$ git commit -m "Your detailed description of your changes."
$ git push origin name-of-your-bugfix-or-feature
10. Submit a pull request through the GitHub website.
Pull Request Guidelines Pull Request Guidelines
----------------------- -----------------------
@ -125,4 +124,8 @@ Before you submit a pull request, check that it meets these guidelines:
2. If the pull request adds functionality, the docs should be updated. Put 2. If the pull request adds functionality, the docs should be updated. Put
your new functionality into a function with a docstring, and add the your new functionality into a function with a docstring, and add the
feature to the list in README.rst. feature to the list in README.rst.
3. The pull request should work on all supported Python versions. 3. The pull request should work for Python 2.6, 2.7 and 3.3+. Check
`Travis`_ and make sure that
the tests pass for all supported Python versions.
.. _Travis: https://travis-ci.org/pinterest/snappass/pull_requests

View file

@ -1,4 +1,4 @@
FROM python:3.8-slim FROM python:3.6
ENV APP_DIR=/usr/src/snappass ENV APP_DIR=/usr/src/snappass
@ -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,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2012-2022 Pinterest Copyright (c) 2012-2013 Pinterest
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

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

@ -1,13 +0,0 @@
.PHONY: dev prod run test
dev: dev-requirements.txt
pip install -r dev-requirements.txt
prod: requirements.txt
pip install -r requirements.txt
run: prod
FLASK_DEBUG=1 FLASK_APP=snappass.main NO_SSL=True venv/bin/flask run
test:
PYTHONPATH=snappass venv/bin/nosetests -s tests

View file

@ -2,25 +2,29 @@
SnapPass SnapPass
======== ========
|pypi| |pypi| |build|
.. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg .. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg
:target: https://pypi.python.org/pypi/snappass :target: https://pypi.python.org/pypi/snappass
:alt: Latest version released on PyPI :alt: Latest version released on PyPI
It's like SnapChat... for passwords. .. |build| image:: https://travis-ci.org/pinterest/snappass.svg
:target: http://travis-ci.org/pinterest/snappass
:alt: Build status
This is a web app that lets you share passwords securely. It's like SnapChat... for Passwords.
This is a webapp that lets you share passwords securely.
Let's say you have a password. You want to give it to your coworker, Jane. Let's say you have a password. You want to give it to your coworker, Jane.
You could email it to her, but then it's in her email, which might be backed up, You could email it to her, but then it's in her email, which might be backed up,
and probably is in some storage device controlled by the NSA. and probably is in some storage device controlled by the NSA.
You could send it to her over chat, but chances are Jane logs all her messages You could send it to her over chat, but chances are Jane logs all her messages
because she uses Google Hangouts Chat, and Google Hangouts Chat might log everything. because she uses Google Talk, and Google Talk logs everything.
You could write it down, but you can't find a pen, and there's way too many You could write it down, but you can't find a pen, and there's way too many
characters because your security person, Paul, is paranoid. characters because your Security Person, Paul, is paranoid.
So we built SnapPass. It's not that complicated, it does one thing. If So we built SnapPass. It's not that complicated, it does one thing. If
Jane gets a link to the password and never looks at it, the password goes away. Jane gets a link to the password and never looks at it, the password goes away.
@ -46,10 +50,8 @@ This means that even if someone has access to the Redis store, the passwords are
Requirements Requirements
------------ ------------
* `Redis`_ * Redis.
* Python 3.8+ * Python 2.6, 2.7 or 3.3+.
.. _Redis: https://redis.io/
Installation Installation
------------ ------------
@ -64,217 +66,29 @@ Installation
Configuration Configuration
------------- -------------
Start by ensuring that Redis is up and running. You can configure the following via environment variables.
Then, you can configure the following via environment variables. `SECRET_KEY` this should be a unique key that's used to sign key. This should
``SECRET_KEY``: unique key that's used to sign key. This should
be kept secret. See the `Flask Documentation`__ for more information. be kept secret. See the `Flask Documentation`__ for more information.
.. __: http://flask.pocoo.org/docs/quickstart/#sessions .. __: http://flask.pocoo.org/docs/quickstart/#sessions
``DEBUG``: to run Flask web server in debug mode. See the `Flask Documentation`__ for more information. `DEBUG` to run Flask web server in debug mode. See the `Flask Documentation`__ for more information.
.. __: http://flask.pocoo.org/docs/quickstart/#debug-mode .. __: http://flask.pocoo.org/docs/quickstart/#debug-mode
``STATIC_URL``: this should be the location of your static assets. You might not `STATIC_URL` this should be the location of your static assets. You might not
need to change this. need to change this.
``NO_SSL``: if you are not using SSL. `NO_SSL` if you are not using SSL.
``URL_PREFIX``: useful when running snappass behind a reverse proxy like `nginx`. Example: ``"/some/path/"``, Defaults to ``None`` `REDIS_HOST` this should be set by Redis, but you can override it if you want. Defaults to `"localhost"`
``REDIS_HOST``: this should be set by Redis, but you can override it if you want. Defaults to ``"localhost"`` `REDIS_PORT` is the port redis is serving on, defaults to 6379
``REDIS_PORT``: is the port redis is serving on, defaults to 6379 `SNAPPASS_REDIS_DB` is the database that you want to use on this redis server. Defaults to db 0
``SNAPPASS_REDIS_DB``: is the database that you want to use on this redis server. Defaults to db 0
``REDIS_URL``: (optional) will be used instead of ``REDIS_HOST``, ``REDIS_PORT``, and ``SNAPPASS_REDIS_DB`` to configure the Redis client object. For example: redis://username:password@localhost:6379/0
``REDIS_PREFIX``: (optional, defaults to ``"snappass"``) prefix used on redis keys to prevent collisions with other potential clients
``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.
`REDIS_URL` is optional and, if set, will be used instead of `REDIS_HOST`, `REDIS_PORT`, and `SNAPPASS_REDIS_DB` to configure the Redis client object. For example: redis://username:password@localhost:6379/0
Docker Docker
------ ------
@ -288,17 +102,4 @@ Alternatively, you can use `Docker`_ and `Docker Compose`_ to install and run Sn
$ docker-compose up -d $ docker-compose up -d
This will pull all dependencies, i.e. Redis and appropriate Python version (3.7), then start up SnapPass and Redis server. SnapPass server is accessible at: http://localhost:5000 This will pull all dependencies, i.e. Redis and appropriate Python version (3.6), then start up SnapPass and Redis server. SnapPass server is accessible at: http://localhost:5000
Similar Tools
-------------
- `Snappass.NET <https://github.com/generateui/Snappass.NET>`_ is a .NET
(ASP.NET Core) port of SnapPass.
We're Hiring!
-------------
Are you really excited about open-source and great software engineering?
`Pinterest is hiring <https://careers.pinterest.com>`_!

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,3 @@
coverage==7.6.0 coverage==4.2
fakeredis==2.24.1 flake8==3.0.4
flake8==7.1.1 tox==2.3.1
freezegun==1.5.1
pytest==8.3.2
pytest-cov==5.0.0
tox==4.18.0
bumpversion==0.6.0
wheel==0.44.0

View file

@ -4,42 +4,14 @@ services:
snappass: snappass:
build: . build: .
#image: pinterest/snappass image: pinterest/snappass
#ports: ports:
# - "5000:5000" - "5000:5000"
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,7 @@
cryptography==43.0.1 Flask==0.11.1
Flask==3.0.0 Jinja2==2.7.1
itsdangerous==2.2.0 MarkupSafe==0.18
Jinja2==3.1.4 Werkzeug==0.9.4
MarkupSafe==2.1.1 itsdangerous==0.23
redis==5.0.1 redis==2.8.0
Werkzeug==3.0.3 cryptography==1.8.1
flask-babel

View file

@ -1,9 +1,12 @@
[bumpversion] [bumpversion]
current_version = 1.6.2 current_version = 1.1.1
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
[bumpversion:file:snappass/__init__.py]

View file

@ -2,12 +2,12 @@ from setuptools import setup
setup( setup(
name='snappass', name='snappass',
version='1.6.2', version='1.1.1',
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()),
url='http://github.com/Pinterest/snappass/', url='http://github.com/Pinterest/snappass/',
install_requires=['Flask', 'redis', 'cryptography'], install_requires=['Flask', 'redis'],
license='MIT', license='MIT',
author='Dave Dash', author='Dave Dash',
author_email='dd+github@davedash.com', author_email='dd+github@davedash.com',
@ -18,7 +18,6 @@ setup(
], ],
}, },
include_package_data=True, include_package_data=True,
python_requires='>=3.8, <4',
classifiers=[ classifiers=[
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers', 'Intended Audience :: Developers',
@ -26,10 +25,14 @@ setup(
'License :: OSI Approved :: MIT License', 'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Software Development :: Libraries :: Python Modules',
], ],
zip_safe=False, zip_safe=False,

View file

@ -1 +1,2 @@
__version__ = '1.5.1' __author__ = 'davedash'
__version__ = '1.1.1'

View file

@ -1,25 +1,23 @@
import os import os
import re
import sys import sys
import uuid 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
from redis.exceptions import ConnectionError from redis.exceptions import ConnectionError
from urllib.parse import quote_plus
from urllib.parse import unquote_plus
from urllib.parse import urljoin
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')))
URL_PREFIX = os.environ.get('URL_PREFIX', None) SNEAKY_USER_AGENTS = ('Slackbot', 'facebookexternalhit', 'Twitterbot',
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None) 'Facebot', 'WhatsApp', 'SkypeUriPreview',
'Iframely')
SNEAKY_USER_AGENTS_RE = re.compile('|'.join(SNEAKY_USER_AGENTS))
NO_SSL = os.environ.get('NO_SSL', False)
TOKEN_SEPARATOR = '~' TOKEN_SEPARATOR = '~'
# Initialize Flask Application
app = Flask(__name__) app = Flask(__name__)
if os.environ.get('DEBUG'): if os.environ.get('DEBUG'):
app.debug = True app.debug = True
@ -27,20 +25,7 @@ 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')))
if os.environ.get('REDIS_URL'):
# 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
if os.environ.get('MOCK_REDIS'):
from fakeredis import FakeStrictRedis
redis_client = FakeStrictRedis()
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'))
else: else:
redis_host = os.environ.get('REDIS_HOST', 'localhost') redis_host = os.environ.get('REDIS_HOST', 'localhost')
@ -48,12 +33,8 @@ else:
redis_db = os.environ.get('SNAPPASS_REDIS_DB', 0) redis_db = os.environ.get('SNAPPASS_REDIS_DB', 0)
redis_client = redis.StrictRedis( redis_client = redis.StrictRedis(
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')
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400, TIME_CONVERSION = {'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 +49,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 +84,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):
""" """
@ -143,7 +92,7 @@ def set_password(password, ttl):
Returns a token comprised of the key where the encrypted password Returns a token comprised of the key where the encrypted password
is stored, and the decryption key. is stored, and the decryption key.
""" """
storage_key = REDIS_PREFIX + uuid.uuid4().hex storage_key = uuid.uuid4().hex
encrypted_password, encryption_key = encrypt(password) encrypted_password, encryption_key = encrypt(password)
redis_client.setex(storage_key, ttl, encrypted_password) redis_client.setex(storage_key, ttl, encrypted_password)
encryption_key = encryption_key.decode('utf-8') encryption_key = encryption_key.decode('utf-8')
@ -171,12 +120,6 @@ def get_password(token):
return password.decode('utf-8') return password.decode('utf-8')
@check_redis_alive
def password_exists(token):
storage_key, decryption_key = parse_token(token)
return redis_client.exists(storage_key)
def empty(value): def empty(value):
if not value: if not value:
return True return True
@ -200,20 +143,12 @@ def clean_input():
return TIME_CONVERSION[time_period], request.form['password'] return TIME_CONVERSION[time_period], request.form['password']
def set_base_url(req): def request_is_valid(request):
if NO_SSL: """
if HOST_OVERRIDE: Ensure the request validates the following:
base_url = f'http://{HOST_OVERRIDE}/' - not made by some specific User-Agents (to avoid chat's preview feature issue)
else: """
base_url = req.url_root return not SNEAKY_USER_AGENTS_RE.search(request.headers.get('User-Agent', ''))
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'])
@ -223,139 +158,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 \ base_url = request.url_root
request.accept_mimetypes.accept_html:
return jsonify(link=link, ttl=ttl)
else: else:
base_url = request.url_root.replace("http://", "https://")
link = base_url + token
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):
password_key = unquote_plus(password_key)
if not password_exists(password_key):
return render_template('expired.html'), 404
return render_template('preview.html')
@app.route('/<password_key>', methods=['POST'])
def show_password(password_key): def show_password(password_key):
password_key = unquote_plus(password_key) if not request_is_valid(request):
abort(404)
password = get_password(password_key) password = get_password(password_key)
if not password: if not password:
return render_template('expired.html'), 404 abort(404)
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 it is too large Load diff

Before

Width:  |  Height:  |  Size: 434 KiB

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,10 +0,0 @@
(function () {
$('#revealSecret').click(function () {
var form = $('<form/>')
.attr('id', 'revealSecretForm')
.attr('method', 'post');
form.appendTo($('body'));
form.submit();
});
})();

View file

@ -1,34 +1,26 @@
<!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">
<link href="{{ config.STATIC_URL }}/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="{{ config.STATIC_URL }}/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="{{ config.STATIC_URL }}/fontawesome/css/font-awesome.min.css?v=4.7.0" rel="stylesheet"> <link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">
<link href="{{ config.STATIC_URL }}/snappass/css/custom.css" rel="stylesheet"> <link href="{{ config.STATIC_URL }}/snappass/css/custom.css" rel="stylesheet">
</head> </head>
<body> <body>
<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

@ -1,11 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<section>
<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">{{ _('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>
</div>
{% 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

@ -1,23 +0,0 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<section>
<div class="page-header">
<h1>{{ _('Secret') }}</h1>
</div>
<p class="lead">{{ _('You can only reveal the secret once!') }}</p>
<div class="row">
<div class="col-sm-6 margin-bottom-10">
<button id="revealSecret" type="button" class="btn-lg btn-primary">{{ _('Reveal secret') }}</button>
</div>
</div>
</section>
</div>
{% endblock %}
{% block js %}
<script src="{{ config.STATIC_URL }}/clipboardjs/clipboard.min.js"></script>
<script src="{{ config.STATIC_URL }}/snappass/scripts/clipboard_button.js"></script>
<script src="{{ config.STATIC_URL }}/snappass/scripts/preview.js"></script>
{% endblock %}

View file

@ -3,27 +3,26 @@
{% 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">
<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"></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="Week">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"

275
tests.py
View file

@ -1,16 +1,10 @@
import re
import time import time
import unittest import unittest
import uuid import uuid
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 werkzeug.exceptions import BadRequest from werkzeug.exceptions import BadRequest
from fakeredis import FakeStrictRedis
# noinspection PyPep8Naming # noinspection PyPep8Naming
import snappass.main as snappass import snappass.main as snappass
@ -20,20 +14,19 @@ __author__ = 'davedash'
class SnapPassTestCase(TestCase): class SnapPassTestCase(TestCase):
@mock.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)
self.assertEqual(password, snappass.get_password(key)) self.assertEqual(password, snappass.get_password(key))
# Assert that we can't look this up a second time. # Assert that we can't look this up a second time.
self.assertIsNone(snappass.get_password(key)) self.assertEqual(None, snappass.get_password(key))
def test_password_is_not_stored_in_plaintext(self): def test_password_is_not_stored_in_plaintext(self):
password = "trustno1" password = "trustno1"
token = snappass.set_password(password, 30) token = snappass.set_password(password, 30)
redis_key = token.split(snappass.TOKEN_SEPARATOR)[0] redis_key = token.split(snappass.TOKEN_SEPARATOR)[0]
stored_password_text = snappass.redis_client.get(redis_key).decode('utf-8') stored_password_text = snappass.redis_client.get(redis_key).decode('utf-8')
self.assertNotIn(password, stored_password_text) self.assertFalse(password in stored_password_text)
def test_returned_token_format(self): def test_returned_token_format(self):
password = "trustsome1" password = "trustsome1"
@ -41,7 +34,7 @@ class SnapPassTestCase(TestCase):
token_fragments = token.split(snappass.TOKEN_SEPARATOR) token_fragments = token.split(snappass.TOKEN_SEPARATOR)
self.assertEqual(2, len(token_fragments)) self.assertEqual(2, len(token_fragments))
redis_key, encryption_key = token_fragments redis_key, encryption_key = token_fragments
self.assertEqual(32 + len(snappass.REDIS_PREFIX), len(redis_key)) self.assertEqual(32, len(redis_key))
try: try:
Fernet(encryption_key.encode('utf-8')) Fernet(encryption_key.encode('utf-8'))
except ValueError: except ValueError:
@ -98,7 +91,7 @@ class SnapPassTestCase(TestCase):
password = 'open sesame' password = 'open sesame'
key = snappass.set_password(password, 1) key = snappass.set_password(password, 1)
time.sleep(1.5) time.sleep(1.5)
self.assertIsNone(snappass.get_password(key)) self.assertEqual(None, snappass.get_password(key))
class SnapPassRoutesTestCase(TestCase): class SnapPassRoutesTestCase(TestCase):
@ -107,250 +100,32 @@ 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):
password = "I like novelty kitten statues!"
key = snappass.set_password(password, 30)
rv = self.app.get('/{0}'.format(key))
self.assertNotIn(password, rv.get_data(as_text=True))
def test_show_password(self): def test_show_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)
rv = self.app.post('/{0}'.format(key)) rv = self.app.get('/{0}'.format(key))
self.assertIn(password, rv.get_data(as_text=True)) self.assertTrue(password in rv.get_data(as_text=True))
def test_url_prefix(self): def test_bots_denial(self):
password = "I like novelty kitten statues!" """
snappass.URL_PREFIX = "/test/prefix" Main known bots User-Agent should be denied access
rv = self.app.post('/', data={'password': password, 'ttl': 'hour'}) """
self.assertIn("localhost/test/prefix/", rv.get_data(as_text=True)) password = "Bots can't access this"
key = snappass.set_password(password, 30)
a_few_sneaky_bots = [
"Slackbot-LinkExpanding 1.0 (+https://api.slack.com/robots)",
"facebookexternalhit/1.1",
"Facebot/1.0",
"Twitterbot/1.0",
"_WhatsApp/2.12.81 (Windows NT 6.1; U; es-ES) Presto/2.9.181 Version/12.00",
"WhatsApp/2.16.6/i",
"SkypeUriPreview Preview/0.5",
"Iframely/0.8.5 (+http://iframely.com/;)",
]
def test_set_password(self): for ua in a_few_sneaky_bots:
with freeze_time("2020-05-08 12:00:00") as frozen_time: rv = self.app.get('/{0}'.format(key), headers={ 'User-Agent': ua })
password = 'my name is my passport. verify me.' self.assertEqual(rv.status_code, 404)
rv = self.app.post('/', data={'password': password, 'ttl': 'two weeks'})
html_content = rv.data.decode("ascii")
key = re.search(r'id="password-link" value="https://localhost/([^"]+)', html_content).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_json(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(
'/',
headers={'Accept': 'application/json'},
data={'password': password, 'ttl': 'two weeks'},
)
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(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__':

16
tox.ini
View file

@ -1,15 +1,15 @@
[tox] [tox]
envlist = py38, py39, py310, flake8 envlist = py26, py27, py33, py34, py35, py36, flake8
[testenv] [testenv]
setenv = deps =
MOCK_REDIS = 1 pytest
pytest-cov
commands = commands =
pip install -r requirements.txt pip install -r requirements.txt --use-wheel
pip install -r dev-requirements.txt py.test --junitxml=junit-{envname}.xml --cov-report xml tests.py
pytest --cov=snappass --cov-report=term-missing tests.py
[testenv:flake8] [testenv:flake8]
commands = commands =
pip install -r dev-requirements.txt pip install flake8
flake8 flake8 snappass/