python api: allow control over the server certificate verification

The --insecure option ("insecure=true" in .zuliprc) disables
verification entirely, similar to other tools like curl.

The --cert_bundle ("cert_bundle=<file>" in .zuliprc) allows
a file to be specified containing the CA certificates to verify
against.
When using self-signed certificates, the server's public key
can be used as the only cerificate in the file.

This change incidentally fixes an issue where the "site" parameter
in .zuliprc was ignored when specifying --user and --api-key on
the command line.

Fixes: #104
This commit is contained in:
Shane Kearns 2015-10-20 19:28:03 +01:00 committed by Tim Abbott
parent bad47da16e
commit 2f573bbb10
2 changed files with 107 additions and 11 deletions

View file

@ -31,11 +31,24 @@ file is as follows:
key=<api key from the web interface> key=<api key from the web interface>
email=<your email address> email=<your email address>
site=<your Zulip server's URI> site=<your Zulip server's URI>
insecure=<true or false, true means do not verify the server certificate>
cert_bundle=<path to a file containing CA or server certificates to trust>
If omitted, these settings have the following defaults:
site=https://api.zulip.com
insecure=false
cert_bundle=<the default CA bundle trusted by Python>
Alternatively, you may explicitly use "--user" and "--api-key" in our Alternatively, you may explicitly use "--user" and "--api-key" in our
examples, which is especially useful if you are running several bots examples, which is especially useful if you are running several bots
which share a home directory. There is also a "--site" option for which share a home directory.
setting the Zulip server on the command line.
The command line equivalents for other configuration options are:
--site=<your Zulip server's URI>
--insecure
--cert-bundle=<file>
You can obtain your Zulip API key, create bots, and manage bots all You can obtain your Zulip API key, create bots, and manage bots all
from your Zulip [settings page](https://zulip.com/#settings). from your Zulip [settings page](https://zulip.com/#settings).
@ -101,3 +114,46 @@ Alternatively, if you don't want to use your ~/.zuliprc file:
--api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 \ --api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 \
hamlet@example.com cordelia@example.com -m \ hamlet@example.com cordelia@example.com -m \
"Conscience doth make cowards of us all." "Conscience doth make cowards of us all."
#### Working with an untrusted server certificate
If your server has either a self-signed certificate, or a certificate signed
by a CA that you don't wish to globally trust then by default the API will
fail with an SSL verification error.
You can add `insecure=true` to your .zuliprc file.
[api]
site=https://zulip.example.com
insecure=true
This disables verification of the server certificate, so connections are
encrypted but unauthenticated. This is not secure, but may be good enough
for a development environment.
You can explicitly trust the server certificate using `cert_bundle=<filename>`
in your .zuliprc file.
[api]
site=https://zulip.example.com
cert_bundle=/home/bots/certs/zulip.example.com.crt
You can also explicitly trust a different set of Certificate Authorities from
the default bundle that is trusted by Python. For example to trust a company
internal CA.
[api]
site=https://zulip.example.com
cert_bundle=/home/bots/certs/example.com.ca-bundle
Save the server certificate (or the CA certificate) in its own file,
converting to PEM format first if necessary.
Verify that the certificate you have saved is the same as the one on the
server.
The `cert_bundle` option trusts the server / CA certificate only for
interaction with the zulip site, and is relatively secure.
Note that a certificate bundle is merely one or more certificates combined
into a single file.

View file

@ -117,6 +117,19 @@ def generate_option_group(parser, prefix=''):
default=None, default=None,
dest="zulip_client", dest="zulip_client",
help=optparse.SUPPRESS_HELP) help=optparse.SUPPRESS_HELP)
group.add_option('--insecure',
action='store_true',
dest='insecure',
help='''Do not verify the server certificate.
The https connection will not be secure.''')
group.add_option('--cert-bundle',
action='store',
dest='cert_bundle',
help='''Specify a file containing either the
server certificate, or a set of trusted
CA certificates. This will be used to
verify the server's identity. All
certificates should be PEM encoded.''')
return group return group
def init_from_options(options, client=None): def init_from_options(options, client=None):
@ -126,7 +139,8 @@ def init_from_options(options, client=None):
client = _default_client() client = _default_client()
return Client(email=options.zulip_email, api_key=options.zulip_api_key, return Client(email=options.zulip_email, api_key=options.zulip_api_key,
config_file=options.zulip_config_file, verbose=options.verbose, config_file=options.zulip_config_file, verbose=options.verbose,
site=options.zulip_site, client=client) site=options.zulip_site, client=client,
cert_bundle=options.cert_bundle, insecure=options.insecure)
def get_default_config_filename(): def get_default_config_filename():
config_file = os.path.join(os.environ["HOME"], ".zuliprc") config_file = os.path.join(os.environ["HOME"], ".zuliprc")
@ -138,15 +152,14 @@ def get_default_config_filename():
class Client(object): class Client(object):
def __init__(self, email=None, api_key=None, config_file=None, def __init__(self, email=None, api_key=None, config_file=None,
verbose=False, retry_on_errors=True, verbose=False, retry_on_errors=True,
site=None, client=None): site=None, client=None,
cert_bundle=None, insecure=None):
if client is None: if client is None:
client = _default_client() client = _default_client()
if None in (api_key, email):
if config_file is None: if config_file is None:
config_file = get_default_config_filename() config_file = get_default_config_filename()
if not os.path.exists(config_file): if os.path.exists(config_file):
raise RuntimeError("api_key or email not specified and %s does not exist"
% (config_file,))
config = SafeConfigParser() config = SafeConfigParser()
with file(config_file, 'r') as f: with file(config_file, 'r') as f:
config.readfp(f, config_file) config.readfp(f, config_file)
@ -156,6 +169,22 @@ class Client(object):
email = config.get("api", "email") email = config.get("api", "email")
if site is None and config.has_option("api", "site"): if site is None and config.has_option("api", "site"):
site = config.get("api", "site") site = config.get("api", "site")
if cert_bundle is None and config.has_option("api", "cert_bundle"):
cert_bundle = config.get("api", "cert_bundle")
if insecure is None and config.has_option("api", "insecure"):
# Be quite strict about what is accepted so that users don't
# disable security unintentionally.
insecure_setting = config.get("api", "insecure").lower()
if insecure_setting == "true":
insecure = True
elif insecure_setting == "false":
insecure = False
else:
raise RuntimeError("insecure is set to '%s', it must be 'true' or 'false' if it is used in %s"
% (insecure_setting, config_file))
elif None in (api_key, email):
raise RuntimeError("api_key or email not specified and %s does not exist"
% (config_file,))
self.api_key = api_key self.api_key = api_key
self.email = email self.email = email
@ -175,6 +204,17 @@ class Client(object):
self.retry_on_errors = retry_on_errors self.retry_on_errors = retry_on_errors
self.client_name = client self.client_name = client
if insecure:
self.tls_verification=False
elif cert_bundle is not None:
if not os.path.isfile(cert_bundle):
raise RuntimeError("tls bundle '%s' does not exist"
%(cert_bundle,))
self.tls_verification=cert_bundle
else:
# Default behavior: verify against system CA certificates
self.tls_verification=True
def get_user_agent(self): def get_user_agent(self):
vendor = '' vendor = ''
vendor_version = '' vendor_version = ''
@ -249,7 +289,7 @@ class Client(object):
urlparse.urljoin(self.base_url, url), urlparse.urljoin(self.base_url, url),
auth=requests.auth.HTTPBasicAuth(self.email, auth=requests.auth.HTTPBasicAuth(self.email,
self.api_key), self.api_key),
verify=True, timeout=90, verify=self.tls_verification, timeout=90,
headers={"User-agent": self.get_user_agent()}, headers={"User-agent": self.get_user_agent()},
**kwargs) **kwargs)