diff options
-rw-r--r-- | tests/helpers/client_side_db.py | 161 | ||||
-rwxr-xr-x | tests/helpers/soledad_sync.py | 123 | ||||
-rw-r--r-- | tests/white-box/webapp.rb | 29 |
3 files changed, 250 insertions, 63 deletions
diff --git a/tests/helpers/client_side_db.py b/tests/helpers/client_side_db.py new file mode 100644 index 00000000..d2a4bbcb --- /dev/null +++ b/tests/helpers/client_side_db.py @@ -0,0 +1,161 @@ +import logging +import os +import tempfile +import getpass +import requests +import srp._pysrp as srp +import binascii +import json + +from twisted.internet.defer import inlineCallbacks + +from leap.soledad.client import Soledad + + +""" +Helper functions to give access to client-side Soledad database. +""" + +# create a logger +logger = logging.getLogger(__name__) + +# DEBUG: enable debug logs +# LOG_FORMAT = '%(asctime)s %(message)s' +# logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG) + + +safe_unhexlify = lambda x: binascii.unhexlify(x) if ( + len(x) % 2 == 0) else binascii.unhexlify('0' + x) + + +def _fail(reason): + logger.error('Fail: ' + reason) + exit(2) + + +def _get_api_info(provider): + info = requests.get( + 'https://'+provider+'/provider.json', verify=False).json() + return info['api_uri'], info['api_version'] + + +def _login(username, passphrase, provider, api_uri, api_version): + usr = srp.User(username, passphrase, srp.SHA256, srp.NG_1024) + auth = None + try: + auth = _authenticate(api_uri, api_version, usr).json() + except requests.exceptions.ConnectionError: + _fail('Could not connect to server.') + if 'errors' in auth: + _fail(str(auth['errors'])) + return api_uri, api_version, auth + + +def _authenticate(api_uri, api_version, usr): + api_url = "%s/%s" % (api_uri, api_version) + session = requests.session() + uname, A = usr.start_authentication() + params = {'login': uname, 'A': binascii.hexlify(A)} + init = session.post( + api_url + '/sessions', data=params, verify=False).json() + if 'errors' in init: + _fail('test user not found') + M = usr.process_challenge( + safe_unhexlify(init['salt']), safe_unhexlify(init['B'])) + return session.put(api_url + '/sessions/' + uname, verify=False, + data={'client_auth': binascii.hexlify(M)}) + + +def _get_soledad_info(username, provider, passphrase, basedir): + api_uri, api_version = _get_api_info(provider) + auth = _login(username, passphrase, provider, api_uri, api_version) + # get soledad server url + service_url = '%s/%s/config/soledad-service.json' % \ + (api_uri, api_version) + soledad_hosts = requests.get(service_url, verify=False).json()['hosts'] + hostnames = soledad_hosts.keys() + # allow for choosing the host + host = hostnames[0] + if len(hostnames) > 1: + i = 1 + print "There are many available hosts:" + for h in hostnames: + print " (%d) %s.%s" % (i, h, provider) + i += 1 + choice = raw_input("Choose a host to use (default: 1): ") + if choice != '': + host = hostnames[int(choice) - 1] + server_url = 'https://%s:%d/user-%s' % \ + (soledad_hosts[host]['hostname'], soledad_hosts[host]['port'], + auth[2]['id']) + # get provider ca certificate + ca_cert = requests.get('https://%s/ca.crt' % provider, verify=False).text + cert_file = os.path.join(basedir, 'ca.crt') + with open(cert_file, 'w') as f: + f.write(ca_cert) + return auth[2]['id'], server_url, cert_file, auth[2]['token'] + + +def get_soledad_instance(uuid, passphrase, basedir, server_url, cert_file, + token): + # setup soledad info + logger.info('UUID is %s' % uuid) + logger.info('Server URL is %s' % server_url) + secrets_path = os.path.join( + basedir, '%s.secret' % uuid) + local_db_path = os.path.join( + basedir, '%s.db' % uuid) + # instantiate soledad + return Soledad( + uuid, + unicode(passphrase), + secrets_path=secrets_path, + local_db_path=local_db_path, + server_url=server_url, + cert_file=cert_file, + auth_token=token, + defer_encryption=True) + +def _get_passphrase(args): + passphrase = args.passphrase + if passphrase is None: + passphrase = getpass.getpass( + 'Password for %s@%s: ' % (args.username, args.provider)) + return passphrase + + +def _get_basedir(args): + basedir = args.basedir + if basedir is None: + basedir = tempfile.mkdtemp() + elif not os.path.isdir(basedir): + os.mkdir(basedir) + logger.info('Using %s as base directory.' % basedir) + return basedir + + +@inlineCallbacks +def _export_key(args, km, fname, private=False): + address = args.username + "@" + args.provider + pkey = yield km.get_key( + address, OpenPGPKey, private=private, fetch_remote=False) + with open(args.export_private_key, "w") as f: + f.write(pkey.key_data) + + +@inlineCallbacks +def _export_incoming_messages(soledad, directory): + yield soledad.create_index("by-incoming", "bool(incoming)") + docs = yield soledad.get_from_index("by-incoming", '1') + i = 1 + for doc in docs: + with open(os.path.join(directory, "message_%d.gpg" % i), "w") as f: + f.write(doc.content["_enc_json"]) + i += 1 + + +@inlineCallbacks +def _get_all_docs(soledad): + _, docs = yield soledad.get_all_docs() + for doc in docs: + print json.dumps(doc.content, indent=4) diff --git a/tests/helpers/soledad_sync.py b/tests/helpers/soledad_sync.py index 2fb865fc..a1cea069 100755 --- a/tests/helpers/soledad_sync.py +++ b/tests/helpers/soledad_sync.py @@ -1,78 +1,75 @@ #!/usr/bin/env python +""" +soledad_sync.py -# -# Test Soledad sync -# -# This script performs a slightly modified U1DB sync to the Soledad server and -# returns whether that sync was successful or not. -# -# It takes three arguments: -# -# uuid -- uuid of the user to sync -# token -- a valid session token -# server -- the url of the soledad server we should connect to -# -# For example: -# -# soledad_sync.py f6bef0586fcfdb8705e26a58f2d9e580 uYO-4ucEJFksJ6afjmcYwIyap2vW7bv6uLxk0w_RfCc https://199.119.112.9:2323/user-f6bef0586fcfdb8705e26a58f2d9e580 -# +This script exercises soledad synchronization. +Its exit code is 0 if the sync took place correctly, 1 otherwise. +It takes 5 arguments: + + uuid: uuid of the user to sync + token: a valid session token + server: the url of the soledad server we should connect to + cert_file: the file containing the certificate for the CA that signed the + cert for the soledad server. + password: the password for the user to sync + +__author__: kali@leap.se +""" import os import sys -import traceback import tempfile -import shutil -import u1db -from u1db.remote.http_target import HTTPSyncTarget +# This is needed because the twisted shipped with wheezy is too old +# to do proper ssl verification. +os.environ['SKIP_TWISTED_SSL_CHECK'] = '1' -# -# monkey patch U1DB's HTTPSyncTarget to perform token based auth -# +from twisted.internet import defer, reactor -def set_token_credentials(self, uuid, token): - self._creds = {'token': (uuid, token)} +from client_side_db import get_soledad_instance +from leap.common.events import flags -def _sign_request(self, method, url_query, params): - uuid, token = self._creds['token'] - auth = '%s:%s' % (uuid, token) - return [('Authorization', 'Token %s' % auth.encode('base64')[:-1])] +flags.set_events_enabled(False) -HTTPSyncTarget.set_token_credentials = set_token_credentials -HTTPSyncTarget._sign_request = _sign_request +NUMDOCS = 1 +USAGE = "Usage: %s uuid token server cert_file password" % sys.argv[0] -# -# Create a temporary local u1db replica and attempt to sync to it. -# Returns a failure message if something went wrong. -# -def soledad_sync(uuid, token, server): - tempdir = tempfile.mkdtemp() - try: - db = u1db.open(os.path.join(tempdir, '%s.db' % uuid), True) - creds = {'token': {'uuid': uuid, 'token': token}} - db.sync(server, creds=creds, autocreate=False) - finally: - shutil.rmtree(tempdir) - -# -# exit codes: -# -# 0 - OK -# 1 - WARNING -# 2 - ERROR -# +def bail(msg, exitcode): + print "[!] %s" % msg + sys.exit(exitcode) + + +def create_docs(soledad): + """ + Populates the soledad database with dummy messages, so we can exercise + sending payloads during the sync. + """ + deferreds = [] + for index in xrange(NUMDOCS): + deferreds.append(soledad.create_doc({'payload': 'dummy'})) + return defer.gatherResults(deferreds) + +# main program if __name__ == '__main__': - try: - uuid, token, server = sys.argv[1:] - result = soledad_sync(uuid, token, server) - if result is None: - exit(0) - else: - print(result) - exit(1) - except Exception as exc: - print(exc.message or str(exc)) - traceback.print_exc(file=sys.stdout) - exit(2) + + tempdir = tempfile.mkdtemp() + if len(sys.argv) < 6: + bail(USAGE, 2) + uuid, token, server, cert_file, passphrase = sys.argv[1:] + s = get_soledad_instance( + uuid, passphrase, tempdir, server, cert_file, token) + + def onSyncDone(sync_result): + print "SYNC_RESULT:", sync_result + s.close() + reactor.stop() + + def start_sync(): + d = create_docs(s) + d.addCallback(lambda _: s.sync()) + d.addCallback(onSyncDone) + + reactor.callWhenRunning(start_sync) + reactor.run() diff --git a/tests/white-box/webapp.rb b/tests/white-box/webapp.rb index 48507521..e689c143 100644 --- a/tests/white-box/webapp.rb +++ b/tests/white-box/webapp.rb @@ -41,6 +41,35 @@ class Webapp < LeapTest pass end + def test_05_Can_create_and_authenticate_and_delete_user_via_API? + if property('webapp.allow_registration') + assert_tmp_user + pass + else + skip "New user registrations are disabled." + end + end + + def test_06_Can_sync_Soledad? + return unless property('webapp.allow_registration') + soledad_config = property('definition_files.soledad_service') + if soledad_config && !soledad_config.empty? + soledad_server = pick_soledad_server(soledad_config) + if soledad_server + assert_tmp_user do |user| + command = File.expand_path "../../helpers/soledad_sync.py", __FILE__ + soledad_url = "https://#{soledad_server}/user-#{user.id}" + soledad_cert = "/usr/local/share/ca-certificates/leap_ca.crt" + assert_run "#{command} #{user.id} #{user.session_token} #{soledad_url} #{soledad_cert} #{user.password}" + assert_user_db_exists(user) + pass + end + end + else + skip 'No soledad service configuration' + end + end + private def url_options |