diff --git a/letsencrypt/cli.py b/letsencrypt/cli.py index be933d891..defa7633d 100644 --- a/letsencrypt/cli.py +++ b/letsencrypt/cli.py @@ -238,6 +238,8 @@ def create_parser(plugins): help="Skip the end user license agreement screen.") add("-t", "--text", dest="text_mode", action="store_true", help="Use the text output instead of the curses UI.") + add("--test-mode", action="store_true", help=config_help("test_mode"), + default=flag_default("test_mode")) subparsers = parser.add_subparsers(metavar="SUBCOMMAND") def add_subparser(name, func): # pylint: disable=missing-docstring diff --git a/letsencrypt/client.py b/letsencrypt/client.py index b92aded4a..ae1667dfa 100644 --- a/letsencrypt/client.py +++ b/letsencrypt/client.py @@ -62,7 +62,8 @@ class Client(object): # TODO: Allow for other alg types besides RS256 self.network = network2.Network( - config.server_url, jwk.JWKRSA.load(self.account.key.pem)) + config.server, jwk.JWKRSA.load(self.account.key.pem), + verify_ssl=(not config.test_mode)) self.config = config diff --git a/letsencrypt/configuration.py b/letsencrypt/configuration.py index a83fb9978..6a808a6a9 100644 --- a/letsencrypt/configuration.py +++ b/letsencrypt/configuration.py @@ -1,5 +1,7 @@ """Let's Encrypt user-supplied configuration.""" import os +import urlparse + import zope.interface from letsencrypt import constants @@ -28,8 +30,6 @@ class NamespaceConfig(object): zope.interface.implements(interfaces.IConfig) def __init__(self, namespace): - assert not namespace.server.startswith('https://') - assert not namespace.server.startswith('http://') self.namespace = namespace def __getattr__(self, name): @@ -47,12 +47,8 @@ class NamespaceConfig(object): @property def server_path(self): """File path based on ``server``.""" - return self.namespace.server.replace('/', os.path.sep) - - @property - def server_url(self): - """Full server URL (including HTTPS scheme).""" - return 'https://' + self.namespace.server + parsed = urlparse.urlparse(self.namespace.server) + return (parsed.netloc + parsed.path).replace('/', os.path.sep) @property def cert_key_backup(self): # pylint: disable=missing-docstring diff --git a/letsencrypt/constants.py b/letsencrypt/constants.py index 4eba69f20..9ff0b128c 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -11,7 +11,7 @@ SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins" CLI_DEFAULTS = dict( config_files=["/etc/letsencrypt/cli.ini"], verbose_count=-(logging.WARNING / 10), - server="www.letsencrypt-demo.org/acme/new-reg", + server="https://www.letsencrypt-demo.org/acme/new-reg", rsa_key_size=2048, rollback_checkpoints=0, config_dir="/etc/letsencrypt", @@ -21,6 +21,7 @@ CLI_DEFAULTS = dict( certs_dir="/etc/letsencrypt/certs", cert_path="/etc/letsencrypt/certs/cert-letsencrypt.pem", chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem", + test_mode=False, ) """Defaults for CLI flags and `.IConfig` attributes.""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index e6c0588c5..609b9410a 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -178,6 +178,9 @@ class IConfig(zope.interface.Interface): cert_path = zope.interface.Attribute("Let's Encrypt certificate file path.") chain_path = zope.interface.Attribute("Let's Encrypt chain file path.") + test_mode = zope.interface.Attribute( + "Test mode. Disables certificate verification.") + class IInstaller(IPlugin): """Generic Let's Encrypt Installer Interface. diff --git a/letsencrypt/network2.py b/letsencrypt/network2.py index 28cb702a3..faf23f414 100644 --- a/letsencrypt/network2.py +++ b/letsencrypt/network2.py @@ -29,6 +29,7 @@ class Network(object): :ivar str new_reg_uri: Location of new-reg :ivar key: `.JWK` (private) :ivar alg: `.JWASignature` + :ivar bool verify_ssl: Verify SSL certificates? """ @@ -36,10 +37,11 @@ class Network(object): JSON_CONTENT_TYPE = 'application/json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' - def __init__(self, new_reg_uri, key, alg=jose.RS256): + def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True): self.new_reg_uri = new_reg_uri self.key = key self.alg = alg + self.verify_ssl = verify_ssl def _wrap_in_jws(self, obj): """Wrap `JSONDeSerializable` object in JWS. @@ -116,6 +118,7 @@ class Network(object): """ logging.debug('Sending GET request to %s', uri) + kwargs.setdefault('verify', self.verify_ssl) try: response = requests.get(uri, **kwargs) except requests.exceptions.RequestException as error: @@ -135,6 +138,7 @@ class Network(object): """ logging.debug('Sending POST data to %s: %s', uri, data) + kwargs.setdefault('verify', self.verify_ssl) try: response = requests.post(uri, data=data, **kwargs) except requests.exceptions.RequestException as error: diff --git a/letsencrypt/tests/configuration_test.py b/letsencrypt/tests/configuration_test.py index 03f8e5d53..d25368feb 100644 --- a/letsencrypt/tests/configuration_test.py +++ b/letsencrypt/tests/configuration_test.py @@ -10,10 +10,10 @@ class NamespaceConfigTest(unittest.TestCase): def setUp(self): from letsencrypt.configuration import NamespaceConfig - namespace = mock.MagicMock( + self.namespace = mock.MagicMock( config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar', - server='acme-server.org:443/new') - self.config = NamespaceConfig(namespace) + server='https://acme-server.org:443/new') + self.config = NamespaceConfig(self.namespace) def test_proxy_getattr(self): self.assertEqual(self.config.foo, 'bar') @@ -23,9 +23,10 @@ class NamespaceConfigTest(unittest.TestCase): self.assertEqual(['acme-server.org:443', 'new'], self.config.server_path.split(os.path.sep)) - def test_server_url(self): - self.assertEqual( - self.config.server_url, 'https://acme-server.org:443/new') + self.namespace.server = ('http://user:pass@acme.server:443' + '/p/a/t/h;parameters?query#fragment') + self.assertEqual(['user:pass@acme.server:443', 'p', 'a', 't', 'h'], + self.config.server_path.split(os.path.sep)) @mock.patch('letsencrypt.configuration.constants') def test_dynamic_dirs(self, constants): diff --git a/letsencrypt/tests/network2_test.py b/letsencrypt/tests/network2_test.py index 3df5b6dab..7bffcf0f4 100644 --- a/letsencrypt/tests/network2_test.py +++ b/letsencrypt/tests/network2_test.py @@ -41,9 +41,10 @@ class NetworkTest(unittest.TestCase): def setUp(self): from letsencrypt.network2 import Network + self.verify_ssl = mock.MagicMock() self.net = Network( new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', - key=KEY, alg=jose.RS256) + key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) self.response = mock.MagicMock(ok=True, status_code=httplib.OK) self.response.headers = {} self.response.links = {} @@ -91,6 +92,9 @@ class NetworkTest(unittest.TestCase): self.net._post = mock.MagicMock(return_value=self.response) self.net._get = mock.MagicMock(return_value=self.response) + def test_init(self): + self.assertTrue(self.net.verify_ssl is self.verify_ssl) + def test_wrap_in_jws(self): class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring @@ -180,6 +184,20 @@ class NetworkTest(unittest.TestCase): self.net._check_response.assert_called_once_with( requests_mock.post('uri', 'data'), content_type='ct') + @mock.patch('letsencrypt.client.network2.requests') + def test_get_post_verify_ssl(self, requests_mock): + # pylint: disable=protected-access + self.net._check_response = mock.MagicMock() + + for verify_ssl in [True, False]: + self.net.verify_ssl = verify_ssl + self.net._get('uri') + self.net._post('uri', 'data') + requests_mock.get.assert_called_once_with('uri', verify=verify_ssl) + requests_mock.post.assert_called_once_with( + 'uri', data='data', verify=verify_ssl) + requests_mock.reset_mock() + def test_register(self): self.response.status_code = httplib.CREATED self.response.json.return_value = self.regr.body.to_json()