Compare commits
217 commits
Author | SHA1 | Date | |
---|---|---|---|
b995f42fb4 | |||
c3ebc7a11b | |||
760d2b7748 | |||
f7a7c4a375 | |||
|
86e7e2e220 | ||
|
c13e80bf2a | ||
|
5745dd40f1 | ||
|
f3af080237 | ||
|
ce7994e95c | ||
|
eea66f49e7 | ||
|
63f7d8f18d | ||
|
7af9712941 | ||
|
6370c0439d | ||
|
9e435787c6 | ||
|
4e5fc2ef1f | ||
|
57ec6249a6 | ||
|
d3ffeac585 | ||
|
f6ad788fda | ||
|
91dd677cdb | ||
|
4c5f63b536 | ||
|
e24732235a | ||
|
95b7573c56 | ||
|
e2ca2fa8b9 | ||
|
7475a98829 | ||
|
cbeb61196d | ||
|
eaf96efa4e | ||
|
20136d9dc0 | ||
|
e4803977c0 | ||
|
b69290425a | ||
|
04235c1edc | ||
|
26b26f9c25 | ||
|
3cfd5f03c0 | ||
|
2023c9dc35 | ||
|
82d3a61afd | ||
|
ad5a7de292 | ||
|
ff35bb6490 | ||
|
ed9e715b68 | ||
|
05cd81c671 | ||
|
760782259d | ||
|
9c233c0bd1 | ||
|
838cdf6d9c | ||
|
dc321ef79c | ||
|
04f9402e5f | ||
|
5d37e45517 | ||
|
2b108d3630 | ||
|
106ac26e26 | ||
|
114b5af6c2 | ||
|
4fffb9c1d5 | ||
|
9fdddab11b | ||
|
415d5ee4e6 | ||
|
ba67b426cc | ||
|
82c345ca92 | ||
|
3cba96671e | ||
|
f551b734ca | ||
|
0084d856c7 | ||
|
e0b8245d46 | ||
|
fcfc1b018a | ||
|
28c396e859 | ||
|
49de2bc0fb | ||
|
d178664d0c | ||
|
6d17603016 | ||
|
62a629021a | ||
|
6d294c63fd | ||
|
455db36189 | ||
|
74ded4156b | ||
|
d8c05a9d62 | ||
|
564a29d25f | ||
|
6798a262b3 | ||
|
29ce62bbf8 | ||
|
a8e4312a6c | ||
|
3871c39b05 | ||
|
7db0be7a90 | ||
|
b66b1e1bb2 | ||
|
b53ceed6eb | ||
|
fd27ab7d4a | ||
|
ae2747311a | ||
|
8103cb4adb | ||
|
baa921f1cb | ||
|
99028bff16 | ||
|
6a10fd32d5 | ||
|
1a9824d24d | ||
|
c4d6074e48 | ||
|
03bf76fbcc | ||
|
f3edccdd1f | ||
|
31ae18d57d | ||
|
9d68d6b058 | ||
|
4c118cf022 | ||
|
5725b0db2e | ||
|
a34aaf8bb4 | ||
|
6fec10eaab | ||
|
147bdf390a | ||
|
95f5c35291 | ||
|
013c0d1e77 | ||
|
6f02f6e2b7 | ||
|
1e1b189d77 | ||
|
a2a887bb2c | ||
|
0aaf1ec89b | ||
|
c251bffc89 | ||
|
7da90b08a4 | ||
|
26fb06efe3 | ||
|
4292228200 | ||
|
f13bc17d92 | ||
|
1245b0c43f | ||
|
25cd5740d4 | ||
|
3011638028 | ||
|
2304a29e7c | ||
|
e61453d577 | ||
|
8f9ecb8a7a | ||
|
b2a41073de | ||
|
bfae576fb2 | ||
|
f89a8b2fdc | ||
|
36b2d79e38 | ||
|
ecdcb70470 | ||
|
ca3ba14c21 | ||
|
bdba9bf7f6 | ||
|
261fa83273 | ||
|
bcef439238 | ||
|
abacd0c776 | ||
|
f16106acc7 | ||
|
d6aa58976e | ||
|
47f002ab2d | ||
|
b8121166b7 | ||
|
37cd63d394 | ||
|
bdefc11a72 | ||
|
cbbe67dcae | ||
|
f21c696a5d | ||
|
ff243787c7 | ||
|
2c702b0a39 | ||
|
5e47d5efa1 | ||
|
7102b4560b | ||
|
8e946c2bdb | ||
|
68c5f14cd4 | ||
|
c491c621d2 | ||
|
4a9c18c1b8 | ||
|
487d10231d | ||
|
1e37c82f64 | ||
|
2341cb6a5e | ||
|
3c379339dd | ||
|
27f70ed5ec | ||
|
35c19a2ae2 | ||
|
9e33a8f7c1 | ||
|
3fbc018ff8 | ||
|
4b1ee0cec1 | ||
|
654d03041b | ||
|
40df900dc7 | ||
|
89a90f4924 | ||
|
188f0f6779 | ||
|
9916076100 | ||
|
5dc2161a5d | ||
|
a94e16802d | ||
|
6a349e83c0 | ||
|
9ea826ef7c | ||
|
50ef7bef82 | ||
|
feab2f69a5 | ||
|
5c9d3bf3cf | ||
|
8a3a7f7c39 | ||
|
3d86f5395b | ||
|
0ca032265a | ||
|
2af7037feb | ||
|
9cb554ca7e | ||
|
f377aa3ed2 | ||
|
e49cd8963a | ||
|
4acef097e8 | ||
|
b3e1068c01 | ||
|
28d9e1e089 | ||
|
5747ee2d14 | ||
|
054c61ae89 | ||
|
f7fbb4575c | ||
|
234f43b889 | ||
|
e0b996d3d3 | ||
|
6e6612cd49 | ||
|
f0f2c9d5d8 | ||
|
37f5d2b658 | ||
|
2aa7272a59 | ||
|
1ac262e34e | ||
|
d4c96cf58a | ||
|
3fc5d2b864 | ||
|
ecebbcb71f | ||
|
0c77baa581 | ||
|
52aefd6ce3 | ||
|
0bd2b4e8d9 | ||
|
95c9ecc7d0 | ||
|
166a73b0dd | ||
|
5894033692 | ||
|
7eee21f413 | ||
|
30db653f14 | ||
|
06149b81e8 | ||
|
921492733b | ||
|
2b53eed348 | ||
|
867dc24f83 | ||
|
0f1cc0900c | ||
|
73c220be97 | ||
|
59196bea40 | ||
|
2c334fc19f | ||
|
5981884cd2 | ||
|
b45312c650 | ||
|
25e10ef8a1 | ||
|
798f358ed6 | ||
|
fb9974cbd5 | ||
|
04ead0da32 | ||
|
47565b3831 | ||
|
76962f8d8c | ||
|
386a378c5d | ||
|
a42815d17e | ||
|
a2d4245a3a | ||
|
5ddecd4e64 | ||
|
d407c2657f | ||
|
80f77a6572 | ||
|
e6eca0daf8 | ||
|
548c9986ee | ||
|
699293b5a0 | ||
|
6fe4733baa | ||
|
173f33f66e | ||
|
75b6a6919f | ||
|
f59fd3379e | ||
|
2e0a296222 | ||
|
331d421e10 |
47 changed files with 4129 additions and 190 deletions
|
@ -1,5 +0,0 @@
|
|||
[bumpversion]
|
||||
files = setup.py
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 0.1.0
|
|
@ -6,7 +6,6 @@
|
|||
CONTRIBUTING.rst
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
requirements.txt
|
||||
tests.py
|
||||
tox.ini
|
||||
|
||||
|
|
8
.github/codeql-config.yml
vendored
Normal file
8
.github/codeql-config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
name: "CodeQL config"
|
||||
|
||||
paths-ignore:
|
||||
- tests.py
|
||||
- 'snappass/static/bootstrap/**'
|
||||
- 'snappass/static/clipboardjs/**'
|
||||
- 'snappass/static/fontawesome/**'
|
||||
- 'snappass/static/jquery/**'
|
14
.github/dependabot.yml
vendored
Normal file
14
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
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
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
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
Normal file
45
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,45 @@
|
|||
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,3 +1,4 @@
|
|||
.env
|
||||
.project
|
||||
*.rdb
|
||||
junit*xml
|
||||
|
@ -50,3 +51,7 @@ htmlcov/
|
|||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Translation catalogs
|
||||
*.mo
|
||||
*.pot
|
||||
|
|
15
.travis.yml
15
.travis.yml
|
@ -1,15 +0,0 @@
|
|||
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
|
9
ADOPTERS.md
Normal file
9
ADOPTERS.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 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,3 +19,4 @@ Thanks a lot for the contributions of:
|
|||
* Donny Winston
|
||||
* James Barclay
|
||||
* Thomas Decaux
|
||||
* Lauri Lubi
|
||||
|
|
|
@ -1,3 +1,43 @@
|
|||
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 '='
|
||||
|
|
40
CODE_OF_CONDUCT.md
Normal file
40
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,40 @@
|
|||
# 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
|
||||
$ pip install -r dev-requirements.txt
|
||||
$ make dev
|
||||
|
||||
4. Create a branch for local development::
|
||||
|
||||
|
@ -85,35 +85,36 @@ Here's how to set up `snappass` for local development.
|
|||
|
||||
Now you can make your changes locally.
|
||||
|
||||
5. You can test your changes in a development server with debug and autoreload::
|
||||
5. You run a development server with debug and autoreload to manually verify::
|
||||
|
||||
$ docker run -d --name redis-server -p 6379:6379 redis
|
||||
$ export FLASK_DEBUG=1 && \
|
||||
export FLASK_APP=snappass.main && \
|
||||
export NO_SSL=True
|
||||
$ flask run
|
||||
$ make run
|
||||
|
||||
You now have a running instance on localhost:5000/
|
||||
|
||||
6. When you're done making changes, check that your changes pass the tests and
|
||||
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
|
||||
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. Submit a pull request through the GitHub website.
|
||||
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.
|
||||
|
||||
Pull Request Guidelines
|
||||
-----------------------
|
||||
|
@ -124,8 +125,4 @@ 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 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
|
||||
3. The pull request should work on all supported Python versions.
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.6
|
||||
FROM python:3.8-slim
|
||||
|
||||
ENV APP_DIR=/usr/src/snappass
|
||||
|
||||
|
@ -8,9 +8,13 @@ RUN groupadd -r snappass && \
|
|||
|
||||
WORKDIR $APP_DIR
|
||||
|
||||
COPY ["setup.py", "MANIFEST.in", "README.rst", "AUTHORS.rst", "$APP_DIR/"]
|
||||
COPY ["setup.py", "requirements.txt", "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-2013 Pinterest
|
||||
Copyright (c) 2012-2022 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,3 +1,4 @@
|
|||
include *.rst LICENSE
|
||||
recursive-include snappass/static *
|
||||
recursive-include snappass/templates *
|
||||
recursive-include snappass/translations *
|
||||
|
|
13
Makefile
Normal file
13
Makefile
Normal file
|
@ -0,0 +1,13 @@
|
|||
.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
|
241
README.rst
241
README.rst
|
@ -2,29 +2,25 @@
|
|||
SnapPass
|
||||
========
|
||||
|
||||
|pypi| |build|
|
||||
|pypi|
|
||||
|
||||
.. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg
|
||||
:target: https://pypi.python.org/pypi/snappass
|
||||
:alt: Latest version released on PyPI
|
||||
|
||||
.. |build| image:: https://travis-ci.org/pinterest/snappass.svg
|
||||
:target: http://travis-ci.org/pinterest/snappass
|
||||
:alt: Build status
|
||||
It's like SnapChat... for passwords.
|
||||
|
||||
It's like SnapChat... for Passwords.
|
||||
|
||||
This is a webapp that lets you share passwords securely.
|
||||
This is a web app 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 Talk, and Google Talk logs everything.
|
||||
because she uses Google Hangouts Chat, and Google Hangouts Chat might log 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.
|
||||
|
@ -50,8 +46,10 @@ This means that even if someone has access to the Redis store, the passwords are
|
|||
Requirements
|
||||
------------
|
||||
|
||||
* Redis.
|
||||
* Python 2.6, 2.7 or 3.3+.
|
||||
* `Redis`_
|
||||
* Python 3.8+
|
||||
|
||||
.. _Redis: https://redis.io/
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
@ -66,29 +64,217 @@ Installation
|
|||
Configuration
|
||||
-------------
|
||||
|
||||
You can configure the following via environment variables.
|
||||
Start by ensuring that Redis is up and running.
|
||||
|
||||
`SECRET_KEY` this should be a unique key that's used to sign key. This should
|
||||
Then, you can configure the following via environment variables.
|
||||
|
||||
``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.
|
||||
|
||||
`REDIS_HOST` this should be set by Redis, but you can override it if you want. Defaults to `"localhost"`
|
||||
``URL_PREFIX``: useful when running snappass behind a reverse proxy like `nginx`. Example: ``"/some/path/"``, Defaults to ``None``
|
||||
|
||||
`REDIS_PORT` is the port redis is serving on, defaults to 6379
|
||||
``REDIS_HOST``: this should be set by Redis, but you can override it if you want. Defaults to ``"localhost"``
|
||||
|
||||
`SNAPPASS_REDIS_DB` is the database that you want to use on this redis server. Defaults to db 0
|
||||
``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
|
||||
|
||||
``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
|
||||
------
|
||||
|
@ -102,4 +288,17 @@ 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.6), 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.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.
|
||||
|
||||
|
||||
We're Hiring!
|
||||
-------------
|
||||
|
||||
Are you really excited about open-source and great software engineering?
|
||||
`Pinterest is hiring <https://careers.pinterest.com>`_!
|
||||
|
|
10
babel.cfg
Normal file
10
babel.cfg
Normal file
|
@ -0,0 +1,10 @@
|
|||
# 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,3 +1,9 @@
|
|||
coverage==4.2
|
||||
flake8==3.0.4
|
||||
tox==2.3.1
|
||||
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
|
||||
|
|
|
@ -2,17 +2,44 @@ version: '2'
|
|||
|
||||
services:
|
||||
|
||||
snappass:
|
||||
build: .
|
||||
image: pinterest/snappass
|
||||
ports:
|
||||
- "5000:5000"
|
||||
stop_signal: SIGINT
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- NO_SSL=True
|
||||
depends_on:
|
||||
- redis
|
||||
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
|
||||
|
||||
redis:
|
||||
image: "redis:latest"
|
||||
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
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
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
|
||||
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
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
[bumpversion]
|
||||
current_version = 1.3.0
|
||||
current_version = 1.6.2
|
||||
commit = True
|
||||
tag = True
|
||||
files = setup.py snappass/__init__.py
|
||||
files = setup.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.3.0',
|
||||
version='1.6.2',
|
||||
description="It's like SnapChat... for Passwords.",
|
||||
long_description=(open('README.rst').read() + '\n\n' +
|
||||
open('AUTHORS.rst').read()),
|
||||
|
@ -18,6 +18,7 @@ setup(
|
|||
],
|
||||
},
|
||||
include_package_data=True,
|
||||
python_requires='>=3.8, <4',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Intended Audience :: Developers',
|
||||
|
@ -25,12 +26,10 @@ 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.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||
],
|
||||
zip_safe=False,
|
||||
|
|
|
@ -1,2 +1 @@
|
|||
__author__ = 'davedash'
|
||||
__version__ = '1.3.0'
|
||||
__version__ = '1.5.1'
|
||||
|
|
236
snappass/main.py
236
snappass/main.py
|
@ -1,25 +1,25 @@
|
|||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
import redis
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from flask import abort, Flask, render_template, request
|
||||
from flask import abort, Flask, render_template, request, jsonify, make_response
|
||||
from redis.exceptions import ConnectionError
|
||||
from werkzeug.urls import url_quote_plus
|
||||
from werkzeug.urls import url_unquote_plus
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
TOKEN_SEPARATOR = '~'
|
||||
|
||||
|
||||
# Initialize Flask Application
|
||||
app = Flask(__name__)
|
||||
if os.environ.get('DEBUG'):
|
||||
app.debug = True
|
||||
|
@ -27,7 +27,20 @@ app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key')
|
|||
app.config.update(
|
||||
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'))
|
||||
else:
|
||||
redis_host = os.environ.get('REDIS_HOST', 'localhost')
|
||||
|
@ -35,8 +48,12 @@ else:
|
|||
redis_db = os.environ.get('SNAPPASS_REDIS_DB', 0)
|
||||
redis_client = redis.StrictRedis(
|
||||
host=redis_host, port=redis_port, db=redis_db)
|
||||
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
|
||||
|
||||
TIME_CONVERSION = {'week': 604800, 'day': 86400, 'hour': 3600}
|
||||
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400,
|
||||
'hour': 3600}
|
||||
DEFAULT_API_TTL = 1209600
|
||||
MAX_TTL = DEFAULT_API_TTL
|
||||
|
||||
|
||||
def check_redis_alive(fn):
|
||||
|
@ -51,6 +68,7 @@ def check_redis_alive(fn):
|
|||
sys.exit(0)
|
||||
else:
|
||||
return abort(500)
|
||||
|
||||
return inner
|
||||
|
||||
|
||||
|
@ -86,6 +104,37 @@ 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):
|
||||
"""
|
||||
|
@ -94,7 +143,7 @@ def set_password(password, ttl):
|
|||
Returns a token comprised of the key where the encrypted password
|
||||
is stored, and the decryption key.
|
||||
"""
|
||||
storage_key = uuid.uuid4().hex
|
||||
storage_key = REDIS_PREFIX + uuid.uuid4().hex
|
||||
encrypted_password, encryption_key = encrypt(password)
|
||||
redis_client.setex(storage_key, ttl, encrypted_password)
|
||||
encryption_key = encryption_key.decode('utf-8')
|
||||
|
@ -122,6 +171,12 @@ 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
|
||||
|
@ -145,12 +200,20 @@ def clean_input():
|
|||
return TIME_CONVERSION[time_period], request.form['password']
|
||||
|
||||
|
||||
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', ''))
|
||||
def set_base_url(req):
|
||||
if NO_SSL:
|
||||
if HOST_OVERRIDE:
|
||||
base_url = f'http://{HOST_OVERRIDE}/'
|
||||
else:
|
||||
base_url = req.url_root
|
||||
else:
|
||||
if HOST_OVERRIDE:
|
||||
base_url = f'https://{HOST_OVERRIDE}/'
|
||||
else:
|
||||
base_url = req.url_root.replace("http://", "https://")
|
||||
if URL_PREFIX:
|
||||
base_url = base_url + URL_PREFIX.strip("/") + "/"
|
||||
return base_url
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
|
@ -160,32 +223,139 @@ def index():
|
|||
|
||||
@app.route('/', methods=['POST'])
|
||||
def handle_password():
|
||||
ttl, password = clean_input()
|
||||
token = set_password(password, ttl)
|
||||
|
||||
if NO_SSL:
|
||||
base_url = request.url_root
|
||||
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:
|
||||
base_url = request.url_root.replace("http://", "https://")
|
||||
link = base_url + url_quote_plus(token)
|
||||
return render_template('confirm.html', password_link=link)
|
||||
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'])
|
||||
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):
|
||||
if not request_is_valid(request):
|
||||
abort(404)
|
||||
password_key = url_unquote_plus(password_key)
|
||||
password_key = unquote_plus(password_key)
|
||||
password = get_password(password_key)
|
||||
if not password:
|
||||
abort(404)
|
||||
return render_template('expired.html'), 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='0.0.0.0')
|
||||
app.run(host=os.environ.get('SNAPPASS_BIND_ADDRESS', '0.0.0.0'),
|
||||
port=os.environ.get('SNAPPASS_PORT', 5000))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
4
snappass/static/fontawesome/css/font-awesome.min.css
vendored
Normal file
4
snappass/static/fontawesome/css/font-awesome.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
snappass/static/fontawesome/fonts/FontAwesome.otf
Normal file
BIN
snappass/static/fontawesome/fonts/FontAwesome.otf
Normal file
Binary file not shown.
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.eot
Normal file
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
snappass/static/fontawesome/fonts/fontawesome-webfont.svg
Normal file
2671
snappass/static/fontawesome/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 434 KiB |
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.ttf
Normal file
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.woff
Normal file
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.woff2
Normal file
BIN
snappass/static/fontawesome/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
5
snappass/static/jquery/jquery-1.12.4.min.js
vendored
5
snappass/static/jquery/jquery-1.12.4.min.js
vendored
File diff suppressed because one or more lines are too long
2
snappass/static/jquery/jquery-3.6.0.min.js
vendored
Normal file
2
snappass/static/jquery/jquery-3.6.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
snappass/static/snappass/scripts/preview.js
Normal file
10
snappass/static/snappass/scripts/preview.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
(function () {
|
||||
|
||||
$('#revealSecret').click(function () {
|
||||
var form = $('<form/>')
|
||||
.attr('id', 'revealSecretForm')
|
||||
.attr('method', 'post');
|
||||
form.appendTo($('body'));
|
||||
form.submit();
|
||||
});
|
||||
})();
|
|
@ -1,26 +1,34 @@
|
|||
<!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="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="{{ config.STATIC_URL }}/fontawesome/css/font-awesome.min.css?v=4.7.0" 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-1.12.4.min.js"></script>
|
||||
<script src="{{ config.STATIC_URL }}/jquery/jquery-3.6.0.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>
|
||||
|
|
11
snappass/templates/expired.html
Normal file
11
snappass/templates/expired.html
Normal file
|
@ -0,0 +1,11 @@
|
|||
{% 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 %}
|
||||
|
|
23
snappass/templates/preview.html
Normal file
23
snappass/templates/preview.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{% 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,26 +3,27 @@
|
|||
{% 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">
|
||||
<form role="form" id="password_create" method="post" autocomplete="off">
|
||||
<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"></textarea>
|
||||
<textarea rows="10" cols="50" id="password" name="password" autofocus="true" class="form-control" placeholder="{{ _('SnapPass allows you to share secrets in a secure, ephemeral way. Input a single or multi-line secret, its expiration time, and click Generate URL. Share the one-time use URL with your intended recipient.') }}" aria-describedby="basic-addon1" autocomplete="off" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-2 margin-bottom-10">
|
||||
<select class="form-control" name="ttl">
|
||||
<option value="Week">Week</option>
|
||||
<option value="Day">Day</option>
|
||||
<option value="Hour">Hour</option>
|
||||
<option value="Two Weeks">{{ _('Two Weeks') }}</option>
|
||||
<option value="Week" selected="selected">{{ _('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>
|
||||
|
|
131
snappass/translations/de/LC_MESSAGES/messages.po
Normal file
131
snappass/translations/de/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,131 @@
|
|||
# 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"
|
||||
|
129
snappass/translations/es/LC_MESSAGES/messages.po
Normal file
129
snappass/translations/es/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,129 @@
|
|||
# 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"
|
||||
|
128
snappass/translations/nl/LC_MESSAGES/messages.po
Normal file
128
snappass/translations/nl/LC_MESSAGES/messages.po
Normal file
|
@ -0,0 +1,128 @@
|
|||
# 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"
|
||||
|
273
tests.py
273
tests.py
|
@ -1,10 +1,16 @@
|
|||
import re
|
||||
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
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
import snappass.main as snappass
|
||||
|
@ -14,19 +20,20 @@ __author__ = 'davedash'
|
|||
|
||||
class SnapPassTestCase(TestCase):
|
||||
|
||||
@mock.patch('redis.client.StrictRedis', FakeStrictRedis)
|
||||
def test_get_password(self):
|
||||
password = "melatonin overdose 1337!$"
|
||||
key = snappass.set_password(password, 30)
|
||||
self.assertEqual(password, snappass.get_password(key))
|
||||
# Assert that we can't look this up a second time.
|
||||
self.assertEqual(None, snappass.get_password(key))
|
||||
self.assertIsNone(snappass.get_password(key))
|
||||
|
||||
def test_password_is_not_stored_in_plaintext(self):
|
||||
password = "trustno1"
|
||||
token = snappass.set_password(password, 30)
|
||||
redis_key = token.split(snappass.TOKEN_SEPARATOR)[0]
|
||||
stored_password_text = snappass.redis_client.get(redis_key).decode('utf-8')
|
||||
self.assertFalse(password in stored_password_text)
|
||||
self.assertNotIn(password, stored_password_text)
|
||||
|
||||
def test_returned_token_format(self):
|
||||
password = "trustsome1"
|
||||
|
@ -34,7 +41,7 @@ class SnapPassTestCase(TestCase):
|
|||
token_fragments = token.split(snappass.TOKEN_SEPARATOR)
|
||||
self.assertEqual(2, len(token_fragments))
|
||||
redis_key, encryption_key = token_fragments
|
||||
self.assertEqual(32, len(redis_key))
|
||||
self.assertEqual(32 + len(snappass.REDIS_PREFIX), len(redis_key))
|
||||
try:
|
||||
Fernet(encryption_key.encode('utf-8'))
|
||||
except ValueError:
|
||||
|
@ -91,7 +98,7 @@ class SnapPassTestCase(TestCase):
|
|||
password = 'open sesame'
|
||||
key = snappass.set_password(password, 1)
|
||||
time.sleep(1.5)
|
||||
self.assertEqual(None, snappass.get_password(key))
|
||||
self.assertIsNone(snappass.get_password(key))
|
||||
|
||||
|
||||
class SnapPassRoutesTestCase(TestCase):
|
||||
|
@ -100,32 +107,250 @@ class SnapPassRoutesTestCase(TestCase):
|
|||
snappass.app.config['TESTING'] = True
|
||||
self.app = snappass.app.test_client()
|
||||
|
||||
def test_show_password(self):
|
||||
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.assertTrue(password in rv.get_data(as_text=True))
|
||||
self.assertNotIn(password, 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"
|
||||
def test_show_password(self):
|
||||
password = "I like novelty kitten statues!"
|
||||
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/;)",
|
||||
]
|
||||
rv = self.app.post('/{0}'.format(key))
|
||||
self.assertIn(password, rv.get_data(as_text=True))
|
||||
|
||||
for ua in a_few_sneaky_bots:
|
||||
rv = self.app.get('/{0}'.format(key), headers={ 'User-Agent': ua })
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
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_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')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
14
tox.ini
14
tox.ini
|
@ -1,15 +1,15 @@
|
|||
[tox]
|
||||
envlist = py27, py34, py35, py36, flake8
|
||||
envlist = py38, py39, py310, flake8
|
||||
|
||||
[testenv]
|
||||
deps =
|
||||
pytest
|
||||
pytest-cov
|
||||
setenv =
|
||||
MOCK_REDIS = 1
|
||||
commands =
|
||||
pip install -r requirements.txt
|
||||
py.test --junitxml=junit-{envname}.xml --cov-report xml tests.py
|
||||
pip install -r dev-requirements.txt
|
||||
pytest --cov=snappass --cov-report=term-missing tests.py
|
||||
|
||||
[testenv:flake8]
|
||||
commands =
|
||||
pip install flake8
|
||||
flake8 snappass/
|
||||
pip install -r dev-requirements.txt
|
||||
flake8
|
||||
|
|
Loading…
Add table
Reference in a new issue