Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

"""Decide which plugins to use for authentication & installation""" 

from __future__ import print_function 

 

import os 

import logging 

 

import six 

import zope.component 

 

from certbot import errors 

from certbot import interfaces 

 

from certbot.display import util as display_util 

 

logger = logging.getLogger(__name__) 

z_util = zope.component.getUtility 

 

def pick_configurator( 

config, default, plugins, 

question="How would you like to authenticate and install " 

"certificates?"): 

"""Pick configurator plugin.""" 

return pick_plugin( 

config, default, plugins, question, 

(interfaces.IAuthenticator, interfaces.IInstaller)) 

 

 

def pick_installer(config, default, plugins, 

question="How would you like to install certificates?"): 

"""Pick installer plugin.""" 

return pick_plugin( 

config, default, plugins, question, (interfaces.IInstaller,)) 

 

 

def pick_authenticator( 

config, default, plugins, question="How would you " 

"like to authenticate with the ACME CA?"): 

"""Pick authentication plugin.""" 

return pick_plugin( 

config, default, plugins, question, (interfaces.IAuthenticator,)) 

 

def get_unprepared_installer(config, plugins): 

""" 

Get an unprepared interfaces.IInstaller object. 

 

:param certbot.interfaces.IConfig config: Configuration 

:param certbot.plugins.disco.PluginsRegistry plugins: 

All plugins registered as entry points. 

 

:returns: Unprepared installer plugin or None 

:rtype: IPlugin or None 

""" 

 

_, req_inst = cli_plugin_requests(config) 

if not req_inst: 

return None 

installers = plugins.filter(lambda p_ep: p_ep.name == req_inst) 

installers.init(config) 

installers = installers.verify((interfaces.IInstaller,)) 

if len(installers) > 1: 

raise errors.PluginSelectionError( 

"Found multiple installers with the name %s, Certbot is unable to " 

"determine which one to use. Skipping." % req_inst) 

if installers: 

inst = list(installers.values())[0] 

logger.debug("Selecting plugin: %s", inst) 

return inst.init(config) 

else: 

raise errors.PluginSelectionError( 

"Could not select or initialize the requested installer %s." % req_inst) 

 

def pick_plugin(config, default, plugins, question, ifaces): 

"""Pick plugin. 

 

:param certbot.interfaces.IConfig: Configuration 

:param str default: Plugin name supplied by user or ``None``. 

:param certbot.plugins.disco.PluginsRegistry plugins: 

All plugins registered as entry points. 

:param str question: Question to be presented to the user in case 

multiple candidates are found. 

:param list ifaces: Interfaces that plugins must provide. 

 

:returns: Initialized plugin. 

:rtype: IPlugin 

 

""" 

if default is not None: 

# throw more UX-friendly error if default not in plugins 

filtered = plugins.filter(lambda p_ep: p_ep.name == default) 

else: 

if config.noninteractive_mode: 

# it's really bad to auto-select the single available plugin in 

# non-interactive mode, because an update could later add a second 

# available plugin 

raise errors.MissingCommandlineFlag( 

"Missing command line flags. For non-interactive execution, " 

"you will need to specify a plugin on the command line. Run " 

"with '--help plugins' to see a list of options, and see " 

"https://eff.org/letsencrypt-plugins for more detail on what " 

"the plugins do and how to use them.") 

 

filtered = plugins.visible().ifaces(ifaces) 

 

filtered.init(config) 

verified = filtered.verify(ifaces) 

verified.prepare() 

prepared = verified.available() 

 

if len(prepared) > 1: 

logger.debug("Multiple candidate plugins: %s", prepared) 

plugin_ep = choose_plugin(list(six.itervalues(prepared)), question) 

if plugin_ep is None: 

return None 

else: 

return plugin_ep.init() 

elif len(prepared) == 1: 

plugin_ep = list(prepared.values())[0] 

logger.debug("Single candidate plugin: %s", plugin_ep) 

if plugin_ep.misconfigured: 

return None 

return plugin_ep.init() 

else: 

logger.debug("No candidate plugin") 

return None 

 

 

def choose_plugin(prepared, question): 

