Skip to content
This repository was archived by the owner on Apr 25, 2023. It is now read-only.

Commit e192eb0

Browse files
committed
Added tests for command line commands. Various test related cleanups. Added (currently broken) HTTP GET support and refactored the integration tests as a result (also currently broken.
1 parent 70ab6fe commit e192eb0

27 files changed

+949
-230
lines changed

Makefile

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,16 @@ test: clean
3535

3636
coverage: clean
3737
coverage run -m unittest discover --buffer
38-
coverage report -m --include=drogulus/* --omit=drogulus/net/*
38+
coverage report -m --include=drogulus/* --omit=drogulus/net/*,drogulus/commands/*,drogulus/contrib/*
3939
coverage report -m --include=drogulus/net/*
40+
coverage report -m --include=drogulus/commands/*
4041

4142
integration:
4243
python integration_tests/run.py
4344

4445
check: clean pep8 pyflakes coverage integration
4546

46-
package: clean
47+
package: check
4748
python setup.py sdist
4849

4950
publish: check

drogulus/commands/keygen.py

+20-21
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@
22
"""
33
Defines the command for generating an RSA keypair for use with the drogulus.
44
"""
5-
from Crypto.PublicKey import RSA
6-
from Crypto import Random
75
from cliff.command import Command
8-
from .utils import data_dir
6+
import rsa
7+
from .utils import data_dir, APPNAME, save_keys
98
import getpass
109
import os
1110

1211

1312
class KeyGen(Command):
1413
"""
15-
Generates an appropriate .pem file containing the user's public/private
16-
key pair for use with the drogulus.
14+
Generates appropriate files containing the user's public/private key pair
15+
for use with the drogulus.
1716
1817
This command will prompt the user for a passphrase to ensure the resulting
19-
.pem file is encrypted.
18+
private key file is encrypted.
2019
"""
2120

2221
def get_description(self):
@@ -29,19 +28,19 @@ def get_parser(self, prog_name):
2928
return parser
3029

3130
def take_action(self, parsed_args):
32-
while True:
33-
passphrase = getpass.getpass('Passphrase (make it tricky): ')
34-
confirm = getpass.getpass('Confirm passphrase: ')
35-
if passphrase == confirm:
36-
break
37-
else:
38-
print('Passphrase and confirmation did not match.')
39-
print('Please try again...')
31+
passphrase = getpass.getpass('Passphrase (make it tricky): ')
32+
confirm = getpass.getpass('Confirm passphrase: ')
33+
if passphrase != confirm:
34+
raise ValueError('Passphrase and confirmation did not match.')
4035
size = parsed_args.size
41-
output_file = os.path.join(data_dir(), 'drogulus.pem')
42-
print('Generating key...')
43-
random_generator = Random.new().read
44-
key = RSA.generate(size, random_generator)
45-
with open(output_file, 'w') as f:
46-
f.write(key.exportKey('PEM', passphrase).decode('ascii'))
47-
print('Key written to {}'.format(output_file))
36+
print('Generating keys (this may take some time, go have a coffee).')
37+
(pub, priv) = rsa.newkeys(size)
38+
output_file_pub = os.path.join(data_dir(), '{}.pub'.format(APPNAME))
39+
output_file_priv = os.path.join(data_dir(),
40+
'{}.scrypt'.format(APPNAME))
41+
private_key = priv.save_pkcs1().decode('ascii')
42+
public_key = pub.save_pkcs1().decode('ascii')
43+
save_keys(private_key, public_key, passphrase, output_file_priv,
44+
output_file_pub)
45+
print('Private key written to {}'.format(output_file_priv))
46+
print('Public key written to {}'.format(output_file_pub))

drogulus/commands/start.py

+31-36
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
"""
55
from ..node import Drogulus
66
from ..net.http import HttpConnector, HttpRequestHandler
7-
from .utils import data_dir, log_dir, get_keys, get_whoami, get_alias
7+
from .utils import data_dir, log_dir, get_keys, get_whoami, APPNAME
88
from cliff.command import Command
99
from getpass import getpass
1010
import logging
11+
import logging.handlers
1112
import asyncio
1213
import json
13-
import sys
1414
import os
15+
import os.path
1516

1617

1718
class Start(Command):
@@ -23,21 +24,20 @@ def get_description(self):
2324
return 'Starts a local drogulus node.'
2425

2526
def get_parser(self, prog_name):
26-
# TODO: Non positional args
2727
parser = super(Start, self).get_parser(prog_name)
2828
parser.add_argument('--passphrase', '-p', nargs='?', default='',
29-
type=str, help='The passphrase for the RSA keys.')
30-
parser.add_argument('--keyfile', '-k', nargs='?', default='', type=str,
31-
help='The pem file of the RSA keys to use.')
29+
type=str, help='The passphrase for the private ' +
30+
'RSA key.')
31+
parser.add_argument('--keys', '-k', nargs='?', default='', type=str,
32+
help='The directory containing the RSA public ' +
33+
'and private keys.')
3234
parser.add_argument('--peers', nargs='?', default='', type=str,
3335
help='The peer.json file used to seed the ' +
3436
'local node\'s routing table.')
3537
parser.add_argument('--port', nargs='?', default=1908, type=int,
3638
help='The incoming port (defaults to 1908).')
3739
parser.add_argument('--whoami', nargs='?', default='', type=str,
3840
help='The whoami.json file to use.')
39-
parser.add_argument('--alias', nargs='?', default='', type=str,
40-
help='The alias.json file to use.')
4141
return parser
4242

4343
def take_action(self, parsed_args):
@@ -47,24 +47,15 @@ def take_action(self, parsed_args):
4747
"""
4848
passphrase = parsed_args.passphrase
4949
if not passphrase:
50-
print('You must supply a passphrase')
51-
passphrase = getpass()
50+
print('You must supply a passphrase.')
51+
passphrase = getpass().strip()
5252
if not passphrase:
53-
sys.exit(1)
53+
raise ValueError('You must supply a passphrase.')
5454
port = parsed_args.port
5555
whoami = parsed_args.whoami
56-
alias = parsed_args.alias
57-
key_file = parsed_args.keyfile
56+
key_dir = parsed_args.keys
5857
peer_file = parsed_args.peers
5958

60-
# RSA key config.
61-
try:
62-
private_key, public_key = get_keys(passphrase, key_file)
63-
except Exception as ex:
64-
print('Unable to get keys from {}'.format(key_file))
65-
print('{}'.format(repr(ex)))
66-
sys.exit(1)
67-
6859
# Setup logging
6960
logfile = os.path.join(log_dir(), 'drogulus.log')
7061
handler = logging.handlers.TimedRotatingFileHandler(logfile,
@@ -79,30 +70,38 @@ def take_action(self, parsed_args):
7970
log = logging.getLogger(__name__)
8071
print('Logging to {}'.format(logfile))
8172

82-
# Whoami and alias
73+
# RSA key config.
74+
priv_path = None
75+
pub_path = None
76+
if key_dir:
77+
priv_path = os.path.join(key_dir, '{}.scrypt'.format(APPNAME))
78+
pub_path = os.path.join(key_dir, '{}.pub'.format(APPNAME))
79+
try:
80+
private_key, public_key = get_keys(passphrase, priv_path,
81+
pub_path)
82+
except Exception as ex:
83+
log.error('Unable to get keys from {}'.format(key_dir))
84+
log.error(ex)
85+
raise ex
86+
87+
# Whoami
8388
try:
8489
whoami = get_whoami(whoami)
8590
except:
8691
log.error('Unable to get whoami file.')
8792
whoami = None
88-
try:
89-
alias = get_alias(alias)
90-
except:
91-
log.error('Unable to get alias file.')
92-
alias = None
9393

9494
# Asyncio boilerplate.
9595
event_loop = asyncio.get_event_loop()
9696
connector = HttpConnector(event_loop) # NetstringConnector(event_loop)
97-
instance = Drogulus(public_key, private_key, event_loop, connector,
98-
port, alias, whoami)
97+
instance = Drogulus(private_key, public_key, event_loop, connector,
98+
port, whoami)
9999

100100
def protocol_factory(connector=connector, node=instance._node):
101101
"""
102102
Returns an appropriately configured Protocol object for
103103
each connection.
104104
"""
105-
# return NetstringProtocol(connector, node)
106105
return HttpRequestHandler(connector, node)
107106

108107
setup_server = event_loop.create_server(protocol_factory, port=port)
@@ -120,16 +119,12 @@ def protocol_factory(connector=connector, node=instance._node):
120119
except KeyboardInterrupt:
121120
log.info('Manual exit')
122121
finally:
123-
# dump alias
124-
with open(os.path.join(data_dir(), 'alias.json'), 'w') as output:
125-
json.dump(instance.alias, output, indent=2)
126-
log.info('Dumped alias')
127122
# dump peers
128123
if not peer_file:
129124
peer_file = os.path.join(data_dir(), 'peers.json')
130125
with open(peer_file, 'w') as output:
131-
json.dump(instance._node.routing_table.all_contacts(),
132-
output, indent=2)
126+
json.dump(instance._node.routing_table.dump(), output,
127+
indent=2)
133128
log.info('Dumped peers')
134129
log.info('STOPPED')
135130
server.close()

drogulus/commands/utils.py

+38-18
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
"""
55
from ..version import get_version
66
from ..contrib.appdirs import user_data_dir, user_log_dir
7-
from Crypto.PublicKey import RSA
87
import json
98
import os
9+
import pyscrypt
10+
from pyscrypt.file import InvalidScryptFileFormat
1011

1112

1213
APPNAME = 'drogulus'
@@ -39,33 +40,52 @@ def log_dir():
3940
return uld
4041

4142

42-
def get_keys(passphrase, input_file=None):
43+
def get_keys(passphrase, priv_file=None, pub_file=None):
4344
"""
4445
Will return a string representation of both the private and public
45-
password protected RSA keys found in the location specified by input_file.
46-
If input_file is None then the sane default location and name is used.
46+
RSA keys found in the locations specified by priv_file and pub_file args.
47+
Since the private key is password protected the passphrase argument is
48+
used to decrypt it. If no file paths are given then sane default
49+
location and names are used.
4750
"""
48-
if not input_file:
49-
input_file = os.path.join(data_dir(), '{}.pem'.format(APPNAME))
50-
f = open(input_file, 'r')
51-
key = RSA.importKey(f.read(), passphrase)
52-
return (key.exportKey('PEM').decode('ascii'),
53-
key.publickey().exportKey('PEM').decode('ascii'))
51+
if not pub_file:
52+
pub_file = os.path.join(data_dir(), '{}.pub'.format(APPNAME))
53+
if not priv_file:
54+
priv_file = os.path.join(data_dir(), '{}.scrypt'.format(APPNAME))
55+
pub = open(pub_file, 'rb').read()
56+
try:
57+
with pyscrypt.ScryptFile(priv_file, passphrase.encode('utf-8')) as f:
58+
priv = f.read()
59+
except InvalidScryptFileFormat:
60+
# Make the exception a bit more human.
61+
msg = 'Unable to read private key file. Check your passphrase!'
62+
raise ValueError(msg)
63+
return (priv, pub)
5464

5565

56-
def get_whoami(input_file=None):
66+
def save_keys(private_key, public_key, passphrase, priv_file, pub_file):
5767
"""
58-
Attempts to get the user's whoami information.
68+
Given private and public keys as bytes, a passphrase and paths to private
69+
and public output files will save the keys in the appropriate file path
70+
location. In the case of the private key, will use the scrypt module (see:
71+
https://en.wikipedia.org/wiki/Scrypt) and the passphrase to encrypt it.
5972
"""
60-
if not input_file:
61-
input_file = os.path.join(data_dir(), 'whoami.json')
62-
return json.load(open(input_file, 'r'))
73+
with open(pub_file, 'wb') as fpub:
74+
fpub.write(public_key)
75+
# PyScrypt has problems using the 'with' keyword and saving content.
76+
fp = open(priv_file, 'wb')
77+
try:
78+
fpriv = pyscrypt.ScryptFile(fp, passphrase.encode('utf-8'), N=1024,
79+
r=1, p=1)
80+
fpriv.write(private_key)
81+
finally:
82+
fpriv.close()
6383

6484

65-
def get_alias(input_file=None):
85+
def get_whoami(input_file=None):
6686
"""
67-
Attempts to get the user's alias dictionary.
87+
Attempts to get the user's whoami information.
6888
"""
6989
if not input_file:
70-
input_file = os.path.join(data_dir(), 'alias.json')
90+
input_file = os.path.join(data_dir(), 'whoami.json')
7191
return json.load(open(input_file, 'r'))

drogulus/commands/whoami.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ class WhoAmI(Command):
1515
owner of a public key in the drogulus DHT.
1616
"""
1717

18+
contact_fields = ['Name', 'Nickname', 'Organizational affiliation',
19+
'Website', 'Contact information (e.g. email)',
20+
'Biography', 'Miscellaneous notes']
21+
"""
22+
This list of contact fields defines the field names used to reference data
23+
in the whoami JSON file. They're a bit arbitrary, but vaguely based upon
24+
a small basic subset of fields used by vcard (see rfc2426).
25+
"""
26+
1827
def get_description(self):
1928
return ' '.join(['Generates a whoami data structure to self-identify',
2029
'a user.'])
@@ -25,14 +34,11 @@ def take_action(self, parsed_args):
2534
is associated with some sort of contact details.
2635
"""
2736
whoami = {}
28-
fields = ['Name', 'Nickname', 'Organizational affiliation', 'Website',
29-
'Contact information (e.g. email)', 'Biography',
30-
'Miscellaneous notes']
3137
print('Create a whoami profile.')
3238
print('(Blank fields will be left out of the profile.)')
3339
while True:
34-
for field in fields:
35-
val = input('{}: '.format(field)).strip()
40+
for field in self.contact_fields:
41+
val = str(input('{}: '.format(field))).strip()
3642
if val:
3743
whoami[field] = val
3844
print('\nPlease check:')

0 commit comments

Comments
 (0)