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
|
"""A class that performs HTTP-01 challenges for Nginx"""
import logging
import os
from acme import challenges
from certbot import errors
from certbot.plugins import common
from certbot_nginx import obj
from certbot_nginx import nginxparser
from acme.magic_typing import List # pylint: disable=unused-import, no-name-in-module
logger = logging.getLogger(__name__)
class NginxHttp01(common.ChallengePerformer):
"""HTTP-01 authenticator for Nginx
:ivar configurator: NginxConfigurator object
:type configurator: :class:`~nginx.configurator.NginxConfigurator`
:ivar list achalls: Annotated
class:`~certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
challenges
:param list indices: Meant to hold indices of challenges in a
larger array. NginxHttp01 is capable of solving many challenges
at once which causes an indexing issue within NginxConfigurator
who must return all responses in order. Imagine NginxConfigurator
maintaining state about where all of the http-01 Challenges,
TLS-SNI-01 Challenges belong in the response array. This is an
optional utility.
"""
def __init__(self, configurator):
super(NginxHttp01, self).__init__(configurator)
self.challenge_conf = os.path.join(
configurator.config.config_dir, "le_http_01_cert_challenge.conf")
self._ipv6 = None
self._ipv6only = None
def perform(self):
"""Perform a challenge on Nginx.
:returns: list of :class:`certbot.acme.challenges.HTTP01Response`
:rtype: list
"""
if not self.achalls:
return []
responses = [x.response(x.account_key) for x in self.achalls]
# Set up the configuration
self._mod_config()
# Save reversible changes
self.configurator.save("HTTP Challenge", True)
return responses
def _mod_config(self):
"""Modifies Nginx config to include server_names_hash_bucket_size directive
and server challenge blocks.
:raises .MisconfigurationError:
Unable to find a suitable HTTP block in which to include
authenticator hosts.
"""
included = False
include_directive = ['\n', 'include', ' ', self.challenge_conf]
root = self.configurator.parser.config_root
bucket_directive = ['\n', 'server_names_hash_bucket_size', ' ', '128']
main = self.configurator.parser.parsed[root]
for line in main:
if line[0] == ['http']:
body = line[1]
found_bucket = False
posn = 0
for inner_line in body:
if inner_line[0] == bucket_directive[1]:
if int(inner_line[1]) < int(bucket_directive[3]):
body[posn] = bucket_directive
found_bucket = True
posn += 1
if not found_bucket:
body.insert(0, bucket_directive)
if include_directive not in body:
body.insert(0, include_directive)
included = True
break
if not included:
raise errors.MisconfigurationError(
'Certbot could not find a block to include '
'challenges in %s.' % root)
config = [self._make_or_mod_server_block(achall) for achall in self.achalls]
config = [x for x in config if x is not None]
config = nginxparser.UnspacedList(config)
self.configurator.reverter.register_file_creation(
True, self.challenge_conf)
with open(self.challenge_conf, "w") as new_conf:
nginxparser.dump(config, new_conf)
def _default_listen_addresses(self):
"""Finds addresses for a challenge block to listen on.
:returns: list of :class:`certbot_nginx.obj.Addr` to apply
:rtype: list
"""
addresses = [] # type: List[obj.Addr]
default_addr = "%s" % self.configurator.config.http01_port
ipv6_addr = "[::]:{0}".format(
self.configurator.config.http01_port)
port = self.configurator.config.http01_port
if self._ipv6 is None or self._ipv6only is None:
self._ipv6, self._ipv6only = self.configurator.ipv6_info(port)
ipv6, ipv6only = self._ipv6, self._ipv6only
if ipv6:
# If IPv6 is active in Nginx configuration
if not ipv6only:
# If ipv6only=on is not already present in the config
ipv6_addr = ipv6_addr + " ipv6only=on"
addresses = [obj.Addr.fromstring(default_addr),
obj.Addr.fromstring(ipv6_addr)]
logger.info(("Using default addresses %s and %s for authentication."),
default_addr,
ipv6_addr)
else:
addresses = [obj.Addr.fromstring(default_addr)]
logger.info("Using default address %s for authentication.",
default_addr)
return addresses
def _get_validation_path(self, achall):
return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token"))
def _make_server_block(self, achall):
"""Creates a server block for a challenge.
:param achall: Annotated HTTP-01 challenge
:type achall:
:class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
:param list addrs: addresses of challenged domain
:class:`list` of type :class:`~nginx.obj.Addr`
:returns: server block for the challenge host
:rtype: list
"""
addrs = self._default_listen_addresses()
block = [['listen', ' ', addr.to_string(include_default=False)] for addr in addrs]
# Ensure we 404 on any other request by setting a root
document_root = os.path.join(
self.configurator.config.work_dir, "http_01_nonexistent")
block.extend([['server_name', ' ', achall.domain],
['root', ' ', document_root],
self._location_directive_for_achall(achall)
])
# TODO: do we want to return something else if they otherwise access this block?
return [['server'], block]
def _location_directive_for_achall(self, achall):
validation = achall.validation(achall.account_key)
validation_path = self._get_validation_path(achall)
location_directive = [['location', ' ', '=', ' ', validation_path],
[['default_type', ' ', 'text/plain'],
['return', ' ', '200', ' ', validation]]]
return location_directive
def _make_or_mod_server_block(self, achall):
"""Modifies a server block to respond to a challenge.
:param achall: Annotated HTTP-01 challenge
:type achall:
:class:`certbot.achallenges.KeyAuthorizationAnnotatedChallenge`
"""
try:
vhosts = self.configurator.choose_redirect_vhosts(achall.domain,
'%i' % self.configurator.config.http01_port, create_if_no_match=True)
except errors.MisconfigurationError:
# Couldn't find either a matching name+port server block
# or a port+default_server block, so create a dummy block
return self._make_server_block(achall)
# len is max 1 because Nginx doesn't authenticate wildcards
# if len were or vhosts None, we would have errored
vhost = vhosts[0]
# Modify existing server block
location_directive = [self._location_directive_for_achall(achall)]
self.configurator.parser.add_server_directives(vhost,
location_directive)
rewrite_directive = [['rewrite', ' ', '^(/.well-known/acme-challenge/.*)',
' ', '$1', ' ', 'break']]
self.configurator.parser.add_server_directives(vhost,
rewrite_directive, insert_at_top=True)
|