From eefe2bdc76106feef9b3d8f382bbc9e506c7789a Mon Sep 17 00:00:00 2001 From: Dave Dash Date: Sat, 5 Oct 2013 14:12:31 -0700 Subject: [PATCH] Prepare snappass for distribution. --- .bumpversion.cfg | 5 + .gitignore | 5 + .travis.yml | 8 ++ AUTHORS.rst | 6 + CONTRIBUTING.rst | 126 ++++++++++++++++++ LICENSE | 21 +++ MANIFEST.in | 3 + README | 1 - README.rst | 61 +++++++++ requirements.txt | 6 + setup.py | 36 +++++ snappass/__init__.py | 1 + app.py => snappass/main.py | 50 ++++--- .../bootstrap/css/bootstrap-responsive.css | 0 .../css/bootstrap-responsive.min.css | 0 .../static}/bootstrap/css/bootstrap.css | 0 .../static}/bootstrap/css/bootstrap.min.css | 0 .../img/glyphicons-halflings-white.png | Bin .../bootstrap/img/glyphicons-halflings.png | Bin .../static}/bootstrap/js/bootstrap.js | 0 .../static}/bootstrap/js/bootstrap.min.js | 0 {templates => snappass/templates}/base.html | 0 .../templates}/confirm.html | 0 .../templates}/password.html | 0 .../templates}/set_password.html | 0 tests.py | 62 +++++++++ tox.ini | 10 ++ 27 files changed, 384 insertions(+), 17 deletions(-) create mode 100644 .bumpversion.cfg create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 AUTHORS.rst create mode 100644 CONTRIBUTING.rst create mode 100644 LICENSE create mode 100644 MANIFEST.in delete mode 100644 README create mode 100644 README.rst create mode 100644 requirements.txt create mode 100644 setup.py create mode 100644 snappass/__init__.py rename app.py => snappass/main.py (59%) rename {static => snappass/static}/bootstrap/css/bootstrap-responsive.css (100%) rename {static => snappass/static}/bootstrap/css/bootstrap-responsive.min.css (100%) rename {static => snappass/static}/bootstrap/css/bootstrap.css (100%) rename {static => snappass/static}/bootstrap/css/bootstrap.min.css (100%) rename {static => snappass/static}/bootstrap/img/glyphicons-halflings-white.png (100%) rename {static => snappass/static}/bootstrap/img/glyphicons-halflings.png (100%) rename {static => snappass/static}/bootstrap/js/bootstrap.js (100%) rename {static => snappass/static}/bootstrap/js/bootstrap.min.js (100%) rename {templates => snappass/templates}/base.html (100%) rename {templates => snappass/templates}/confirm.html (100%) rename {templates => snappass/templates}/password.html (100%) rename {templates => snappass/templates}/set_password.html (100%) create mode 100644 tests.py create mode 100644 tox.ini diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..095cb45 --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,5 @@ +[bumpversion] +files = setup.py +commit = True +tag = True +current_version = 0.1.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2950c9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.tox +.coverage +.project +*.rdb +junit*xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..51b7061 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: 2.7 +install: + - pip install tox +script: + - tox +services: + - redis-server diff --git a/AUTHORS.rst b/AUTHORS.rst new file mode 100644 index 0000000..0a79475 --- /dev/null +++ b/AUTHORS.rst @@ -0,0 +1,6 @@ +Credits +======= + +"snappass" is originally written and by Owen Coutts and Ryan Park. + +It is currently maintained by Dave Dash and Pinterest. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..0d05ba5 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,126 @@ +============ +Contributing +============ + +Contributions are welcome, and they are greatly appreciated! Every +little bit helps, and credit will always be given. + +You can contribute in many ways: + +Types of Contributions +---------------------- + +Report Bugs +~~~~~~~~~~~ + +Report bugs at https://github.com/pinterest/snappass/issues. + +If you are reporting a bug, please include: + +* Your operating system name and version (if relevant). +* Any details about your local setup that might be helpful in troubleshooting. +* If you can, provide detailed steps to reproduce the bug. +* If you don't have steps to reproduce the bug, just note your observations in + as much detail as you can. Questions to start a discussion about the issue + are welcome. + +Python 3.3 Support +~~~~~~~~~~~~~~~~~~ + +We'd love for ``tox -e py33`` to work and would welcome anybody who can help +make that a reality. + +Fix Bugs +~~~~~~~~ + +Look through the GitHub issues for bugs. Anything tagged with "bug" +is open to whoever wants to implement it. + +Implement Features +~~~~~~~~~~~~~~~~~~ + +Look through the GitHub issues for features. Anything tagged with "enhancement" +is open to whoever wants to implement it. + + +Write Documentation +~~~~~~~~~~~~~~~~~~~ + +Snappass could always use better documentation, whether as part of the +official docs, in docstrings, or even on the web in blog posts, articles, and +such. + +Submit Feedback +~~~~~~~~~~~~~~~ + +The best way to send feedback is to file an issue at +https://github.com/pinterest/snappass/issues. + +If you are proposing a feature: + +* Explain in detail how it would work. +* Note that this project has an intentionally narrow scope. + Our target users are small organizations that really need a + quick and dirty way to exchange secrets. +* Remember that this is a volunteer-driven project, and that contributions + are welcome :) + + +Setting Up the Code for Local Development +----------------------------------------- + +Here's how to set up `snappass` for local development. + +1. Fork the `snappass` repo on GitHub. +2. Clone your fork locally:: + + $ git clone git@github.com:your_name_here/snappass.git + +3. Install your local copy into a ``virtualenv``. Assuming you have + ``virtualenvwrapper`` installed, this is how you set up your fork for local + development:: + + $ mkvirtualenv snappass + $ cd snappass/ + $ python setup.py develop + +4. Create a branch for local development:: + + $ git checkout -b name-of-your-bugfix-or-feature + + Now you can make your changes locally. + +5. When you're done making changes, check that your changes pass the tests and + flake8:: + + $ flake8 snappass tests + $ tox + +6. 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 + +7. Check that the test coverage hasn't dropped:: + + coverage run --source snappass setup.py tests + coverage report -m + coverage html + +8. Submit a pull request through the GitHub website. + +Pull Request Guidelines +----------------------- + +Before you submit a pull request, check that it meets these guidelines: + +1. The pull request should include tests. +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.7 and ideally 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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fa6a9b5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2012-2013 Pinterest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..ec67e98 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include *.rst LICENSE +recursive-include snappass/static * +recursive-include snappass/templates * diff --git a/README b/README deleted file mode 100644 index 636cc31..0000000 --- a/README +++ /dev/null @@ -1 +0,0 @@ -Redis frontend to securley share passwords diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..500e8e1 --- /dev/null +++ b/README.rst @@ -0,0 +1,61 @@ +======== +SnapPass +======== + +.. image:: https://travis-ci.org/pinterest/snappass.png + + +It's like SnapChat... for Passwords. + +This is a webapp that lets you share passwords securely. + +Let's say you have a password. You want to give it to your coworker, Jane. +You could email it to her, but then it's in her email, which might be backed up, +and probably is in some storage device controlled by the NSA. + +You could send it to her over chat, but chances are Jane logs all her messages +because she uses Google Talk, and Google Talk logs everything. + +You could write it down, but you can't find a pen, and there's way too many +characters because your Security Person, Paul, is paranoid. + +So we build 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. +If the NSA gets a hold of the link, and they look at the password... well they +have the password. Also, Jane can't get the password, but now Jane knows that +not only is someone looking in her email, they are clicking on links. + +Anyway, this took us very little time to write, but we figure we'd save you the +trouble of writing it yourself, because maybe you are busy and have other things +to do. Enjoy. + +Requirements +------------ + +* Redis. +* Python 2.6, 2.7 or 3.3. + +Installation +------------ + +:: + + $ pip install snappass + $ snappass + * Running on http://0.0.0.0:5000/ + * Restarting with reloader + +Configuration +------------- + +You can configure the following via environment variables. + +`SECRET_KEY` this should be a unique key that's used to sign key. This should +be kept secret. See the `Flask Documentation`_ for more information. + +.. _Flask Documentation: http://flask.pocoo.org/docs/quickstart/#sessions + +`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. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..222c1be --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Flask==0.10.1 +Jinja2==2.7.1 +MarkupSafe==0.18 +Werkzeug==0.9.4 +itsdangerous==0.23 +redis==2.8.0 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..edce534 --- /dev/null +++ b/setup.py @@ -0,0 +1,36 @@ +from setuptools import setup + +setup( + name='snappass', + version='0.1.0', + description="It's like SnapChat... for Passwords.", + long_description=(open('README.rst').read() + '\n\n' + + open('AUTHORS.rst').read()), + url='http://github.com/Pinterest/snappass/', + install_requires=['Flask', 'redis'], + license='MIT', + author='Dave Dash', + author_email='dd+github@davedash.com', + packages=['snappass'], + entry_points={ + 'console_scripts': [ + 'snappass = snappass.main:main', + ], + }, + include_package_data=True, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], +) + diff --git a/snappass/__init__.py b/snappass/__init__.py new file mode 100644 index 0000000..845eb13 --- /dev/null +++ b/snappass/__init__.py @@ -0,0 +1 @@ +__author__ = 'davedash' diff --git a/app.py b/snappass/main.py similarity index 59% rename from app.py rename to snappass/main.py index 4aafad1..88f53c9 100644 --- a/app.py +++ b/snappass/main.py @@ -6,13 +6,15 @@ import redis from flask import abort, Flask, render_template, request -application = Flask(__name__) -application.secret_key = os.environ.get('SECRET_KEY', 'Secret Key') -application.config.update(dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) +NO_SSL = os.environ.get('NO_SSL', False) +app = Flask(__name__) +app.secret_key = os.environ.get('SECRET_KEY', 'Secret Key') +app.config.update( + dict(STATIC_URL=os.environ.get('STATIC_URL', 'static'))) -id = lambda: uuid.uuid4().get_hex() +id_ = lambda: uuid.uuid4().hex redis_host = os.environ.get('REDIS_HOST', 'localhost') -r = redis.StrictRedis(host=redis_host, port=6379, db=0) +redis_client = redis.StrictRedis(host=redis_host, port=6379, db=0) time_conversion = { 'week': 604800, @@ -20,17 +22,20 @@ time_conversion = { 'hour': 3600 } + def set_password(password, ttl): - key = id() - r.set(key, password) - r.expire(key, ttl) + key = id_() + redis_client.set(key, password) + redis_client.expire(key, ttl) return key + def get_password(key): - password = r.get(key) - r.delete(key) + password = redis_client.get(key) + redis_client.delete(key) return password + def clean_input(): """ Make sure we're not getting bad data from the front end, @@ -47,19 +52,27 @@ def clean_input(): abort(400) return time_conversion[time_period], request.form['password'] - -@application.route('/', methods=['GET']) + + +@app.route('/', methods=['GET']) def index(): return render_template('set_password.html') -@application.route('/', methods=['POST']) + +@app.route('/', methods=['POST']) def handle_password(): ttl, password = clean_input() key = set_password(password, ttl) - link = request.url_root.replace("http://", "https://") + key + + if NO_SSL: + base_url = request.url_root + else: + base_url = request.url_root.replace("http://", "https://") + link = base_url + key return render_template('confirm.html', password_link=link) -@application.route('/', methods=['GET']) + +@app.route('/', methods=['GET']) def show_password(password_key): password = get_password(password_key) if not password: @@ -67,5 +80,10 @@ def show_password(password_key): return render_template('password.html', password=password) + +def main(): + app.run(host='0.0.0.0', debug=True) + + if __name__ == '__main__': - application.run(host='0.0.0.0', debug=True) + main() diff --git a/static/bootstrap/css/bootstrap-responsive.css b/snappass/static/bootstrap/css/bootstrap-responsive.css similarity index 100% rename from static/bootstrap/css/bootstrap-responsive.css rename to snappass/static/bootstrap/css/bootstrap-responsive.css diff --git a/static/bootstrap/css/bootstrap-responsive.min.css b/snappass/static/bootstrap/css/bootstrap-responsive.min.css similarity index 100% rename from static/bootstrap/css/bootstrap-responsive.min.css rename to snappass/static/bootstrap/css/bootstrap-responsive.min.css diff --git a/static/bootstrap/css/bootstrap.css b/snappass/static/bootstrap/css/bootstrap.css similarity index 100% rename from static/bootstrap/css/bootstrap.css rename to snappass/static/bootstrap/css/bootstrap.css diff --git a/static/bootstrap/css/bootstrap.min.css b/snappass/static/bootstrap/css/bootstrap.min.css similarity index 100% rename from static/bootstrap/css/bootstrap.min.css rename to snappass/static/bootstrap/css/bootstrap.min.css diff --git a/static/bootstrap/img/glyphicons-halflings-white.png b/snappass/static/bootstrap/img/glyphicons-halflings-white.png similarity index 100% rename from static/bootstrap/img/glyphicons-halflings-white.png rename to snappass/static/bootstrap/img/glyphicons-halflings-white.png diff --git a/static/bootstrap/img/glyphicons-halflings.png b/snappass/static/bootstrap/img/glyphicons-halflings.png similarity index 100% rename from static/bootstrap/img/glyphicons-halflings.png rename to snappass/static/bootstrap/img/glyphicons-halflings.png diff --git a/static/bootstrap/js/bootstrap.js b/snappass/static/bootstrap/js/bootstrap.js similarity index 100% rename from static/bootstrap/js/bootstrap.js rename to snappass/static/bootstrap/js/bootstrap.js diff --git a/static/bootstrap/js/bootstrap.min.js b/snappass/static/bootstrap/js/bootstrap.min.js similarity index 100% rename from static/bootstrap/js/bootstrap.min.js rename to snappass/static/bootstrap/js/bootstrap.min.js diff --git a/templates/base.html b/snappass/templates/base.html similarity index 100% rename from templates/base.html rename to snappass/templates/base.html diff --git a/templates/confirm.html b/snappass/templates/confirm.html similarity index 100% rename from templates/confirm.html rename to snappass/templates/confirm.html diff --git a/templates/password.html b/snappass/templates/password.html similarity index 100% rename from templates/password.html rename to snappass/templates/password.html diff --git a/templates/set_password.html b/snappass/templates/set_password.html similarity index 100% rename from templates/set_password.html rename to snappass/templates/set_password.html diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..b6fc65d --- /dev/null +++ b/tests.py @@ -0,0 +1,62 @@ +import unittest +from unittest import TestCase + +from werkzeug.exceptions import ClientDisconnected + +#noinspection PyPep8Naming +import snappass.main as snappass + +__author__ = 'davedash' + + +class SnapPassTestCase(TestCase): + + def test_set_password(self): + """Ensure we return a 32-bit key.""" + key = snappass.set_password("foo", 30) + self.assertEqual(32, len(key)) + + 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)) + + def test_clean_input(self): + # Test Bad Data + with snappass.app.test_request_context( + "/", data={'password': 'foo', 'ttl': 'bar'}, method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + # No Password + with snappass.app.test_request_context( + "/", method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + # No TTL + with snappass.app.test_request_context( + "/", data={'password': 'foo'}, method='POST'): + self.assertRaises(ClientDisconnected, snappass.clean_input) + + with snappass.app.test_request_context( + "/", data={'password': 'foo', 'ttl': 'hour'}, method='POST'): + self.assertEqual((3600, 'foo'), snappass.clean_input()) + + +class SnapPassRoutesTestCase(TestCase): + #noinspection PyPep8Naming + def setUp(self): + snappass.app.config['TESTING'] = True + self.app = snappass.app.test_client() + + def test_show_password(self): + password = "I like novelty kitten statues!" + key = snappass.set_password(password, 30) + rv = self.app.get('/{}'.format(key)) + self.assertIn(password, rv.data) + + +if __name__ == '__main__': + unittest.main() + diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..5d83da1 --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = py27 + +[testenv] +deps = + pytest + pytest-cov +commands = + pip install -r requirements.txt --use-wheel + py.test --junitxml=junit-{envname}.xml --cov-report xml tests.py