"""Allow the user to choose their plugin. 

 

:param list prepared: List of `~.PluginEntryPoint`. 

:param str question: Question to be presented to the user. 

 

:returns: Plugin entry point chosen by the user. 

:rtype: `~.PluginEntryPoint` 

 

""" 

opts = [plugin_ep.description_with_name + 

(" [Misconfigured]" if plugin_ep.misconfigured else "") 

for plugin_ep in prepared] 

names = set(plugin_ep.name for plugin_ep in prepared) 

 

while True: 

disp = z_util(interfaces.IDisplay) 

if "CERTBOT_AUTO" in os.environ and names == set(("apache", "nginx")): 

# The possibility of being offered exactly apache and nginx here 

# is new interactivity brought by https://github.com/certbot/certbot/issues/4079, 

# so set apache as a default for those kinds of non-interactive use 

# (the user will get a warning to set --non-interactive or --force-interactive) 

apache_idx = [n for n, p in enumerate(prepared) if p.name == "apache"][0] 

code, index = disp.menu(question, opts, default=apache_idx) 

else: 

code, index = disp.menu(question, opts, force_interactive=True) 

 

if code == display_util.OK: 

plugin_ep = prepared[index] 

if plugin_ep.misconfigured: 

z_util(interfaces.IDisplay).notification( 

"The selected plugin encountered an error while parsing " 

"your server configuration and cannot be used. The error " 

"was:\n\n{0}".format(plugin_ep.prepare()), pause=False) 

else: 

return plugin_ep 

else: 

return None 

 

noninstaller_plugins = ["webroot", "manual", "standalone", "dns-cloudflare", "dns-cloudxns", 

"dns-digitalocean", "dns-dnsimple", "dns-dnsmadeeasy", "dns-gehirn", 

"dns-google", "dns-linode", "dns-luadns", "dns-nsone", "dns-ovh", 

"dns-rfc2136", "dns-route53", "dns-sakuracloud"] 

 

def record_chosen_plugins(config, plugins, auth, inst): 

"Update the config entries to reflect the plugins we actually selected." 

config.authenticator = plugins.find_init(auth).name if auth else None 

config.installer = plugins.find_init(inst).name if inst else None 

logger.info("Plugins selected: Authenticator %s, Installer %s", 

config.authenticator, config.installer) 

 

 

def choose_configurator_plugins(config, plugins, verb): 

# pylint: disable=too-many-branches 

""" 

Figure out which configurator we're going to use, modifies 

config.authenticator and config.installer strings to reflect that choice if 

necessary. 

 

:raises errors.PluginSelectionError if there was a problem 

 

:returns: (an `IAuthenticator` or None, an `IInstaller` or None) 

:rtype: tuple 

""" 

 

req_auth, req_inst = cli_plugin_requests(config) 

installer_question = None 

 

if verb == "enhance": 

installer_question = ("Which installer would you like to use to " 

"configure the selected enhancements?") 

 

# Which plugins do we need? 

if verb == "run": 

need_inst = need_auth = True 

from certbot.cli import cli_command 

if req_auth in noninstaller_plugins and not req_inst: 

msg = ('With the {0} plugin, you probably want to use the "certonly" command, eg:{1}' 

'{1} {2} certonly --{0}{1}{1}' 

'(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins' 

'{1} and "--help plugins" for more information.)'.format( 

req_auth, os.linesep, cli_command)) 

 

raise errors.MissingCommandlineFlag(msg) 

else: 

need_inst = need_auth = False 

if verb == "certonly": 

need_auth = True 

if verb == "install" or verb == "enhance": 

need_inst = True 

if config.authenticator: 

logger.warning("Specifying an authenticator doesn't make sense when " 

"running Certbot with verb \"%s\"", verb) 

# Try to meet the user's request and/or ask them to pick plugins 

authenticator = installer = None 

if verb == "run" and req_auth == req_inst: 

# Unless the user has explicitly asked for different auth/install, 

# only consider offering a single choice 

authenticator = installer = pick_configurator(config, req_inst, plugins) 

else: 

if need_inst or req_inst: 

