Compare commits
140 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 |
34 changed files with 1181 additions and 146 deletions
|
@ -1,6 +0,0 @@
|
||||||
[bumpversion]
|
|
||||||
files = setup.py
|
|
||||||
commit = True
|
|
||||||
tag = True
|
|
||||||
current_version = 1.5.0
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
CONTRIBUTING.rst
|
CONTRIBUTING.rst
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
Dockerfile
|
Dockerfile
|
||||||
requirements.txt
|
|
||||||
tests.py
|
tests.py
|
||||||
tox.ini
|
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
|
.project
|
||||||
*.rdb
|
*.rdb
|
||||||
junit*xml
|
junit*xml
|
||||||
|
@ -50,3 +51,7 @@ htmlcov/
|
||||||
# virtualenv
|
# virtualenv
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
|
|
||||||
|
# Translation catalogs
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
17
.travis.yml
17
.travis.yml
|
@ -1,17 +0,0 @@
|
||||||
branches:
|
|
||||||
only:
|
|
||||||
- "master"
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "2.7"
|
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
|
||||||
- "3.7"
|
|
||||||
- "3.8"
|
|
||||||
cache: pip
|
|
||||||
install:
|
|
||||||
- pip install tox-travis
|
|
||||||
script:
|
|
||||||
- tox
|
|
||||||
services:
|
|
||||||
- redis-server
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
Version 1.6.0
|
||||||
|
-------------
|
||||||
|
* Drop support for officially unsupported Python versions (< Python 3.7)
|
||||||
|
|
||||||
Version 1.5.1
|
Version 1.5.1
|
||||||
-------------
|
-------------
|
||||||
* The ``HOST_OVERRIDE`` environment variable can be used to override the base URL. Useful when behind a reverseproxy.
|
* The ``HOST_OVERRIDE`` environment variable can be used to override the base URL. Useful when behind a reverseproxy.
|
||||||
|
|
|
@ -125,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
|
2. If the pull request adds functionality, the docs should be updated. Put
|
||||||
your new functionality into a function with a docstring, and add the
|
your new functionality into a function with a docstring, and add the
|
||||||
feature to the list in README.rst.
|
feature to the list in README.rst.
|
||||||
3. The pull request should work for Python 2.6, 2.7 and 3.3+. Check
|
3. The pull request should work on all supported Python versions.
|
||||||
`Travis`_ and make sure that
|
|
||||||
the tests pass for all supported Python versions.
|
|
||||||
|
|
||||||
.. _Travis: https://travis-ci.org/pinterest/snappass/pull_requests
|
|
||||||
|
|
|
@ -8,9 +8,13 @@ RUN groupadd -r snappass && \
|
||||||
|
|
||||||
WORKDIR $APP_DIR
|
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"]
|
COPY ["./snappass", "$APP_DIR/snappass"]
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
RUN pybabel compile -d snappass/translations
|
||||||
|
|
||||||
RUN python setup.py install && \
|
RUN python setup.py install && \
|
||||||
chown -R snappass $APP_DIR && \
|
chown -R snappass $APP_DIR && \
|
||||||
chgrp -R snappass $APP_DIR
|
chgrp -R snappass $APP_DIR
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
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
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
include *.rst LICENSE
|
include *.rst LICENSE
|
||||||
recursive-include snappass/static *
|
recursive-include snappass/static *
|
||||||
recursive-include snappass/templates *
|
recursive-include snappass/templates *
|
||||||
|
recursive-include snappass/translations *
|
||||||
|
|
188
README.rst
188
README.rst
|
@ -2,16 +2,12 @@
|
||||||
SnapPass
|
SnapPass
|
||||||
========
|
========
|
||||||
|
|
||||||
|pypi| |build|
|
|pypi|
|
||||||
|
|
||||||
.. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg
|
.. |pypi| image:: https://img.shields.io/pypi/v/snappass.svg
|
||||||
:target: https://pypi.python.org/pypi/snappass
|
:target: https://pypi.python.org/pypi/snappass
|
||||||
:alt: Latest version released on PyPI
|
:alt: Latest version released on PyPI
|
||||||
|
|
||||||
.. |build| image:: https://travis-ci.org/pinterest/snappass.svg
|
|
||||||
:target: https://travis-ci.org/pinterest/snappass
|
|
||||||
:alt: Build status
|
|
||||||
|
|
||||||
It's like SnapChat... for passwords.
|
It's like SnapChat... for passwords.
|
||||||
|
|
||||||
This is a web app that lets you share passwords securely.
|
This is a web app that lets you share passwords securely.
|
||||||
|
@ -51,7 +47,7 @@ Requirements
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* `Redis`_
|
* `Redis`_
|
||||||
* Python 2.7+ or 3.5+
|
* Python 3.8+
|
||||||
|
|
||||||
.. _Redis: https://redis.io/
|
.. _Redis: https://redis.io/
|
||||||
|
|
||||||
|
@ -100,6 +96,186 @@ need to change this.
|
||||||
|
|
||||||
``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com``
|
``HOST_OVERRIDE``: (optional) Used to override the base URL if the app is unaware. Useful when running behind reverse proxies like an identity-aware SSO. Example: ``sub.domain.com``
|
||||||
|
|
||||||
|
``SNAPPASS_BIND_ADDRESS``: (optional) Used to override the default bind address of 0.0.0.0 for flask app Example: ``127.0.0.1``
|
||||||
|
|
||||||
|
``SNAPPASS_PORT``: (optional) Used to override the default port of 5000 Example: ``6000``
|
||||||
|
|
||||||
|
APIs
|
||||||
|
----
|
||||||
|
|
||||||
|
SnapPass has 2 APIs :
|
||||||
|
1. A simple API : That can be used to create passwords links, and then share them with users
|
||||||
|
2. A more REST-y API : Which facilitate programmatic interactions with SnapPass, without having to parse HTML content when retrieving the password
|
||||||
|
|
||||||
|
Simple API
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
The advantage of using the simple API is that you can create a password and retrieve the link without having to open the web interface. This is useful if you want to embed it in a script or use it in a CI/CD pipeline.
|
||||||
|
|
||||||
|
To create a password, send a POST request to ``/api/set_password`` like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/set_password/
|
||||||
|
|
||||||
|
This will return a JSON response with the password link:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"link": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
|
||||||
|
"ttl":1209600
|
||||||
|
}
|
||||||
|
|
||||||
|
the default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/set_password/
|
||||||
|
|
||||||
|
|
||||||
|
REST API
|
||||||
|
^^^^^^^^
|
||||||
|
|
||||||
|
The advantage of using the REST API is that you can fully manage the lifecycle of the password stored in SnapPass without having to interact with any web user interface.
|
||||||
|
|
||||||
|
This is useful if you want to embed it in a script, use it in a CI/CD pipeline or share it between multiple client applications.
|
||||||
|
|
||||||
|
Create a password
|
||||||
|
"""""""""""""""""
|
||||||
|
|
||||||
|
To create a password, send a POST request to ``/api/v2/passwords`` like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar"}' http://localhost:5000/api/v2/passwords
|
||||||
|
|
||||||
|
This will return a JSON response with a token and the password link:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"token": "snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY=",
|
||||||
|
"links": [{
|
||||||
|
"rel": "self",
|
||||||
|
"href": "http://127.0.0.1:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
|
||||||
|
},{
|
||||||
|
"rel": "web-view",
|
||||||
|
"href": "http://127.0.0.1:5000/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D",
|
||||||
|
}],
|
||||||
|
"ttl":1209600
|
||||||
|
}
|
||||||
|
|
||||||
|
The default TTL is 2 weeks (1209600 seconds), but you can override it by adding a expiration parameter:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X POST -H "Content-Type: application/json" -d '{"password": "foobar", "ttl": 3600 }' http://localhost:5000/api/v2/passwords
|
||||||
|
|
||||||
|
If the password is null or empty, and the TTL is larger than the max TTL of the application, the API will return an error like this:
|
||||||
|
|
||||||
|
|
||||||
|
Otherwise, the API will return a 404 (Not Found) response like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"invalid-params": [{
|
||||||
|
"name": "password",
|
||||||
|
"reason": "The password is required and should not be null or empty."
|
||||||
|
}, {
|
||||||
|
"name": "ttl",
|
||||||
|
"reason": "The specified TTL is longer than the maximum supported."
|
||||||
|
}],
|
||||||
|
"title": "The password and/or the TTL are invalid.",
|
||||||
|
"type": "https://127.0.0.1:5000/set-password-validation-error"
|
||||||
|
}
|
||||||
|
|
||||||
|
Check if a password exists
|
||||||
|
""""""""""""""""""""""""""
|
||||||
|
|
||||||
|
To check if a password exists, send a HEAD request to ``/api/v2/passwords/<token>``, where ``<token>`` is the token of the API response when a password is created (url encoded), or simply use the `self` link:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl --head http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
|
||||||
|
|
||||||
|
If :
|
||||||
|
- the passwork_key is valid
|
||||||
|
- the password :
|
||||||
|
- exists,
|
||||||
|
- has not been read
|
||||||
|
- is not expired
|
||||||
|
|
||||||
|
Then the API will return a 200 (OK) response like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Server: Werkzeug/3.0.1 Python/3.12.2
|
||||||
|
Date: Fri, 29 Mar 2024 22:15:54 GMT
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Length: 0
|
||||||
|
Connection: close
|
||||||
|
|
||||||
|
Otherwise, the API will return a 404 (Not Found) response like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
HTTP/1.1 404 NOT FOUND
|
||||||
|
Server: Werkzeug/3.0.1 Python/3.12.2
|
||||||
|
Date: Fri, 29 Mar 2024 22:19:29 GMT
|
||||||
|
Content-Type: text/html; charset=utf-8
|
||||||
|
Content-Length: 0
|
||||||
|
Connection: close
|
||||||
|
|
||||||
|
|
||||||
|
Read a password
|
||||||
|
"""""""""""""""
|
||||||
|
|
||||||
|
To read a password, send a GET request to ``/api/v2/passwords/<password_key>``, where ``<password_key>`` is the token of the API response when a password is created, or simply use the `self` link:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ curl -X GET http://localhost:5000/api/v2/passwords/snappassbedf19b161794fd288faec3eba15fa41~hHnILpQ50ZfJc3nurDfHCb_22rBr5gGEya68e_cZOrY%3D
|
||||||
|
|
||||||
|
If :
|
||||||
|
- the token is valid
|
||||||
|
- the password :
|
||||||
|
- exists
|
||||||
|
- has not been read
|
||||||
|
- is not expired
|
||||||
|
|
||||||
|
Then the API will return a 200 (OK) with a JSON response containing the password :
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"password": "foobar"
|
||||||
|
}
|
||||||
|
|
||||||
|
Otherwise, the API will return a 404 (Not Found) response like so:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
{
|
||||||
|
"invalid-params": [{
|
||||||
|
"name": "token"
|
||||||
|
}],
|
||||||
|
"title": "The password doesn't exist.",
|
||||||
|
"type": "https://127.0.0.1:5000/get-password-error"
|
||||||
|
}
|
||||||
|
|
||||||
|
Notes on APIs
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- When using the APIs, you can specify any ttl, as long as it is lower than the default.
|
||||||
|
- The password is passed in the body of the request rather than in the URL. This is to prevent the password from being logged in the server logs.
|
||||||
|
- Depending on the environment you are running it, you might want to expose the ``/api`` endpoint to your internal network only, and put the web interface behind authentication.
|
||||||
|
|
||||||
|
|
||||||
Docker
|
Docker
|
||||||
------
|
------
|
||||||
|
|
||||||
|
|
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,9 +1,9 @@
|
||||||
coverage==4.5.1
|
coverage==7.6.0
|
||||||
fakeredis==0.7.0
|
fakeredis==2.24.1
|
||||||
flake8==3.5.0
|
flake8==7.1.1
|
||||||
freezegun==0.3.15
|
freezegun==1.5.1
|
||||||
mock==2.0.0
|
pytest==8.3.2
|
||||||
nose==1.3.7
|
pytest-cov==5.0.0
|
||||||
pytest-cov==2.5.1
|
tox==4.18.0
|
||||||
pytest==3.6.3
|
bumpversion==0.6.0
|
||||||
tox==3.1.2
|
wheel==0.44.0
|
||||||
|
|
|
@ -2,17 +2,44 @@ version: '2'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
|
|
||||||
snappass:
|
snappass:
|
||||||
build: .
|
build: .
|
||||||
image: pinterest/snappass
|
#image: pinterest/snappass
|
||||||
ports:
|
#ports:
|
||||||
- "5000:5000"
|
# - "5000:5000"
|
||||||
stop_signal: SIGINT
|
stop_signal: SIGINT
|
||||||
environment:
|
environment:
|
||||||
- REDIS_HOST=redis
|
- REDIS_HOST=redis
|
||||||
- NO_SSL=True
|
- NO_SSL=false
|
||||||
depends_on:
|
- SECRET_KEY=${SECRET_KEY}
|
||||||
- redis
|
- VIRTUAL_HOST=share.brothertec.eu
|
||||||
|
- VIRTUAL_PORT=5000
|
||||||
|
- LETSENCRYPT_HOST=share.brothertec.eu
|
||||||
|
- LETSENCRYPT_EMAIL=admin@brothertec.eu
|
||||||
|
|
||||||
redis:
|
restart: unless-stopped
|
||||||
image: "redis:latest"
|
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,13 +1,8 @@
|
||||||
asn1crypto==1.3.0
|
cryptography==43.0.1
|
||||||
cffi==1.14.0
|
Flask==3.0.0
|
||||||
click==7.1.2
|
itsdangerous==2.2.0
|
||||||
cryptography==3.3.2
|
Jinja2==3.1.4
|
||||||
Flask==1.0.2
|
MarkupSafe==2.1.1
|
||||||
idna==2.9
|
redis==5.0.1
|
||||||
itsdangerous==0.24
|
Werkzeug==3.0.3
|
||||||
Jinja2==2.11.3
|
flask-babel
|
||||||
MarkupSafe==1.1.1
|
|
||||||
pycparser==2.20
|
|
||||||
redis==2.10.6
|
|
||||||
six==1.14.0
|
|
||||||
Werkzeug==0.15.6
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
[bumpversion]
|
[bumpversion]
|
||||||
current_version = 1.5.1
|
current_version = 1.6.2
|
||||||
commit = True
|
commit = True
|
||||||
tag = True
|
tag = True
|
||||||
files = setup.py snappass/__init__.py
|
files = setup.py
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
show-source = True
|
show-source = True
|
||||||
max-line-length = 120
|
max-line-length = 120
|
||||||
|
|
||||||
|
|
11
setup.py
11
setup.py
|
@ -2,7 +2,7 @@ from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='snappass',
|
name='snappass',
|
||||||
version='1.5.1',
|
version='1.6.2',
|
||||||
description="It's like SnapChat... for Passwords.",
|
description="It's like SnapChat... for Passwords.",
|
||||||
long_description=(open('README.rst').read() + '\n\n' +
|
long_description=(open('README.rst').read() + '\n\n' +
|
||||||
open('AUTHORS.rst').read()),
|
open('AUTHORS.rst').read()),
|
||||||
|
@ -18,7 +18,7 @@ setup(
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4',
|
python_requires='>=3.8, <4',
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
@ -26,13 +26,10 @@ setup(
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
'Programming Language :: Python :: 2',
|
|
||||||
'Programming Language :: Python :: 2.7',
|
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.5',
|
|
||||||
'Programming Language :: Python :: 3.6',
|
|
||||||
'Programming Language :: Python :: 3.7',
|
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.8',
|
||||||
|
'Programming Language :: Python :: 3.9',
|
||||||
|
'Programming Language :: Python :: 3.10',
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
||||||
],
|
],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
|
|
199
snappass/main.py
199
snappass/main.py
|
@ -5,18 +5,20 @@ import uuid
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from flask import abort, Flask, render_template, request, jsonify
|
from flask import abort, Flask, render_template, request, jsonify, make_response
|
||||||
from redis.exceptions import ConnectionError
|
from redis.exceptions import ConnectionError
|
||||||
from werkzeug.urls import url_quote_plus
|
from urllib.parse import quote_plus
|
||||||
from werkzeug.urls import url_unquote_plus
|
from urllib.parse import unquote_plus
|
||||||
|
from urllib.parse import urljoin
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
# _ is required to get the Jinja templates translated
|
||||||
|
from flask_babel import Babel, _ # noqa: F401
|
||||||
|
|
||||||
NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False')))
|
NO_SSL = bool(strtobool(os.environ.get('NO_SSL', 'False')))
|
||||||
URL_PREFIX = os.environ.get('URL_PREFIX', None)
|
URL_PREFIX = os.environ.get('URL_PREFIX', None)
|
||||||
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
|
HOST_OVERRIDE = os.environ.get('HOST_OVERRIDE', None)
|
||||||
TOKEN_SEPARATOR = '~'
|
TOKEN_SEPARATOR = '~'
|
||||||
|
|
||||||
|
|
||||||
# Initialize Flask Application
|
# Initialize Flask Application
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
if os.environ.get('DEBUG'):
|
if os.environ.get('DEBUG'):
|
||||||
|
@ -25,9 +27,18 @@ app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key')
|
||||||
app.config.update(
|
app.config.update(
|
||||||
dict(STATIC_URL=os.environ.get('STATIC_URL', 'static')))
|
dict(STATIC_URL=os.environ.get('STATIC_URL', 'static')))
|
||||||
|
|
||||||
|
|
||||||
|
# Set up Babel
|
||||||
|
def get_locale():
|
||||||
|
return request.accept_languages.best_match(['en', 'es', 'de', 'nl'])
|
||||||
|
|
||||||
|
|
||||||
|
babel = Babel(app, locale_selector=get_locale)
|
||||||
|
|
||||||
# Initialize Redis
|
# Initialize Redis
|
||||||
if os.environ.get('MOCK_REDIS'):
|
if os.environ.get('MOCK_REDIS'):
|
||||||
from fakeredis import FakeStrictRedis
|
from fakeredis import FakeStrictRedis
|
||||||
|
|
||||||
redis_client = FakeStrictRedis()
|
redis_client = FakeStrictRedis()
|
||||||
elif os.environ.get('REDIS_URL'):
|
elif os.environ.get('REDIS_URL'):
|
||||||
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
|
redis_client = redis.StrictRedis.from_url(os.environ.get('REDIS_URL'))
|
||||||
|
@ -39,7 +50,10 @@ else:
|
||||||
host=redis_host, port=redis_port, db=redis_db)
|
host=redis_host, port=redis_port, db=redis_db)
|
||||||
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
|
REDIS_PREFIX = os.environ.get('REDIS_PREFIX', 'snappass')
|
||||||
|
|
||||||
TIME_CONVERSION = {'two weeks': 1209600, 'week': 604800, 'day': 86400, '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):
|
def check_redis_alive(fn):
|
||||||
|
@ -54,6 +68,7 @@ def check_redis_alive(fn):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
return abort(500)
|
return abort(500)
|
||||||
|
|
||||||
return inner
|
return inner
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,6 +104,37 @@ def parse_token(token):
|
||||||
return storage_key, decryption_key
|
return storage_key, decryption_key
|
||||||
|
|
||||||
|
|
||||||
|
def as_validation_problem(request, problem_type, problem_title, invalid_params):
|
||||||
|
base_url = set_base_url(request)
|
||||||
|
|
||||||
|
problem = {
|
||||||
|
"type": base_url + problem_type,
|
||||||
|
"title": problem_title,
|
||||||
|
"invalid-params": invalid_params
|
||||||
|
}
|
||||||
|
return as_problem_response(problem)
|
||||||
|
|
||||||
|
|
||||||
|
def as_not_found_problem(request, problem_type, problem_title, invalid_params):
|
||||||
|
base_url = set_base_url(request)
|
||||||
|
|
||||||
|
problem = {
|
||||||
|
"type": base_url + problem_type,
|
||||||
|
"title": problem_title,
|
||||||
|
"invalid-params": invalid_params
|
||||||
|
}
|
||||||
|
return as_problem_response(problem, 404)
|
||||||
|
|
||||||
|
|
||||||
|
def as_problem_response(problem, status_code=None):
|
||||||
|
if not isinstance(status_code, int) or not status_code:
|
||||||
|
status_code = 400
|
||||||
|
|
||||||
|
response = make_response(jsonify(problem), status_code)
|
||||||
|
response.headers['Content-Type'] = 'application/problem+json'
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@check_redis_alive
|
@check_redis_alive
|
||||||
def set_password(password, ttl):
|
def set_password(password, ttl):
|
||||||
"""
|
"""
|
||||||
|
@ -154,6 +200,22 @@ def clean_input():
|
||||||
return TIME_CONVERSION[time_period], request.form['password']
|
return TIME_CONVERSION[time_period], request.form['password']
|
||||||
|
|
||||||
|
|
||||||
|
def set_base_url(req):
|
||||||
|
if NO_SSL:
|
||||||
|
if HOST_OVERRIDE:
|
||||||
|
base_url = f'http://{HOST_OVERRIDE}/'
|
||||||
|
else:
|
||||||
|
base_url = req.url_root
|
||||||
|
else:
|
||||||
|
if HOST_OVERRIDE:
|
||||||
|
base_url = f'https://{HOST_OVERRIDE}/'
|
||||||
|
else:
|
||||||
|
base_url = req.url_root.replace("http://", "https://")
|
||||||
|
if URL_PREFIX:
|
||||||
|
base_url = base_url + URL_PREFIX.strip("/") + "/"
|
||||||
|
return base_url
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'])
|
@app.route('/', methods=['GET'])
|
||||||
def index():
|
def index():
|
||||||
return render_template('set_password.html')
|
return render_template('set_password.html')
|
||||||
|
@ -161,31 +223,113 @@ def index():
|
||||||
|
|
||||||
@app.route('/', methods=['POST'])
|
@app.route('/', methods=['POST'])
|
||||||
def handle_password():
|
def handle_password():
|
||||||
ttl, password = clean_input()
|
password = request.form.get('password')
|
||||||
token = set_password(password, ttl)
|
ttl = request.form.get('ttl')
|
||||||
|
if clean_input():
|
||||||
if NO_SSL:
|
ttl = TIME_CONVERSION[ttl.lower()]
|
||||||
if HOST_OVERRIDE:
|
token = set_password(password, ttl)
|
||||||
base_url = f'http://{HOST_OVERRIDE}/'
|
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:
|
else:
|
||||||
base_url = request.url_root
|
return render_template('confirm.html', password_link=link)
|
||||||
else:
|
else:
|
||||||
if HOST_OVERRIDE:
|
abort(500)
|
||||||
base_url = f'https://{HOST_OVERRIDE}/'
|
|
||||||
else:
|
|
||||||
base_url = request.url_root.replace("http://", "https://")
|
@app.route('/api/set_password/', methods=['POST'])
|
||||||
if URL_PREFIX:
|
def api_handle_password():
|
||||||
base_url = base_url + URL_PREFIX.strip("/") + "/"
|
password = request.json.get('password')
|
||||||
link = base_url + url_quote_plus(token)
|
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
|
||||||
if request.accept_mimetypes.accept_json and not request.accept_mimetypes.accept_html:
|
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)
|
return jsonify(link=link, ttl=ttl)
|
||||||
else:
|
else:
|
||||||
return render_template('confirm.html', password_link=link)
|
abort(500)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v2/passwords', methods=['POST'])
|
||||||
|
def api_v2_set_password():
|
||||||
|
password = request.json.get('password')
|
||||||
|
ttl = int(request.json.get('ttl', DEFAULT_API_TTL))
|
||||||
|
|
||||||
|
invalid_params = []
|
||||||
|
|
||||||
|
if not password:
|
||||||
|
invalid_params.append({
|
||||||
|
"name": "password",
|
||||||
|
"reason": "The password is required and should not be null or empty."
|
||||||
|
})
|
||||||
|
|
||||||
|
if not isinstance(ttl, int) or ttl > MAX_TTL:
|
||||||
|
invalid_params.append({
|
||||||
|
"name": "ttl",
|
||||||
|
"reason": "The specified TTL is longer than the maximum supported."
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(invalid_params) > 0:
|
||||||
|
# Return a ProblemDetails expliciting issue with Password and/or TTL
|
||||||
|
return as_validation_problem(
|
||||||
|
request,
|
||||||
|
"set-password-validation-error",
|
||||||
|
"The password and/or the TTL are invalid.",
|
||||||
|
invalid_params
|
||||||
|
)
|
||||||
|
|
||||||
|
token = set_password(password, ttl)
|
||||||
|
url_token = quote_plus(token)
|
||||||
|
base_url = set_base_url(request)
|
||||||
|
api_link = urljoin(base_url, request.path + "/" + url_token)
|
||||||
|
web_link = urljoin(base_url, url_token)
|
||||||
|
response_content = {
|
||||||
|
"token": token,
|
||||||
|
"links": [{
|
||||||
|
"rel": "self",
|
||||||
|
"href": api_link
|
||||||
|
}, {
|
||||||
|
"rel": "web-view",
|
||||||
|
"href": web_link
|
||||||
|
}],
|
||||||
|
"ttl": ttl
|
||||||
|
}
|
||||||
|
return jsonify(response_content)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v2/passwords/<token>', methods=['HEAD'])
|
||||||
|
def api_v2_check_password(token):
|
||||||
|
token = unquote_plus(token)
|
||||||
|
if not password_exists(token):
|
||||||
|
# Return NotFound, to indicate that password does not exists (anymore or at all)
|
||||||
|
return ('', 404)
|
||||||
|
else:
|
||||||
|
# Return OK, to indicate that password still exists
|
||||||
|
return ('', 200)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/v2/passwords/<token>', methods=['GET'])
|
||||||
|
def api_v2_retrieve_password(token):
|
||||||
|
token = unquote_plus(token)
|
||||||
|
password = get_password(token)
|
||||||
|
if not password:
|
||||||
|
# Return NotFound, to indicate that password does not exists (anymore or at all)
|
||||||
|
return as_not_found_problem(
|
||||||
|
request,
|
||||||
|
"get-password-error",
|
||||||
|
"The password doesn't exist.",
|
||||||
|
[{"name": "token"}]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Return OK and the password in JSON message
|
||||||
|
return jsonify(password=password)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<password_key>', methods=['GET'])
|
@app.route('/<password_key>', methods=['GET'])
|
||||||
def preview_password(password_key):
|
def preview_password(password_key):
|
||||||
password_key = url_unquote_plus(password_key)
|
password_key = unquote_plus(password_key)
|
||||||
if not password_exists(password_key):
|
if not password_exists(password_key):
|
||||||
return render_template('expired.html'), 404
|
return render_template('expired.html'), 404
|
||||||
|
|
||||||
|
@ -194,7 +338,7 @@ def preview_password(password_key):
|
||||||
|
|
||||||
@app.route('/<password_key>', methods=['POST'])
|
@app.route('/<password_key>', methods=['POST'])
|
||||||
def show_password(password_key):
|
def show_password(password_key):
|
||||||
password_key = url_unquote_plus(password_key)
|
password_key = unquote_plus(password_key)
|
||||||
password = get_password(password_key)
|
password = get_password(password_key)
|
||||||
if not password:
|
if not password:
|
||||||
return render_template('expired.html'), 404
|
return render_template('expired.html'), 404
|
||||||
|
@ -202,9 +346,16 @@ def show_password(password_key):
|
||||||
return render_template('password.html', password=password)
|
return render_template('password.html', password=password)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/_/_/health', methods=['GET'])
|
||||||
|
@check_redis_alive
|
||||||
|
def health_check():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@check_redis_alive
|
@check_redis_alive
|
||||||
def main():
|
def main():
|
||||||
app.run(host='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__':
|
if __name__ == '__main__':
|
||||||
|
|
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
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="{{ _('en') }}">
|
||||||
<head>
|
<head>
|
||||||
<title>Snappass - Share Secrets</title>
|
<title>{{ _('Snappass - Share Secrets') }}</title>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
@ -13,14 +13,22 @@
|
||||||
<nav class="navbar navbar-default navbar-static-top">
|
<nav class="navbar navbar-default navbar-static-top">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="navbar-header">
|
<div class="navbar-header">
|
||||||
<a class="navbar-brand" href="/">Share Secret</a>
|
<a class="navbar-brand" href="/">{{ _('Share Secret') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
<script src="{{ config.STATIC_URL }}/jquery/jquery-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>
|
<script src="{{ config.STATIC_URL }}/bootstrap/js/bootstrap.min.js"></script>
|
||||||
{% block js %}{% endblock %}
|
{% block js %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<p>
|
||||||
|
<p><strong>no tracking | no logging | no advertising</strong></p>
|
||||||
|
<p>proudly presented by <a href="https://brothertec.eu/" target="_blank">brothertec.eu</a> | <a href="https://datenschutz.brothertec.eu/impressum/" target="_blank">Impressum</a> | <a href="https://datenschutz.brothertec.eu/datenschutzerkl%C3%A4rung/" target="_blank">Datenschutzhinweis</a> | <a href="https://github.com/pinterest/snappass" target="_blank">Code</a></p>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<div class="page-header"><h1>Share Secret Link</h1></div>
|
<div class="page-header"><h1>{{ _('Share Secret Link') }}</h1></div>
|
||||||
<p>The secret has been temporarily saved. Send the following URL to your intended recipient.</p>
|
<p>{{ _('The secret has been temporarily saved. Send the following URL to your intended recipient.') }}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 margin-bottom-10">
|
<div class="col-sm-6 margin-bottom-10">
|
||||||
<input type="text" class="form-control" id="password-link" value="{{ password_link }}" readonly="readonly">
|
<input type="text" class="form-control" id="password-link" value="{{ password_link }}" readonly="readonly">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<button title="Copy to clipboard" type="button" class="btn btn-primary copy-clipboard-btn"
|
<button title="{{ _('Copy to clipboard') }}" type="button" class="btn btn-primary copy-clipboard-btn"
|
||||||
id="copy-clipboard-btn" data-clipboard-target="#password-link"
|
id="copy-clipboard-btn" data-clipboard-target="#password-link"
|
||||||
data-placement='bottom'>
|
data-placement='bottom'>
|
||||||
<i class="fa fa-clipboard"></i>
|
<i class="fa fa-clipboard"></i>
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<div class="page-header"><h1>Secret not found</h1></div>
|
<div class="page-header"><h1>{{ _('Secret not found') }}</h1></div>
|
||||||
<p class="lead">The requested URL was not found on the server. This could be because this URL never contained a secret, or because it expired or was revealed earlier.</p>
|
<p class="lead">{{ _('The requested URL was not found on the server. This could be because this URL never contained a secret, or because it expired or was revealed earlier.') }}</p>
|
||||||
<p class="lead">If this URL was sent to you by someone, make sure to check your spelling or ask the person who sent it to you to send a new secret.</p>
|
<p class="lead">{{ _('If this URL was sent to you by someone, make sure to check your spelling or ask the person who sent it to you to send a new secret.') }}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,22 +3,22 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<div class="page-header"><h1>Secret</h1></div>
|
<div class="page-header"><h1>{{ _('Secret') }}</h1></div>
|
||||||
<p>Save the following secret to a secure location.</p>
|
<p>{{ _('Save the following secret to a secure location.') }}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 margin-bottom-10">
|
<div class="col-sm-6 margin-bottom-10">
|
||||||
<textarea class="form-control" rows="10" cols="50" id="password-text" name="password-text" readonly="readonly">{{ password }}</textarea>
|
<textarea class="form-control" rows="10" cols="50" id="password-text" name="password-text" readonly="readonly">{{ password }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<button title="Copy to clipboard" type="button" class="btn btn-primary copy-clipboard-btn"
|
<button title="{{ _('Copy to clipboard') }}" type="button" class="btn btn-primary copy-clipboard-btn"
|
||||||
id="copy-clipboard-btn" data-clipboard-target="#password-text"
|
id="copy-clipboard-btn" data-clipboard-target="#password-text"
|
||||||
data-placement='bottom'>
|
data-placement='bottom'>
|
||||||
<i class="fa fa-clipboard"></i>
|
<i class="fa fa-clipboard"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.</p>
|
<p>{{ _('The secret has now been permanently deleted from the system, and the URL will no longer work. Refresh this page to verify.') }}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -4,12 +4,12 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Secret</h1>
|
<h1>{{ _('Secret') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="lead">You can only reveal the secret once!</p>
|
<p class="lead">{{ _('You can only reveal the secret once!') }}</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 margin-bottom-10">
|
<div class="col-sm-6 margin-bottom-10">
|
||||||
<button id="revealSecret" type="button" class="btn-lg btn-primary">Reveal secret</button>
|
<button id="revealSecret" type="button" class="btn-lg btn-primary">{{ _('Reveal secret') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
@ -20,4 +20,4 @@
|
||||||
<script src="{{ config.STATIC_URL }}/clipboardjs/clipboard.min.js"></script>
|
<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/clipboard_button.js"></script>
|
||||||
<script src="{{ config.STATIC_URL }}/snappass/scripts/preview.js"></script>
|
<script src="{{ config.STATIC_URL }}/snappass/scripts/preview.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -3,27 +3,27 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section>
|
<section>
|
||||||
<div class="page-header"><h1>Set Secret</h1></div>
|
<div class="page-header"><h1>{{ _('Set Secret') }}</h1></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form role="form" id="password_create" method="post" autocomplete="off">
|
<form role="form" id="password_create" method="post" autocomplete="off">
|
||||||
<div class="col-sm-6 margin-bottom-10">
|
<div class="col-sm-6 margin-bottom-10">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span>
|
<span class="input-group-addon" id="basic-addon1"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span>
|
||||||
<textarea rows="10" cols="50" id="password" name="password" autofocus="true" class="form-control" placeholder="SnapPass allows you to share secrets in a secure, ephemeral way. Input a single or multi-line secret, its expiration time, and click Generate URL. Share the one-time use URL with your intended recipient." aria-describedby="basic-addon1" autocomplete="off" required></textarea>
|
<textarea rows="10" cols="50" id="password" name="password" autofocus="true" class="form-control" placeholder="{{ _('SnapPass allows you to share secrets in a secure, ephemeral way. Input a single or multi-line secret, its expiration time, and click Generate URL. Share the one-time use URL with your intended recipient.') }}" aria-describedby="basic-addon1" autocomplete="off" required></textarea>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-2 margin-bottom-10">
|
<div class="col-sm-2 margin-bottom-10">
|
||||||
<select class="form-control" name="ttl">
|
<select class="form-control" name="ttl">
|
||||||
<option value="Two Weeks">Two Weeks</option>
|
<option value="Two Weeks">{{ _('Two Weeks') }}</option>
|
||||||
<option value="Week" selected="selected">Week</option>
|
<option value="Week" selected="selected">{{ _('Week') }}</option>
|
||||||
<option value="Day">Day</option>
|
<option value="Day">{{ _('Day') }}</option>
|
||||||
<option value="Hour">Hour</option>
|
<option value="Hour">{{ _('Hour') }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-sm-4">
|
<div class="col-sm-4">
|
||||||
<button type="submit" class="btn btn-primary" id="submit">Generate URL</button>
|
<button type="submit" class="btn btn-primary" id="submit">{{ _('Generate URL') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
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"
|
||||||
|
|
209
tests.py
209
tests.py
|
@ -1,16 +1,16 @@
|
||||||
from mock import patch
|
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
import unittest
|
import unittest
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from unittest import mock
|
||||||
|
from urllib.parse import quote
|
||||||
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
from freezegun import freeze_time
|
from freezegun import freeze_time
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from fakeredis import FakeStrictRedis
|
from fakeredis import FakeStrictRedis
|
||||||
from six.moves.urllib.parse import unquote
|
|
||||||
|
|
||||||
# noinspection PyPep8Naming
|
# noinspection PyPep8Naming
|
||||||
import snappass.main as snappass
|
import snappass.main as snappass
|
||||||
|
@ -20,7 +20,7 @@ __author__ = 'davedash'
|
||||||
|
|
||||||
class SnapPassTestCase(TestCase):
|
class SnapPassTestCase(TestCase):
|
||||||
|
|
||||||
@patch('redis.client.StrictRedis', FakeStrictRedis)
|
@mock.patch('redis.client.StrictRedis', FakeStrictRedis)
|
||||||
def test_get_password(self):
|
def test_get_password(self):
|
||||||
password = "melatonin overdose 1337!$"
|
password = "melatonin overdose 1337!$"
|
||||||
key = snappass.set_password(password, 30)
|
key = snappass.set_password(password, 30)
|
||||||
|
@ -107,6 +107,11 @@ class SnapPassRoutesTestCase(TestCase):
|
||||||
snappass.app.config['TESTING'] = True
|
snappass.app.config['TESTING'] = True
|
||||||
self.app = snappass.app.test_client()
|
self.app = snappass.app.test_client()
|
||||||
|
|
||||||
|
def test_health_check(self):
|
||||||
|
response = self.app.get('/_/_/health')
|
||||||
|
self.assertEqual('200 OK', response.status)
|
||||||
|
self.assertEqual('{}', response.get_data(as_text=True).strip())
|
||||||
|
|
||||||
def test_preview_password(self):
|
def test_preview_password(self):
|
||||||
password = "I like novelty kitten statues!"
|
password = "I like novelty kitten statues!"
|
||||||
key = snappass.set_password(password, 30)
|
key = snappass.set_password(password, 30)
|
||||||
|
@ -139,11 +144,15 @@ class SnapPassRoutesTestCase(TestCase):
|
||||||
|
|
||||||
frozen_time.move_to("2020-05-22 12:00:00")
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
self.assertIsNone(snappass.get_password(key))
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
def test_set_password_json(self):
|
def test_set_password_json(self):
|
||||||
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
password = 'my name is my passport. verify me.'
|
password = 'my name is my passport. verify me.'
|
||||||
rv = self.app.post('/', headers={'Accept': 'application/json'}, data={'password': password, 'ttl': 'two weeks'})
|
rv = self.app.post(
|
||||||
|
'/',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
data={'password': password, 'ttl': 'two weeks'},
|
||||||
|
)
|
||||||
|
|
||||||
json_content = rv.get_json()
|
json_content = rv.get_json()
|
||||||
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
||||||
|
@ -155,6 +164,194 @@ class SnapPassRoutesTestCase(TestCase):
|
||||||
frozen_time.move_to("2020-05-22 12:00:00")
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
self.assertIsNone(snappass.get_password(key))
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/set_password/',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password, 'ttl': '1209600'},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
||||||
|
key = unquote(key)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api_default_ttl(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/set_password/',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = re.search(r'https://localhost/([^"]+)', json_content['link']).group(1)
|
||||||
|
key = unquote(key)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api_v2(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password, 'ttl': '1209600'},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api_v2_default_ttl(self):
|
||||||
|
with freeze_time("2020-05-08 12:00:00") as frozen_time:
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 11:59:59")
|
||||||
|
self.assertEqual(snappass.get_password(key), password)
|
||||||
|
|
||||||
|
frozen_time.move_to("2020-05-22 12:00:00")
|
||||||
|
self.assertIsNone(snappass.get_password(key))
|
||||||
|
|
||||||
|
def test_set_password_api_v2_no_password(self):
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': ''},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rv.status_code, 400)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
invalid_params = json_content['invalid-params']
|
||||||
|
self.assertEqual(len(invalid_params), 1)
|
||||||
|
bad_password = invalid_params[0]
|
||||||
|
self.assertEqual(bad_password['name'], 'password')
|
||||||
|
|
||||||
|
def test_set_password_api_v2_too_big_ttl(self):
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password, 'ttl': '1209600000'},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rv.status_code, 400)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
invalid_params = json_content['invalid-params']
|
||||||
|
self.assertEqual(len(invalid_params), 1)
|
||||||
|
bad_ttl = invalid_params[0]
|
||||||
|
self.assertEqual(bad_ttl['name'], 'ttl')
|
||||||
|
|
||||||
|
def test_set_password_api_v2_no_password_and_too_big_ttl(self):
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': '', 'ttl': '1209600000'},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(rv.status_code, 400)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
invalid_params = json_content['invalid-params']
|
||||||
|
self.assertEqual(len(invalid_params), 2)
|
||||||
|
bad_password = invalid_params[0]
|
||||||
|
self.assertEqual(bad_password['name'], 'password')
|
||||||
|
bad_ttl = invalid_params[1]
|
||||||
|
self.assertEqual(bad_ttl['name'], 'ttl')
|
||||||
|
|
||||||
|
def test_check_password_api_v2(self):
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
rvc = self.app.head('/api/v2/passwords/' + quote(key))
|
||||||
|
self.assertEqual(rvc.status_code, 200)
|
||||||
|
|
||||||
|
def test_check_password_api_v2_bad_keys(self):
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
rvc = self.app.head('/api/v2/passwords/' + quote(key[::-1]))
|
||||||
|
self.assertEqual(rvc.status_code, 404)
|
||||||
|
|
||||||
|
def test_retrieve_password_api_v2(self):
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
rvc = self.app.get('/api/v2/passwords/' + quote(key))
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
|
||||||
|
json_content_retrieved = rvc.get_json()
|
||||||
|
retrieved_password = json_content_retrieved['password']
|
||||||
|
self.assertEqual(retrieved_password, password)
|
||||||
|
|
||||||
|
def test_retrieve_password_api_v2_bad_keys(self):
|
||||||
|
password = 'my name is my passport. verify me.'
|
||||||
|
rv = self.app.post(
|
||||||
|
'/api/v2/passwords',
|
||||||
|
headers={'Accept': 'application/json'},
|
||||||
|
json={'password': password},
|
||||||
|
)
|
||||||
|
|
||||||
|
json_content = rv.get_json()
|
||||||
|
key = unquote(json_content['token'])
|
||||||
|
|
||||||
|
rvc = self.app.get('/api/v2/passwords/' + quote(key[::-1]))
|
||||||
|
self.assertEqual(rvc.status_code, 404)
|
||||||
|
|
||||||
|
json_content_retrieved = rvc.get_json()
|
||||||
|
invalid_params = json_content_retrieved['invalid-params']
|
||||||
|
self.assertEqual(len(invalid_params), 1)
|
||||||
|
bad_token = invalid_params[0]
|
||||||
|
self.assertEqual(bad_token['name'], 'token')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
8
tox.ini
8
tox.ini
|
@ -1,5 +1,5 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py27, py34, py35, py36, py37, py38, flake8
|
envlist = py38, py39, py310, flake8
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
setenv =
|
setenv =
|
||||||
|
@ -7,9 +7,9 @@ setenv =
|
||||||
commands =
|
commands =
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
pip install -r dev-requirements.txt
|
pip install -r dev-requirements.txt
|
||||||
pytest --junitxml=junit-{envname}.xml --cov-report xml tests.py
|
pytest --cov=snappass --cov-report=term-missing tests.py
|
||||||
|
|
||||||
[testenv:flake8]
|
[testenv:flake8]
|
||||||
commands =
|
commands =
|
||||||
pip install flake8
|
pip install -r dev-requirements.txt
|
||||||
flake8 snappass/
|
flake8
|
||||||
|
|
Loading…
Add table
Reference in a new issue