Compare commits
No commits in common. "master" and "v1.4.0" have entirely different histories.
47 changed files with 190 additions and 4108 deletions
5
.bumpversion.cfg
Normal file
5
.bumpversion.cfg
Normal file
|
@ -0,0 +1,5 @@
|
|||
[bumpversion]
|
||||
files = setup.py
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 0.1.0
|
|
@ -6,6 +6,7 @@
|
|||
CONTRIBUTING.rst
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
requirements.txt
|
||||
tests.py
|
||||
tox.ini
|
||||
|
||||
|
|
8
.github/codeql-config.yml
vendored
8
.github/codeql-config.yml
vendored
|
@ -1,8 +0,0 @@
|
|||
name: "CodeQL config"
|
||||
|
||||
paths-ignore:
|
||||
- tests.py
|
||||
- 'snappass/static/bootstrap/**'
|
||||
- 'snappass/static/clipboardjs/**'
|
||||
- 'snappass/static/fontawesome/**'
|
||||
- 'snappass/static/jquery/**'
|
14
.github/dependabot.yml
vendored
14
.github/dependabot.yml
vendored
|
@ -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"]
|
37
.github/workflows/ci.yml
vendored
37
.github/workflows/ci.yml
vendored
|
@ -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
|
45
.github/workflows/codeql-analysis.yml
vendored
45
.github/workflows/codeql-analysis.yml
vendored
|
@ -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
5
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
|||
.env
|
||||
.project
|
||||
*.rdb
|
||||
junit*xml
|
||||
|
@ -51,7 +50,3 @@ htmlcov/
|
|||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Translation catalogs
|
||||
*.mo
|
||||
*.pot
|
||||
|
|
13
.travis.yml
Normal file
13
.travis.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
sudo: false
|
||||
install:
|
||||
- pip install tox-travis
|
||||
script:
|
||||
- tox
|
||||
services:
|
||||
- redis-server
|
|
@ -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/)
|
|
@ -19,4 +19,3 @@ Thanks a lot for the contributions of:
|
|||
* Donny Winston
|
||||
* James Barclay
|
||||
* Thomas Decaux
|
||||
* Lauri Lubi
|
||||
|
|
|
@ -1,43 +1,3 @@
|
|||
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 '='
|
||||
|
|
|
@ -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 isn’t the place for it).
|
||||
- Publishing other’s 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.
|
|
@ -63,9 +63,9 @@ If you are proposing a feature:
|
|||
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::
|
||||
|
||||
$ 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
|
||||
$ cd snappass/
|
||||
$ python setup.py develop
|
||||
$ make dev
|
||||
$ pip install -r dev-requirements.txt
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
$ 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/
|
||||
|
||||
6. Please add some tests to tests.py and run tests::
|
||||
|
||||
$ make test
|
||||
|
||||
7. When you're done making changes, check that your changes pass the tests and
|
||||
6. When you're done making changes, check that your changes pass the tests and
|
||||
flake8::
|
||||
|
||||
$ flake8 snappass tests.py setup.py
|
||||
$ 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::
|
||||
|
||||
$ coverage run --source snappass tests.py
|
||||
$ coverage report -m
|
||||
$ coverage html
|
||||
|
||||
9. 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
|
||||
|
||||
10. Submit a pull request through the GitHub website.
|
||||
9. Submit a pull request through the GitHub website.
|
||||
|
||||
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
|
||||
your new functionality into a function with a docstring, and add the
|
||||
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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.8-slim
|
||||
FROM python:3.6
|
||||
|
||||
ENV APP_DIR=/usr/src/snappass
|
||||
|
||||
|
@ -8,13 +8,9 @@ RUN groupadd -r snappass && \
|
|||
|
||||
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"]
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
RUN pybabel compile -d snappass/translations
|
||||
|
||||
RUN python setup.py install && \
|
||||
chown -R snappass $APP_DIR && \
|
||||
chgrp -R snappass $APP_DIR
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
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
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
include *.rst LICENSE
|
||||
recursive-include snappass/static *
|
||||
recursive-include snappass/templates *
|
||||
recursive-include snappass/translations *
|
||||
|
|
13
Makefile
13
Makefile
|
@ -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
|
237
README.rst
237
README.rst
|
@ -2,25 +2,29 @@
|
|||
SnapPass
|
||||
========
|
||||
|
||||
|pypi|
|
||||
|pypi| |build|
|
||||
|
||||
.. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg
|
||||
:target: https://pypi.python.org/pypi/snappass
|
||||
: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.
|
||||
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.
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
------------
|
||||
|
||||
* `Redis`_
|
||||
* Python 3.8+
|
||||
|
||||
.. _Redis: https://redis.io/
|
||||
* Redis
|
||||
* Python 2.7+ or 3.4+ (both included)
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -64,217 +66,31 @@ Installation
|
|||
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``: 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.
|
||||
|
||||
.. __: 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
|
||||
|
||||
``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.
|
||||
|
||||
``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` (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
|
||||
|
||||
Docker
|
||||
------
|
||||
|
@ -288,17 +104,10 @@ Alternatively, you can use `Docker`_ and `Docker Compose`_ to install and run Sn
|
|||
|
||||
$ 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
|
||||
|
||||
Similar Tools
|
||||
-------------
|
||||
|
||||
- `Snappass.NET <https://github.com/generateui/Snappass.NET>`_ is a .NET
|
||||
(ASP.NET Core) port of SnapPass.
|
||||
|
||||
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
|
||||
|
||||
We're Hiring!
|
||||
-------------
|
||||
|
||||
Are you really excited about open-source and great software engineering?
|
||||
`Pinterest is hiring <https://careers.pinterest.com>`_!
|
||||
Pinterest is [hiring](https://careers.pinterest.com/)!
|
||||
|
|
10
babel.cfg
10
babel.cfg
|
@ -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]
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
coverage==7.6.0
|
||||
fakeredis==2.24.1
|
||||
flake8==7.1.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
|
||||
pytest==3.5.1
|
||||
pytest-cov==2.5.1
|
||||
mockredispy==2.9.3
|
||||
coverage==4.2
|
||||
flake8==3.0.4
|
||||
tox==3.0.0
|
||||
|
|
|
@ -2,44 +2,17 @@ version: '2'
|
|||
|
||||
services:
|
||||
|
||||
snappass:
|
||||
build: .
|
||||
#image: pinterest/snappass
|
||||
#ports:
|
||||
# - "5000:5000"
|
||||
stop_signal: SIGINT
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- NO_SSL=false
|
||||
- SECRET_KEY=${SECRET_KEY}
|
||||
- VIRTUAL_HOST=share.brothertec.eu
|
||||
- VIRTUAL_PORT=5000
|
||||
- LETSENCRYPT_HOST=share.brothertec.eu
|
||||
- LETSENCRYPT_EMAIL=admin@brothertec.eu
|
||||
snappass:
|
||||
build: .
|
||||
image: pinterest/snappass
|
||||
ports:
|
||||
- "5000:5000"
|
||||
stop_signal: SIGINT
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- NO_SSL=True
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
- "com.centurylinklabs.watchtower.enable=false"
|
||||
|
||||
networks:
|
||||
- default
|
||||
- proxy
|
||||
- edge-tier
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
redis:
|
||||
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
|
||||
redis:
|
||||
image: "redis:latest"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
cryptography==43.0.1
|
||||
Flask==3.0.0
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.4
|
||||
MarkupSafe==2.1.1
|
||||
redis==5.0.1
|
||||
Werkzeug==3.0.3
|
||||
flask-babel
|
||||
Flask==0.11.1
|
||||
Jinja2==2.7.1
|
||||
MarkupSafe==0.18
|
||||
Werkzeug==0.9.4
|
||||
itsdangerous==0.23
|
||||
redis==2.8.0
|
||||
cryptography==1.8.1
|
||||
mock==1.0.1
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
[bumpversion]
|
||||
current_version = 1.6.2
|
||||
current_version = 1.4.0
|
||||
commit = True
|
||||
tag = True
|
||||
files = setup.py
|
||||
files = setup.py snappass/__init__.py
|
||||
|
||||
[flake8]
|
||||
show-source = True
|
||||
max-line-length = 120
|
||||
|
||||
[bumpversion:file:snappass/__init__.py]
|
||||
|
||||
|
|
11
setup.py
11
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup
|
|||
|
||||
setup(
|
||||
name='snappass',
|
||||
version='1.6.2',
|
||||
version='1.4.0',
|
||||
description="It's like SnapChat... for Passwords.",
|
||||
long_description=(open('README.rst').read() + '\n\n' +
|
||||
open('AUTHORS.rst').read()),
|
||||
|
@ -18,7 +18,6 @@ setup(
|
|||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
python_requires='>=3.8, <4',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
|
@ -26,10 +25,12 @@ setup(
|
|||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
zip_safe=False,
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
__version__ = '1.5.1'
|
||||
__author__ = 'davedash'
|
||||
__version__ = '1.4.0'
|
||||
|
|
225
snappass/main.py
225
snappass/main.py
|
@ -1,24 +1,24 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import redis
|
||||
|
||||
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 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
|
||||
from werkzeug.urls import url_quote_plus
|
||||
from werkzeug.urls import url_unquote_plus
|
||||
|
||||
NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False')))
|
||||
URL_PREFIX = os.environ.get('URL_PREFIX', None)
|
||||
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
|
||||
|
||||
SNEAKY_USER_AGENTS = ('Slackbot', 'facebookexternalhit', 'Twitterbot',
|
||||
'Facebot', 'WhatsApp', 'SkypeUriPreview', 'Iframely')
|
||||
SNEAKY_USER_AGENTS_RE = re.compile('|'.join(SNEAKY_USER_AGENTS))
|
||||
NO_SSL = os.environ.get('NO_SSL', False)
|
||||
TOKEN_SEPARATOR = '~'
|
||||
|
||||
|
||||
# Initialize Flask Application
|
||||
app = Flask(__name__)
|
||||
if os.environ.get('DEBUG'):
|
||||
|
@ -27,19 +27,10 @@ app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key')
|
|||
app.config.update(
|
||||
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
|
||||
if os.environ.get('MOCK_REDIS'):
|
||||
from fakeredis import FakeStrictRedis
|
||||
|
||||
redis_client = FakeStrictRedis()
|
||||
from mockredis import mock_strict_redis_client
|
||||
redis_client = mock_strict_redis_client()
|
||||
elif os.environ.get('REDIS_URL'):
|
||||
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
|
||||
else:
|
||||
|
@ -50,10 +41,7 @@ else:
|
|||
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,
|
||||
'hour': 3600}
|
||||
DEFAULT_API_TTL = 1209600
|
||||
MAX_TTL = DEFAULT_API_TTL
|
||||
TIME_CONVERSION = {'week': 604800, 'day': 86400, 'hour': 3600}
|
||||
|
||||
|
||||
def check_redis_alive(fn):
|
||||
|
@ -68,7 +56,6 @@ def check_redis_alive(fn):
|
|||
sys.exit(0)
|
||||
else:
|
||||
return abort(500)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
|
@ -104,37 +91,6 @@ def parse_token(token):
|
|||
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
|
||||
def set_password(password, ttl):
|
||||
"""
|
||||
|
@ -171,12 +127,6 @@ def get_password(token):
|
|||
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):
|
||||
if not value:
|
||||
return True
|
||||
|
@ -200,20 +150,12 @@ def clean_input():
|
|||
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
|
||||
def request_is_valid(request):
|
||||
"""
|
||||
Ensure the request validates the following:
|
||||
- not made by some specific User-Agents (to avoid chat's preview feature issue)
|
||||
"""
|
||||
return not SNEAKY_USER_AGENTS_RE.search(request.headers.get('User-Agent', ''))
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
|
@ -223,139 +165,32 @@ def index():
|
|||
|
||||
@app.route('/', methods=['POST'])
|
||||
def handle_password():
|
||||
password = request.form.get('password')
|
||||
ttl = request.form.get('ttl')
|
||||
if clean_input():
|
||||
ttl = TIME_CONVERSION[ttl.lower()]
|
||||
token = set_password(password, ttl)
|
||||
base_url = set_base_url(request)
|
||||
link = base_url + quote_plus(token)
|
||||
if request.accept_mimetypes.accept_json and not \
|
||||
request.accept_mimetypes.accept_html:
|
||||
return jsonify(link=link, ttl=ttl)
|
||||
else:
|
||||
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
|
||||
)
|
||||
|
||||
ttl, password = clean_input()
|
||||
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)
|
||||
if NO_SSL:
|
||||
base_url = request.url_root
|
||||
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)
|
||||
base_url = request.url_root.replace("http://", "https://")
|
||||
link = base_url + url_quote_plus(token)
|
||||
return render_template('confirm.html', password_link=link)
|
||||
|
||||
|
||||
@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):
|
||||
password_key = unquote_plus(password_key)
|
||||
if not request_is_valid(request):
|
||||
abort(404)
|
||||
password_key = url_unquote_plus(password_key)
|
||||
password = get_password(password_key)
|
||||
if not password:
|
||||
return render_template('expired.html'), 404
|
||||
abort(404)
|
||||
|
||||
return render_template('password.html', password=password)
|
||||
|
||||
|
||||
@app.route('/_/_/health', methods=['GET'])
|
||||
@check_redis_alive
|
||||
def health_check():
|
||||
return {}
|
||||
|
||||
|
||||
@check_redis_alive
|
||||
def main():
|
||||
app.run(host=os.environ.get('SNAPPASS_BIND_ADDRESS', '0.0.0.0'),
|
||||
port=os.environ.get('SNAPPASS_PORT', 5000))
|
||||
app.run(host='0.0.0.0')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load diff
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
5
snappass/static/jquery/jquery-1.12.4.min.js
vendored
Normal file
5
snappass/static/jquery/jquery-1.12.4.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
snappass/static/jquery/jquery-3.6.0.min.js
vendored
2
snappass/static/jquery/jquery-3.6.0.min.js
vendored
File diff suppressed because one or more lines are too long
|
@ -1,10 +0,0 @@
|
|||
(function () {
|
||||
|
||||
$('#revealSecret').click(function () {
|
||||
var form = $('<form/>')
|
||||
.attr('id', 'revealSecretForm')
|
||||
.attr('method', 'post');
|
||||
form.appendTo($('body'));
|
||||
form.submit();
|
||||
});
|
||||
})();
|
|
@ -1,34 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ _('en') }}">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{{ _('Snappass - Share Secrets') }}</title>
|
||||
<title>Snappass - Share Secrets</title>
|
||||
<meta charset="UTF-8">
|
||||
<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 }}/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">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-default navbar-static-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/">{{ _('Share Secret') }}</a>
|
||||
<a class="navbar-brand" href="/">Share Secret</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{% 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>
|
||||
{% 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>
|
||||
</html>
|
||||
|
|
|
@ -3,15 +3,15 @@
|
|||
{% block content %}
|
||||
<div class="container">
|
||||
<section>
|
||||
<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>
|
||||
<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>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 margin-bottom-10">
|
||||
<input type="text" class="form-control" id="password-link" value="{{ password_link }}" readonly="readonly">
|
||||
</div>
|
||||
|
||||
<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"
|
||||
data-placement='bottom'>
|
||||
<i class="fa fa-clipboard"></i>
|
||||
|
|
|
@ -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 %}
|
|
@ -3,22 +3,22 @@
|
|||
{% block content %}
|
||||
<div class="container">
|
||||
<section>
|
||||
<div class="page-header"><h1>{{ _('Secret') }}</h1></div>
|
||||
<p>{{ _('Save the following secret to a secure location.') }}</p>
|
||||
<div class="page-header"><h1>Secret</h1></div>
|
||||
<p>Save the following secret to a secure location.</p>
|
||||
<div class="row">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
data-placement='bottom'>
|
||||
<i class="fa fa-clipboard"></i>
|
||||
</button>
|
||||
</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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -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 %}
|
|
@ -3,27 +3,26 @@
|
|||
{% block content %}
|
||||
<div class="container">
|
||||
<section>
|
||||
<div class="page-header"><h1>{{ _('Set Secret') }}</h1></div>
|
||||
<div class="page-header"><h1>Set Secret</h1></div>
|
||||
<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="input-group">
|
||||
<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 class="col-sm-2 margin-bottom-10">
|
||||
<select class="form-control" name="ttl">
|
||||
<option value="Two Weeks">{{ _('Two Weeks') }}</option>
|
||||
<option value="Week" selected="selected">{{ _('Week') }}</option>
|
||||
<option value="Day">{{ _('Day') }}</option>
|
||||
<option value="Hour">{{ _('Hour') }}</option>
|
||||
<option value="Week">Week</option>
|
||||
<option value="Day">Day</option>
|
||||
<option value="Hour">Hour</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
271
tests.py
271
tests.py
|
@ -1,16 +1,12 @@
|
|||
import re
|
||||
from mock import patch
|
||||
import time
|
||||
import unittest
|
||||
import uuid
|
||||
from unittest import TestCase
|
||||
from unittest import mock
|
||||
from urllib.parse import quote
|
||||
from urllib.parse import unquote
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from freezegun import freeze_time
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from fakeredis import FakeStrictRedis
|
||||
from mockredis import mock_strict_redis_client
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
import snappass.main as snappass
|
||||
|
@ -20,7 +16,7 @@ __author__ = 'davedash'
|
|||
|
||||
class SnapPassTestCase(TestCase):
|
||||
|
||||
@mock.patch('redis.client.StrictRedis', FakeStrictRedis)
|
||||
@patch('redis.client.StrictRedis', mock_strict_redis_client)
|
||||
def test_get_password(self):
|
||||
password = "melatonin overdose 1337!$"
|
||||
key = snappass.set_password(password, 30)
|
||||
|
@ -98,6 +94,9 @@ class SnapPassTestCase(TestCase):
|
|||
password = 'open sesame'
|
||||
key = snappass.set_password(password, 1)
|
||||
time.sleep(1.5)
|
||||
# Expire functionality must be explicitly invoked using do_expire(time).
|
||||
# mockredis does not support automatic expiration at this time
|
||||
snappass.redis_client.do_expire()
|
||||
self.assertIsNone(snappass.get_password(key))
|
||||
|
||||
|
||||
|
@ -107,250 +106,32 @@ class SnapPassRoutesTestCase(TestCase):
|
|||
snappass.app.config['TESTING'] = True
|
||||
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):
|
||||
password = "I like novelty kitten statues!"
|
||||
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))
|
||||
|
||||
def test_url_prefix(self):
|
||||
password = "I like novelty kitten statues!"
|
||||
snappass.URL_PREFIX = "/test/prefix"
|
||||
rv = self.app.post('/', data={'password': password, 'ttl': 'hour'})
|
||||
self.assertIn("localhost/test/prefix/", rv.get_data(as_text=True))
|
||||
def test_bots_denial(self):
|
||||
"""
|
||||
Main known bots User-Agent should be denied access
|
||||
"""
|
||||
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):
|
||||
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||
password = 'my name is my passport. verify me.'
|
||||
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')
|
||||
for ua in a_few_sneaky_bots:
|
||||
rv = self.app.get('/{0}'.format(key), headers={'User-Agent': ua})
|
||||
self.assertEqual(404, rv.status_code)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
8
tox.ini
8
tox.ini
|
@ -1,5 +1,5 @@
|
|||
[tox]
|
||||
envlist = py38, py39, py310, flake8
|
||||
envlist = py27, py34, py35, py36, flake8
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
|
@ -7,9 +7,9 @@ setenv =
|
|||
commands =
|
||||
pip install -r 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]
|
||||
commands =
|
||||
pip install -r dev-requirements.txt
|
||||
flake8
|
||||
pip install flake8
|
||||
flake8 snappass/
|
||||
|
|
Loading…
Add table
Reference in a new issue