diff --git a/README.md b/README.md index 5fb82a5..c9c6b84 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,24 @@ file is as follows: key= email= site= + insecure= + cert_bundle= + +If omitted, these settings have the following defaults: + + site=https://api.zulip.com + insecure=false + cert_bundle= Alternatively, you may explicitly use "--user" and "--api-key" in our examples, which is especially useful if you are running several bots -which share a home directory. There is also a "--site" option for -setting the Zulip server on the command line. +which share a home directory. + +The command line equivalents for other configuration options are: + + --site= + --insecure + --cert-bundle= You can obtain your Zulip API key, create bots, and manage bots all 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 \ hamlet@example.com cordelia@example.com -m \ "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=` +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. diff --git a/zulip/__init__.py b/zulip/__init__.py index c0ac410..d8d8a9e 100644 --- a/zulip/__init__.py +++ b/zulip/__init__.py @@ -117,6 +117,19 @@ def generate_option_group(parser, prefix=''): default=None, dest="zulip_client", 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 def init_from_options(options, client=None): @@ -126,7 +139,8 @@ def init_from_options(options, client=None): client = _default_client() return Client(email=options.zulip_email, api_key=options.zulip_api_key, 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(): config_file = os.path.join(os.environ["HOME"], ".zuliprc") @@ -138,15 +152,14 @@ def get_default_config_filename(): class Client(object): def __init__(self, email=None, api_key=None, config_file=None, verbose=False, retry_on_errors=True, - site=None, client=None): + site=None, client=None, + cert_bundle=None, insecure=None): if client is None: client = _default_client() - if None in (api_key, email): - if config_file is None: - config_file = get_default_config_filename() - if not os.path.exists(config_file): - raise RuntimeError("api_key or email not specified and %s does not exist" - % (config_file,)) + + if config_file is None: + config_file = get_default_config_filename() + if os.path.exists(config_file): config = SafeConfigParser() with file(config_file, 'r') as f: config.readfp(f, config_file) @@ -156,6 +169,22 @@ class Client(object): email = config.get("api", "email") if site is None and config.has_option("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.email = email @@ -175,6 +204,17 @@ class Client(object): self.retry_on_errors = retry_on_errors 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): vendor = '' vendor_version = '' @@ -249,7 +289,7 @@ class Client(object): urlparse.urljoin(self.base_url, url), auth=requests.auth.HTTPBasicAuth(self.email, self.api_key), - verify=True, timeout=90, + verify=self.tls_verification, timeout=90, headers={"User-agent": self.get_user_agent()}, **kwargs)