Skip to content

Commit

Permalink
Merge pull request certbot#1455 from letsencrypt/useragent
Browse files Browse the repository at this point in the history
Add a User Agent string for client analytics
  • Loading branch information
bmw committed Nov 14, 2015
2 parents 2aab878 + d8a32ee commit 151c674
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 51 deletions.
44 changes: 29 additions & 15 deletions letsencrypt/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import zope.interface.exceptions
import zope.interface.verify

from acme import client as acme_client
from acme import jose

import letsencrypt
Expand All @@ -39,7 +38,6 @@
from letsencrypt.display import ops as display_ops
from letsencrypt.plugins import disco as plugins_disco


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -304,7 +302,7 @@ def _report_new_cert(cert_path, fullchain_path):
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)


def _auth_from_domains(le_client, config, domains, plugins):
def _auth_from_domains(le_client, config, domains):
"""Authenticate and enroll certificate."""
# Note: This can raise errors... caught above us though.
lineage = _treat_as_renewal(config, domains)
Expand All @@ -325,7 +323,7 @@ def _auth_from_domains(le_client, config, domains, plugins):
# configuration values from this attempt? <- Absolutely (jdkasten)
else:
# TREAT AS NEW REQUEST
lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
lineage = le_client.obtain_and_enroll_certificate(domains)
if not lineage:
raise errors.Error("Certificate could not be obtained")

Expand Down Expand Up @@ -425,14 +423,23 @@ def choose_configurator_plugins(args, config, plugins, verb):
authenticator = display_ops.pick_authenticator(config, req_auth, plugins)
logger.debug("Selected authenticator %s and installer %s", authenticator, installer)

# Report on any failures
if need_inst and not installer:
diagnose_configurator_problem("installer", req_inst, plugins)
if need_auth and not authenticator:
diagnose_configurator_problem("authenticator", req_auth, plugins)

record_chosen_plugins(config, plugins, authenticator, installer)
return installer, authenticator


def record_chosen_plugins(config, plugins, auth, inst):
"Update the config entries to reflect the plugins we actually selected."
cn = config.namespace
cn.authenticator = plugins.find_init(auth).name if auth else "none"
cn.installer = plugins.find_init(inst).name if inst else "none"


# TODO: Make run as close to auth + install as possible
# Possible difficulties: args.csr was hacked into auth
def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals
Expand All @@ -447,7 +454,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(args, config, authenticator, installer)

lineage = _auth_from_domains(le_client, config, domains, plugins)
lineage = _auth_from_domains(le_client, config, domains)

le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert,
Expand All @@ -461,7 +468,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
display_ops.success_renewal(domains)


def obtaincert(args, config, plugins):
def obtain_cert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""

if args.domains is not None and args.csr is not None:
Expand All @@ -487,7 +494,7 @@ def obtaincert(args, config, plugins):
_report_new_cert(cert_path, cert_fullchain)
else:
domains = _find_domains(args, installer)
_auth_from_domains(le_client, config, domains, plugins)
_auth_from_domains(le_client, config, domains)


def install(args, config, plugins):
Expand All @@ -512,18 +519,19 @@ def install(args, config, plugins):

def revoke(args, config, unused_plugins): # TODO: coop with renewal config
"""Revoke a previously obtained certificate."""
# For user-agent construction
config.namespace.installer = config.namespace.authenticator = "none"
if args.key_path is not None: # revocation by cert key
logger.debug("Revoking %s using cert key %s",
args.cert_path[0], args.key_path[0])
acme = acme_client.Client(
config.server, key=jose.JWK.load(args.key_path[1]))
key = jose.JWK.load(args.key_path[1])
else: # revocation by account key
logger.debug("Revoking %s using Account Key", args.cert_path[0])
acc, _ = _determine_account(args, config)
# pylint: disable=protected-access
acme = client._acme_from_config_key(config, acc.key)
acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate(
args.cert_path[1])[0]))
key = acc.key
acme = client.acme_from_config_key(config, key)
cert = crypto_util.pyopenssl_load_certificate(args.cert_path[1])[0]
acme.revoke(jose.ComparableX509(cert))