installer = pick_installer(config, req_inst, plugins, installer_question) 

if need_auth: 

authenticator = 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 set_configurator(previously, now): 

""" 

Setting configurators multiple ways is okay, as long as they all agree 

:param str previously: previously identified request for the installer/authenticator 

:param str requested: the request currently being processed 

""" 

if not now: 

# we're not actually setting anything 

return previously 

if previously: 

if previously != now: 

msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}" 

raise errors.PluginSelectionError(msg.format(repr(previously), repr(now))) 

return now 

 

 

def cli_plugin_requests(config): # pylint: disable=too-many-branches 

""" 

Figure out which plugins the user requested with CLI and config options 

 

:returns: (requested authenticator string or None, requested installer string or None) 

:rtype: tuple 

""" 

req_inst = req_auth = config.configurator 

req_inst = set_configurator(req_inst, config.installer) 

req_auth = set_configurator(req_auth, config.authenticator) 

 

if config.nginx: 

req_inst = set_configurator(req_inst, "nginx") 

req_auth = set_configurator(req_auth, "nginx") 

if config.apache: 

req_inst = set_configurator(req_inst, "apache") 

req_auth = set_configurator(req_auth, "apache") 

if config.standalone: 

req_auth = set_configurator(req_auth, "standalone") 

if config.webroot: 

req_auth = set_configurator(req_auth, "webroot") 

if config.manual: 

req_auth = set_configurator(req_auth, "manual") 

if config.dns_cloudflare: 

req_auth = set_configurator(req_auth, "dns-cloudflare") 

if config.dns_cloudxns: 

req_auth = set_configurator(req_auth, "dns-cloudxns") 

if config.dns_digitalocean: 

req_auth = set_configurator(req_auth, "dns-digitalocean") 

if config.dns_dnsimple: 

req_auth = set_configurator(req_auth, "dns-dnsimple") 

if config.dns_dnsmadeeasy: 

req_auth = set_configurator(req_auth, "dns-dnsmadeeasy") 

if config.dns_gehirn: 

req_auth = set_configurator(req_auth, "dns-gehirn") 

if config.dns_google: 

req_auth = set_configurator(req_auth, "dns-google") 

if config.dns_linode: 

req_auth = set_configurator(req_auth, "dns-linode") 

if config.dns_luadns: 

req_auth = set_configurator(req_auth, "dns-luadns") 

if config.dns_nsone: 

req_auth = set_configurator(req_auth, "dns-nsone") 

if config.dns_ovh: 

req_auth = set_configurator(req_auth, "dns-ovh") 

if config.dns_rfc2136: 

req_auth = set_configurator(req_auth, "dns-rfc2136") 

if config.dns_route53: 

req_auth = set_configurator(req_auth, "dns-route53") 

if config.dns_sakuracloud: 

req_auth = set_configurator(req_auth, "dns-sakuracloud") 

logger.debug("Requested authenticator %s and installer %s", req_auth, req_inst) 

return req_auth, req_inst 

 

 

def diagnose_configurator_problem(cfg_type, requested, plugins): 

""" 

Raise the most helpful error message about a plugin being unavailable 

 

:param str cfg_type: either "installer" or "authenticator" 

:param str requested: the plugin that was requested 

:param .PluginsRegistry plugins: available plugins 

 

:raises error.PluginSelectionError: if there was a problem 

""" 

 

if requested: 

if requested not in plugins: 

msg = "The requested {0} plugin does not appear to be installed".format(requested) 

else: 

msg = ("The {0} plugin is not working; there may be problems with " 

"your existing configuration.\nThe error was: {1!r}" 

.format(requested, plugins[requested].problem)) 

elif cfg_type == "installer": 

from certbot.cli import cli_command 

msg = ('Certbot doesn\'t know how to automatically configure the web ' 

'server on this system. However, it can still get a certificate for ' 

'you. Please run "{0} certonly" to do so. You\'ll need to ' 

'manually configure your web server to use the resulting ' 

'certificate.').format(cli_command) 

else: 

msg = "{0} could not be determined or is not installed".format(cfg_type) 

raise errors.PluginSelectionError(msg)