Compare commits
28 Commits
remove-ngi
...
http01-ngi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97acf34198 | ||
|
|
cf541b7815 | ||
|
|
88e9e595a3 | ||
|
|
b1df26a4dc | ||
|
|
265afaf7a0 | ||
|
|
cd2188a681 | ||
|
|
ffe8870b9b | ||
|
|
77493d6e1a | ||
|
|
874a4c6773 | ||
|
|
5210588a29 | ||
|
|
7fa4a7a5b9 | ||
|
|
c36bca765d | ||
|
|
dc60852355 | ||
|
|
b11c685339 | ||
|
|
88d4a7da55 | ||
|
|
34217cf36e | ||
|
|
91d18d1234 | ||
|
|
f3395d487c | ||
|
|
787b0c358f | ||
|
|
6575ed955c | ||
|
|
b574bdf145 | ||
|
|
4c1bcd1bcd | ||
|
|
5d517a1f0d | ||
|
|
1afec8a8fd | ||
|
|
c9759bd1b6 | ||
|
|
557815bf94 | ||
|
|
9209d1eeca | ||
|
|
9c02b83773 |
@@ -26,6 +26,7 @@ from certbot_nginx import constants
|
||||
from certbot_nginx import nginxparser
|
||||
from certbot_nginx import parser
|
||||
from certbot_nginx import tls_sni_01
|
||||
from certbot_nginx import http_01
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -208,7 +209,8 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
:param str target_name: domain name
|
||||
:param bool create_if_no_match: If we should create a new vhost from default
|
||||
when there is no match found
|
||||
when there is no match found. If we can't choose a default, raise a
|
||||
MisconfigurationError.
|
||||
|
||||
:returns: ssl vhost associated with name
|
||||
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
|
||||
@@ -259,9 +261,9 @@ class NginxConfigurator(common.Installer):
|
||||
ipv6only_present = True
|
||||
return (ipv6_active, ipv6only_present)
|
||||
|
||||
def _vhost_from_duplicated_default(self, domain):
|
||||
def _vhost_from_duplicated_default(self, domain, port=None):
|
||||
if self.new_vhost is None:
|
||||
default_vhost = self._get_default_vhost()
|
||||
default_vhost = self._get_default_vhost(port)
|
||||
self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True)
|
||||
self.new_vhost.names = set()
|
||||
|
||||
@@ -276,15 +278,16 @@ class NginxConfigurator(common.Installer):
|
||||
name_block[0].append(name)
|
||||
self.parser.add_server_directives(vhost, name_block, replace=True)
|
||||
|
||||
def _get_default_vhost(self):
|
||||
def _get_default_vhost(self, port):
|
||||
vhost_list = self.parser.get_vhosts()
|
||||
# if one has default_server set, return that one
|
||||
default_vhosts = []
|
||||
for vhost in vhost_list:
|
||||
for addr in vhost.addrs:
|
||||
if addr.default:
|
||||
default_vhosts.append(vhost)
|
||||
break
|
||||
if port is None or self._port_matches(port, addr.get_port()):
|
||||
default_vhosts.append(vhost)
|
||||
break
|
||||
|
||||
if len(default_vhosts) == 1:
|
||||
return default_vhosts[0]
|
||||
@@ -366,7 +369,7 @@ class NginxConfigurator(common.Installer):
|
||||
return sorted(matches, key=lambda x: x['rank'])
|
||||
|
||||
|
||||
def choose_redirect_vhost(self, target_name, port):
|
||||
def choose_redirect_vhost(self, target_name, port, create_if_no_match=False):
|
||||
"""Chooses a single virtual host for redirect enhancement.
|
||||
|
||||
Chooses the vhost most closely matching target_name that is
|
||||
@@ -380,12 +383,27 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
:param str target_name: domain name
|
||||
:param str port: port number
|
||||
:param bool create_if_no_match: If we should create a new vhost from default
|
||||
when there is no match found. If we can't choose a default, raise a
|
||||
MisconfigurationError.
|
||||
|
||||
:returns: vhost associated with name
|
||||
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
matches = self._get_redirect_ranked_matches(target_name, port)
|
||||
return self._select_best_name_match(matches)
|
||||
vhost = self._select_best_name_match(matches)
|
||||
if not vhost and create_if_no_match:
|
||||
vhost = self._vhost_from_duplicated_default(target_name, port=port)
|
||||
return vhost
|
||||
|
||||
def _port_matches(self, test_port, matching_port):
|
||||
# test_port is a number, matching is a number or "" or None
|
||||
if matching_port == "" or matching_port is None:
|
||||
# if no port is specified, Nginx defaults to listening on port 80.
|
||||
return test_port == self.DEFAULT_LISTEN_PORT
|
||||
else:
|
||||
return test_port == matching_port
|
||||
|
||||
def _get_redirect_ranked_matches(self, target_name, port):
|
||||
"""Gets a ranked list of plaintextish port-listening vhosts matching target_name
|
||||
@@ -394,20 +412,13 @@ class NginxConfigurator(common.Installer):
|
||||
Rank by how well these match target_name.
|
||||
|
||||
:param str target_name: The name to match
|
||||
:param str port: port number
|
||||
:param str port: port number as a string
|
||||
:returns: list of dicts containing the vhost, the matching name, and
|
||||
the numerical rank
|
||||
:rtype: list
|
||||
|
||||
"""
|
||||
all_vhosts = self.parser.get_vhosts()
|
||||
def _port_matches(test_port, matching_port):
|
||||
# test_port is a number, matching is a number or "" or None
|
||||
if matching_port == "" or matching_port is None:
|
||||
# if no port is specified, Nginx defaults to listening on port 80.
|
||||
return test_port == self.DEFAULT_LISTEN_PORT
|
||||
else:
|
||||
return test_port == matching_port
|
||||
|
||||
def _vhost_matches(vhost, port):
|
||||
found_matching_port = False
|
||||
@@ -417,7 +428,7 @@ class NginxConfigurator(common.Installer):
|
||||
found_matching_port = (port == self.DEFAULT_LISTEN_PORT)
|
||||
else:
|
||||
for addr in vhost.addrs:
|
||||
if _port_matches(port, addr.get_port()) and addr.ssl == False:
|
||||
if self._port_matches(port, addr.get_port()) and addr.ssl == False:
|
||||
found_matching_port = True
|
||||
|
||||
if found_matching_port:
|
||||
@@ -840,7 +851,7 @@ class NginxConfigurator(common.Installer):
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.TLSSNI01]
|
||||
return [challenges.TLSSNI01, challenges.HTTP01]
|
||||
|
||||
# Entry point in main.py for performing challenges
|
||||
def perform(self, achalls):
|
||||
@@ -853,15 +864,20 @@ class NginxConfigurator(common.Installer):
|
||||
"""
|
||||
self._chall_out += len(achalls)
|
||||
responses = [None] * len(achalls)
|
||||
chall_doer = tls_sni_01.NginxTlsSni01(self)
|
||||
sni_doer = tls_sni_01.NginxTlsSni01(self)
|
||||
http_doer = http_01.NginxHttp01(self)
|
||||
|
||||
for i, achall in enumerate(achalls):
|
||||
# Currently also have chall_doer hold associated index of the
|
||||
# challenge. This helps to put all of the responses back together
|
||||
# when they are all complete.
|
||||
chall_doer.add_chall(achall, i)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
http_doer.add_chall(achall, i)
|
||||
else: # tls-sni-01
|
||||
sni_doer.add_chall(achall, i)
|
||||
|
||||
sni_response = chall_doer.perform()
|
||||
sni_response = sni_doer.perform()
|
||||
http_response = http_doer.perform()
|
||||
# Must restart in order to activate the challenges.
|
||||
# Handled here because we may be able to load up other challenge types
|
||||
self.restart()
|
||||
@@ -869,8 +885,9 @@ class NginxConfigurator(common.Installer):
|
||||
# Go through all of the challenges and assign them to the proper place
|
||||
# in the responses return value. All responses must be in the same order
|
||||
# as the original challenges.
|
||||
for i, resp in enumerate(sni_response):
|
||||
responses[chall_doer.indices[i]] = resp
|
||||
for chall_response, chall_doer in ((sni_response, sni_doer), (http_response, http_doer)):
|
||||
for i, resp in enumerate(chall_response):
|
||||
responses[chall_doer.indices[i]] = resp
|
||||
|
||||
return responses
|
||||
|
||||
|
||||
112
certbot-nginx/certbot_nginx/http_01.py
Normal file
112
certbot-nginx/certbot_nginx/http_01.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""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
|
||||
|
||||
|
||||
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 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 _add_bucket_directive(self):
|
||||
"""Modifies Nginx config to include server_names_hash_bucket_size directive."""
|
||||
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)
|
||||
break
|
||||
|
||||
def _mod_config(self):
|
||||
"""Modifies Nginx config to handle challenges.
|
||||
|
||||
"""
|
||||
self._add_bucket_directive()
|
||||
|
||||
for achall in self.achalls:
|
||||
self._mod_server_block(achall)
|
||||
|
||||
def _get_validation_path(self, achall):
|
||||
return os.sep + os.path.join(challenges.HTTP01.URI_ROOT_PATH, achall.chall.encode("token"))
|
||||
|
||||
def _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:
|
||||
vhost = self.configurator.choose_redirect_vhost(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
|
||||
|
||||
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]]]]
|
||||
|
||||
self.configurator.parser.add_server_directives(vhost,
|
||||
location_directive, replace=False)
|
||||
@@ -524,7 +524,7 @@ def _is_ssl_on_directive(entry):
|
||||
def _add_directives(directives, replace, block):
|
||||
"""Adds or replaces directives in a config block.
|
||||
|
||||
When replace=False, it's an error to try and add a directive that already
|
||||
When replace=False, it's an error to try and add a nonrepeatable directive that already
|
||||
exists in the config block with a conflicting value.
|
||||
|
||||
When replace=True and a directive with the same name already exists in the
|
||||
@@ -545,7 +545,7 @@ def _add_directives(directives, replace, block):
|
||||
|
||||
|
||||
INCLUDE = 'include'
|
||||
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE])
|
||||
REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE, 'location'])
|
||||
COMMENT = ' managed by Certbot'
|
||||
COMMENT_BLOCK = [' ', '#', COMMENT]
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
errors.PluginError, self.config.enhance, 'myhost', 'unknown_enhancement')
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual([challenges.TLSSNI01],
|
||||
self.assertEqual([challenges.TLSSNI01, challenges.HTTP01],
|
||||
self.config.get_chall_pref('myhost'))
|
||||
|
||||
def test_save(self):
|
||||
@@ -291,9 +291,11 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
parsed_migration_conf[0])
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.tls_sni_01.NginxTlsSni01.perform")
|
||||
@mock.patch("certbot_nginx.configurator.http_01.NginxHttp01.perform")
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.restart")
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.revert_challenge_config")
|
||||
def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_perform):
|
||||
def test_perform_and_cleanup(self, mock_revert, mock_restart, mock_http_perform,
|
||||
mock_tls_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
@@ -304,7 +306,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
), domain="localhost", account_key=self.rsa512jwk)
|
||||
achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=messages.ChallengeBody(
|
||||
chall=challenges.TLSSNI01(token=b"m8TdO1qik4JVFtgPPurJmg"),
|
||||
chall=challenges.HTTP01(token=b"m8TdO1qik4JVFtgPPurJmg"),
|
||||
uri="https://ca.org/chall1_uri",
|
||||
status=messages.Status("pending"),
|
||||
), domain="example.com", account_key=self.rsa512jwk)
|
||||
@@ -314,10 +316,12 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
achall2.response(self.rsa512jwk),
|
||||
]
|
||||
|
||||
mock_perform.return_value = expected
|
||||
mock_tls_perform.return_value = expected[:1]
|
||||
mock_http_perform.return_value = expected[1:]
|
||||
responses = self.config.perform([achall1, achall2])
|
||||
|
||||
self.assertEqual(mock_perform.call_count, 1)
|
||||
self.assertEqual(mock_tls_perform.call_count, 1)
|
||||
self.assertEqual(mock_http_perform.call_count, 1)
|
||||
self.assertEqual(responses, expected)
|
||||
|
||||
self.config.cleanup([achall1, achall2])
|
||||
|
||||
113
certbot-nginx/certbot_nginx/tests/http_01_test.py
Normal file
113
certbot-nginx/certbot_nginx/tests/http_01_test.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""Tests for certbot_nginx.http_01"""
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
import mock
|
||||
import six
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import achallenges
|
||||
|
||||
from certbot.plugins import common_test
|
||||
from certbot.tests import acme_util
|
||||
|
||||
from certbot_nginx.tests import util
|
||||
|
||||
|
||||
class HttpPerformTest(util.NginxTest):
|
||||
"""Test the NginxHttp01 challenge."""
|
||||
|
||||
account_key = common_test.AUTH_KEY
|
||||
achalls = [
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(token=b"kNdwjwOeX0I_A8DXt9Msmg"), "pending"),
|
||||
domain="www.example.com", account_key=account_key),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(
|
||||
token=b"\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y"
|
||||
b"\x80\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945"
|
||||
), "pending"),
|
||||
domain="ipv6.com", account_key=account_key),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(
|
||||
token=b"\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd"
|
||||
b"\xeb9\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4"
|
||||
), "pending"),
|
||||
domain="www.example.org", account_key=account_key),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(token=b"kNdwjxOeX0I_A8DXt9Msmg"), "pending"),
|
||||
domain="migration.com", account_key=account_key),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(HttpPerformTest, self).setUp()
|
||||
|
||||
config = util.get_nginx_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir, self.logs_dir)
|
||||
|
||||
from certbot_nginx import http_01
|
||||
self.http01 = http_01.NginxHttp01(config)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
shutil.rmtree(self.work_dir)
|
||||
|
||||
def test_perform0(self):
|
||||
responses = self.http01.perform()
|
||||
self.assertEqual([], responses)
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.NginxConfigurator.save")
|
||||
def test_perform1(self, mock_save):
|
||||
self.http01.add_chall(self.achalls[0])
|
||||
response = self.achalls[0].response(self.account_key)
|
||||
|
||||
responses = self.http01.perform()
|
||||
|
||||
self.assertEqual([response], responses)
|
||||
self.assertEqual(mock_save.call_count, 1)
|
||||
|
||||
def test_perform2(self):
|
||||
acme_responses = []
|
||||
for achall in self.achalls:
|
||||
self.http01.add_chall(achall)
|
||||
acme_responses.append(achall.response(self.account_key))
|
||||
|
||||
sni_responses = self.http01.perform()
|
||||
|
||||
self.assertEqual(len(sni_responses), 4)
|
||||
for i in six.moves.range(4):
|
||||
self.assertEqual(sni_responses[i], acme_responses[i])
|
||||
|
||||
def test_mod_config(self):
|
||||
self.http01.add_chall(self.achalls[0])
|
||||
self.http01.add_chall(self.achalls[2])
|
||||
|
||||
self.http01._mod_config() # pylint: disable=protected-access
|
||||
|
||||
self.http01.configurator.save()
|
||||
|
||||
self.http01.configurator.parser.load()
|
||||
|
||||
# vhosts = self.http01.configurator.parser.get_vhosts()
|
||||
|
||||
# for vhost in vhosts:
|
||||
# pass
|
||||
# if the name matches
|
||||
# check that the location block is in there and is correct
|
||||
|
||||
# if vhost.addrs == set(v_addr1):
|
||||
# response = self.achalls[0].response(self.account_key)
|
||||
# else:
|
||||
# response = self.achalls[2].response(self.account_key)
|
||||
# self.assertEqual(vhost.addrs, set(v_addr2_print))
|
||||
# self.assertEqual(vhost.names, set([response.z_domain.decode('ascii')]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -64,6 +64,7 @@ def get_nginx_configurator(
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
server="https://acme-server.org:443/new",
|
||||
tls_sni_01_port=5001,
|
||||
http01_port=80
|
||||
),
|
||||
name="nginx",
|
||||
version=version)
|
||||
|
||||
@@ -49,10 +49,10 @@ http {
|
||||
|
||||
server {
|
||||
# IPv4.
|
||||
listen 8081;
|
||||
listen 5002;
|
||||
# IPv6.
|
||||
listen [::]:8081 default ipv6only=on;
|
||||
server_name nginx.wtf;
|
||||
listen [::]:5002 default ipv6only=on;
|
||||
server_name nginx.wtf nginx2.wtf;
|
||||
|
||||
root $root/webroot;
|
||||
|
||||
|
||||
@@ -22,13 +22,20 @@ certbot_test_nginx () {
|
||||
"$@"
|
||||
}
|
||||
|
||||
certbot_test_nginx --domains nginx.wtf run
|
||||
echo | openssl s_client -connect localhost:5001 \
|
||||
| openssl x509 -out $root/nginx.pem
|
||||
diff -q $root/nginx.pem $root/conf/live/nginx.wtf/cert.pem
|
||||
test_deployment_and_rollback() {
|
||||
# Arguments: certname
|
||||
echo | openssl s_client -connect localhost:5001 \
|
||||
| openssl x509 -out $root/nginx.pem
|
||||
diff -q $root/nginx.pem "$root/conf/live/$1/cert.pem"
|
||||
|
||||
certbot_test_nginx rollback --checkpoints 9001
|
||||
diff -q <(echo "$original") $nginx_conf
|
||||
certbot_test_nginx rollback --checkpoints 9001
|
||||
diff -q <(echo "$original") $nginx_conf
|
||||
}
|
||||
|
||||
certbot_test_nginx --domains nginx.wtf run
|
||||
test_deployment_and_rollback nginx.wtf
|
||||
certbot_test_nginx --domains nginx2.wtf --preferred-challenges http
|
||||
test_deployment_and_rollback nginx2.wtf
|
||||
|
||||
# note: not reached if anything above fails, hence "killall" at the
|
||||
# top
|
||||
|
||||
Reference in New Issue
Block a user