def rollback(args, config, plugins):
Expand Down Expand Up @@ -625,7 +633,7 @@ class HelpfulArgumentParser(object):
"""

# Maps verbs/subcommands to the functions that implement them
VERBS = {"auth": obtaincert, "certonly": obtaincert,
VERBS = {"auth": obtain_cert, "certonly": obtain_cert,
"config_changes": config_changes, "everything": run,
"install": install, "plugins": plugins_cmd,
"revoke": revoke, "rollback": rollback, "run": run}
Expand Down Expand Up @@ -921,7 +929,13 @@ def _create_subparsers(helpful):
helpful.add_group("revoke", description="Options for revocation of certs")
helpful.add_group("rollback", description="Options for reverting config changes")
helpful.add_group("plugins", description="Plugin options")

helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
'Encrypt server, set this to "".'
)
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
Expand Down
42 changes: 29 additions & 13 deletions letsencrypt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from acme import jose
from acme import messages

import letsencrypt

from letsencrypt import account
from letsencrypt import auth_handler
from letsencrypt import configuration
Expand All @@ -31,10 +33,30 @@
logger = logging.getLogger(__name__)


def _acme_from_config_key(config, key):
def acme_from_config_key(config, key):
"Wrangle ACME client construction"
# TODO: Allow for other alg types besides RS256
return acme_client.Client(directory=config.server, key=key,
verify_ssl=(not config.no_verify_ssl))
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
user_agent=_determine_user_agent(config))
return acme_client.Client(config.server, key=key, net=net)


def _determine_user_agent(config):
"""
Set a user_agent string in the config based on the choice of plugins.
(this wasn't knowable at construction time)
:returns: the client's User-Agent string
:rtype: `str`
"""

if config.user_agent is None:
ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}"
ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()),
config.authenticator, config.installer)
else:
ua = config.user_agent
return ua


def register(config, account_storage, tos_cb=None):
Expand Down Expand Up @@ -86,7 +108,7 @@ def register(config, account_storage, tos_cb=None):
public_exponent=65537,
key_size=config.rsa_key_size,
backend=default_backend())))
acme = _acme_from_config_key(config, key)
acme = acme_from_config_key(config, key)
# TODO: add phone?
regr = acme.register(messages.NewRegistration.from_data(email=config.email))

Expand All @@ -100,6 +122,7 @@ def register(config, account_storage, tos_cb=None):
acc = account.Account(regr, key)
account.report_new_account(acc, config)
account_storage.save(acc)

return acc, acme


Expand Down Expand Up @@ -128,7 +151,7 @@ def __init__(self, config, account_, dv_auth, installer, acme=None):

# Initialize ACME if account is provided
if acme is None and self.account is not None:
acme = _acme_from_config_key(config, self.account.key)
acme = acme_from_config_key(config, self.account.key)
self.acme = acme

# TODO: Check if self.config.enroll_autorenew is None. If
Expand Down Expand Up @@ -213,7 +236,7 @@ def obtain_certificate(self, domains):

return self._obtain_certificate(domains, csr) + (key, csr)

def obtain_and_enroll_certificate(self, domains, plugins):
def obtain_and_enroll_certificate(self, domains):
"""Obtain and enroll certificate.
Get a new certificate for the specified domains using the specified
Expand All @@ -230,13 +253,6 @@ def obtain_and_enroll_certificate(self, domains, plugins):
"""
certr, chain, key, _ = self.obtain_certificate(domains)

# TODO: remove this dirty hack
self.config.namespace.authenticator = plugins.find_init(
self.dv_auth).name
if self.installer is not None:
self.config.namespace.installer = plugins.find_init(
self.installer).name

# XXX: We clearly need a more general and correct way of getting
# options into the configobj for the RenewableCert instance.
# This is a quick-and-dirty way to do it to allow integration
Expand Down
40 changes: 40 additions & 0 deletions letsencrypt/le_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import errno
import logging
import os
import platform
import re
import subprocess
import stat
Expand Down Expand Up @@ -202,6 +203,45 @@ def safely_remove(path):
raise


def get_os_info():
"""
Get Operating System type/distribution and major version
:returns: (os_name, os_version)
:rtype: `tuple` of `str`
"""
info = platform.system_alias(
platform.system(),
platform.release(),
platform.version()
)
os_type, os_ver, _ = info
os_type = os_type.lower()
if os_type.startswith('linux'):
info = platform.linux_distribution()
# On arch, platform.linux_distribution() is reportedly ('','',''),
# so handle it defensively
if info[0]:
os_type = info[0]
if info[1]:
os_ver = info[1]
elif os_type.startswith('darwin'):
os_ver = subprocess.Popen(
["sw_vers", "-productVersion"],
stdout=subprocess.PIPE
).communicate()[0]
os_ver = os_ver.partition(".")[0]
elif os_type.startswith('freebsd'):
# eg "9.3-RC3-p1"
os_ver = os_ver.partition("-")[0]
os_ver = os_ver.partition(".")[0]
elif platform.win32_ver()[1]:
os_ver = platform.win32_ver()[1]
else:
# Cases known to fall here: Cygwin python
os_ver = ''
return os_type, os_ver

# Just make sure we don't get pwned... Make sure that it also doesn't
# start with a period or have two consecutive periods <- this needs to
# be done in addition to the regex
Expand Down
Loading

0 comments on commit 151c674

Please sign in to comment.