Compare commits

...

62 Commits

Author SHA1 Message Date
Brad Warren
0e4bd0389e quiet and fast 2019-08-06 22:37:26 +02:00
Adrien Ferrand
b06187f606 Merge remote-tracking branch 'origin/master' into fix-integration-tests-windows 2019-08-06 22:36:52 +02:00
Adrien Ferrand
9801eef297 Add conditionally pywin in certbot-ci like in certbot 2019-08-06 22:32:58 +02:00
Adrien Ferrand
c04ff72765 Update certbot-ci/certbot_integration_tests/certbot_tests/assertions.py
Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-08-06 21:43:33 +02:00
Adrien Ferrand
f4fbed7714 Merge branch 'master' into fix-integration-tests-windows 2019-08-02 21:15:13 +02:00
Adrien Ferrand
30de14453a Fix for virtualenv on Python 3.7.4 for Windows 2019-08-02 14:03:07 +02:00
Adrien Ferrand
fa93b7e3ef Improve assertions. Control the keyword echoed in hooks 2019-08-02 13:14:08 +02:00
Adrien Ferrand
45ae50a2be Update tox target 2019-08-02 12:57:27 +02:00
Adrien Ferrand
2d2e661fdd Various cleanup, ensure to remove workspace after tests 2019-08-02 12:21:19 +02:00
Adrien Ferrand
20e7b5c503 Clean debug code 2019-08-02 12:14:16 +02:00
Adrien Ferrand
5e5b88a453 Remove injection of nginx in PATH 2019-08-02 12:11:08 +02:00
Adrien Ferrand
4114afb17a Pin pywin32 for tests, give a minimal requirement for certbot. 2019-08-02 12:10:03 +02:00
Adrien Ferrand
ef6a71fc13 Merge branch 'master' into fix-integration-tests-windows 2019-08-01 20:11:47 +02:00
Adrien Ferrand
3476c8c6db Update appveyor.yml 2019-08-01 20:09:25 +02:00
Adrien Ferrand
93e0109dba Set PYTEST_ADDOPTS silently 2019-08-01 20:09:25 +02:00
Adrien Ferrand
0547d2218f Update pebble_artifacts.py 2019-08-01 20:09:25 +02:00
Adrien Ferrand
990c6d3c51 Use again official pebble 2019-08-01 20:09:25 +02:00
Adrien Ferrand
115175c658 Re-enable all AppVeyor envs 2019-08-01 20:09:25 +02:00
Adrien Ferrand
905030c5a1 Optimize integration env 2019-08-01 20:09:25 +02:00
Adrien Ferrand
2488c98862 Clean code 2019-08-01 20:09:25 +02:00
Adrien Ferrand
d418b1927b Assert file permissions for world on private keys 2019-08-01 20:09:25 +02:00
Adrien Ferrand
b50aed41f6 Various fixes 2019-08-01 20:09:25 +02:00
Adrien Ferrand
2165bfb903 Use forked pebble for now 2019-08-01 20:09:25 +02:00
Adrien Ferrand
fe196c933a Specific config for appveyor 2019-08-01 20:09:25 +02:00
Adrien Ferrand
2b1fc85b69 Change mode 2019-08-01 20:09:25 +02:00
Adrien Ferrand
5607721a9b Fix several things 2019-08-01 20:09:25 +02:00
Adrien Ferrand
cf6b289987 No broken tests in certbot core anymore 2019-08-01 20:09:25 +02:00
Adrien Ferrand
0118a65c30 Permissions comparison for Windows 2019-08-01 20:09:25 +02:00
Adrien Ferrand
4e1b88a34e Test for permissions on Windows 2019-08-01 20:09:25 +02:00
Adrien Ferrand
6a9d9e807b New tests fixed 2019-08-01 20:09:25 +02:00
Adrien Ferrand
379f123ce1 New approach to be compliant with Linux and Windows on hook scripts 2019-08-01 20:09:25 +02:00
Adrien Ferrand
addee821dd Use a python hook script, to prepare cross-platform 2019-08-01 20:09:25 +02:00
Adrien Ferrand
2378de966c Fix a lot of tests 2019-08-01 20:09:25 +02:00
Adrien Ferrand
73517cc30a Add integration target 2019-08-01 20:09:25 +02:00
Adrien Ferrand
cb16115ace Add broken_on_windows for integration tests 2019-08-01 20:09:25 +02:00
Adrien Ferrand
dd86189c7e Fix probe fd close 2019-08-01 20:09:25 +02:00
Adrien Ferrand
228c02fae9 Platform independent hooks 2019-08-01 20:09:24 +02:00
Adrien Ferrand
2bbc62922b Start fixing tests 2019-08-01 20:09:24 +02:00
Adrien Ferrand
f56bcc763f Update os.py 2019-08-01 10:25:06 +02:00
Adrien Ferrand
9357f0dbc7 Update filesystem.py 2019-08-01 02:29:28 +02:00
Adrien Ferrand
96ffb3cf59 Update filesystem.py 2019-08-01 01:34:29 +02:00
Adrien Ferrand
811203e526 Update filesystem.py 2019-08-01 01:30:21 +02:00
Adrien Ferrand
132f08101f Fix lint 2019-08-01 01:21:59 +02:00
Adrien Ferrand
7b9e811665 Update os.py 2019-08-01 01:06:14 +02:00
Adrien Ferrand
5e6119d758 Update os_test.py 2019-08-01 00:40:00 +02:00
Adrien Ferrand
11e5f72e7e Forbid os.access 2019-08-01 00:39:15 +02:00
Adrien Ferrand
2f6c9e4a2c Fix pylint 2019-07-31 12:35:50 +02:00
Adrien Ferrand
25a080db6e Update util_test.py 2019-07-31 06:54:28 +02:00
Adrien Ferrand
9cd359ece4 Update certbot/tests/compat/filesystem_test.py
Co-Authored-By: Brad Warren <bmw@users.noreply.github.com>
2019-07-31 06:52:17 +02:00
Adrien Ferrand
65f464efeb Adapt coverage 2019-07-30 21:27:44 +02:00
Adrien Ferrand
9885c30fb1 Fix lint 2019-07-30 21:15:17 +02:00
Adrien Ferrand
af701498f6 Corrections 2019-07-30 21:07:51 +02:00
Adrien Ferrand
e2a9adf9e1 Adapt coverage 2019-07-26 11:53:33 +02:00
Adrien Ferrand
69fb019663 Fix mypy 2019-07-26 11:01:20 +02:00
Adrien Ferrand
dbb3712236 Fix lint 2019-07-26 11:01:20 +02:00
Adrien Ferrand
839821920b Fix context manager 2019-07-26 11:01:20 +02:00
Adrien Ferrand
55af6736a1 More elegant mock 2019-07-26 11:01:20 +02:00
Adrien Ferrand
1dc4f5386e No broken unit test on Windows anymore 2019-07-26 11:01:19 +02:00
Adrien Ferrand
a6a8c93295 Fix util_test 2019-07-26 11:01:19 +02:00
Adrien Ferrand
16310a019e Remove the temporary decorator @broken_on_windows 2019-07-26 11:01:19 +02:00
Adrien Ferrand
a664bead38 Fix hook executable test 2019-07-26 11:01:19 +02:00
Adrien Ferrand
de11ec7370 Fix account_tests 2019-07-26 11:01:18 +02:00
12 changed files with 274 additions and 264 deletions

View File

@@ -34,98 +34,6 @@ extended-test-suite: &extended-test-suite
matrix: matrix:
include: include:
# Main test suite
- python: "2.7"
env: ACME_SERVER=pebble TOXENV=integration
sudo: required
services: docker
<<: *not-on-master
# This job is always executed, including on master
- python: "2.7"
env: TOXENV=py27-cover FYI="py27 tests + code coverage"
- python: "2.7"
env: TOXENV=lint
<<: *not-on-master
- python: "3.4"
env: TOXENV=mypy
<<: *not-on-master
- python: "3.5"
env: TOXENV=mypy
<<: *not-on-master
- python: "2.7"
# Ubuntu Trusty or older must be used because the oldest version of
# cryptography we support cannot be compiled against the version of
# OpenSSL in Xenial or newer.
dist: trusty
env: TOXENV='py27-{acme,apache,certbot,dns,nginx}-oldest'
sudo: required
services: docker
<<: *not-on-master
- python: "3.4"
env: TOXENV=py34
sudo: required
services: docker
<<: *not-on-master
- python: "3.7"
dist: xenial
env: TOXENV=py37
sudo: required
services: docker
<<: *not-on-master
- sudo: required
env: TOXENV=apache_compat
services: docker
before_install:
addons:
<<: *not-on-master
- sudo: required
env: TOXENV=le_auto_xenial
services: docker
<<: *not-on-master
- python: "2.7"
env: TOXENV=apacheconftest-with-pebble
sudo: required
services: docker
<<: *not-on-master
- python: "2.7"
env: TOXENV=nginxroundtrip
<<: *not-on-master
# Extended test suite on cron jobs and pushes to tested branches other than master
- sudo: required
env: TOXENV=nginx_compat
services: docker
before_install:
addons:
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-apache2
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-leauto-upgrades
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
git:
depth: false # This is needed to have the history to checkout old versions of certbot-auto.
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-certonly-standalone
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "2.7"
env:
- TOXENV=travis-test-farm-sdists
- secure: "f+j/Lj9s1lcuKo5sEFrlRd1kIAMnIJI4z0MTI7QF8jl9Fkmbx7KECGzw31TNgzrOSzxSapHbcueFYvNCLKST+kE/8ogMZBbwqXfEDuKpyF6BY3uYoJn+wPVE5pIb8Hhe08xPte8TTDSMIyHI3EyTfcAKrIreauoArePvh/cRvSw="
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: TOXENV=py37 CERTBOT_NO_PIN=1
<<: *extended-test-suite
- python: "2.7" - python: "2.7"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required sudo: required
@@ -156,19 +64,6 @@ matrix:
sudo: required sudo: required
services: docker services: docker
<<: *extended-test-suite <<: *extended-test-suite
- python: "3.4"
env: TOXENV=py34
<<: *extended-test-suite
- python: "3.5"
env: TOXENV=py35
<<: *extended-test-suite
- python: "3.6"
env: TOXENV=py36
<<: *extended-test-suite
- python: "3.7"
dist: xenial
env: TOXENV=py37
<<: *extended-test-suite
- python: "3.4" - python: "3.4"
env: ACME_SERVER=boulder-v1 TOXENV=integration env: ACME_SERVER=boulder-v1 TOXENV=integration
sudo: required sudo: required
@@ -211,46 +106,6 @@ matrix:
sudo: required sudo: required
services: docker services: docker
<<: *extended-test-suite <<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_jessie
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=le_auto_centos6
services: docker
<<: *extended-test-suite
- sudo: required
env: TOXENV=docker_dev
services: docker
addons:
apt:
packages: # don't install nginx and apache
- libaugeas0
<<: *extended-test-suite
- language: generic
env: TOXENV=py27
os: osx
# Using this osx_image is a workaround for
# https://travis-ci.community/t/xcode-8-3-homebrew-outdated-error/3798.
osx_image: xcode10.2
addons:
homebrew:
packages:
- augeas
- python2
<<: *extended-test-suite
- language: generic
env: TOXENV=py3
os: osx
# Using this osx_image is a workaround for
# https://travis-ci.community/t/xcode-8-3-homebrew-outdated-error/3798.
osx_image: xcode10.2
addons:
homebrew:
packages:
- augeas
- python3
<<: *extended-test-suite
# container-based infrastructure # container-based infrastructure
sudo: false sudo: false
@@ -280,13 +135,3 @@ after_success: '[ "$TOXENV" == "py27-cover" ] && codecov -F linux'
notifications: notifications:
email: false email: false
irc:
channels:
# This is set to a secure variable to prevent forks from sending
# notifications. This value was created by installing
# https://github.com/travis-ci/travis.rb and running
# `travis encrypt "chat.freenode.net#certbot-devel"`.
- secure: "EWW66E2+KVPZyIPR8ViENZwfcup4Gx3/dlimmAZE0WuLwxDCshBBOd3O8Rf6pBokEoZlXM5eDT6XdyJj8n0DLslgjO62pExdunXpbcMwdY7l1ELxX2/UbnDTE6UnPYa09qVBHNG7156Z6yE0x2lH4M9Ykvp0G0cubjPQHylAwo0="
on_cancel: never
on_success: never
on_failure: always

View File

@@ -2,8 +2,7 @@ image: Visual Studio 2015
environment: environment:
matrix: matrix:
- TOXENV: py35 - TOXENV: integration-certbot
- TOXENV: py37-cover
branches: branches:
only: only:
@@ -24,14 +23,16 @@ init:
install: install:
# Use Python 3.7 by default # Use Python 3.7 by default
- "SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%" - SET PATH=C:\\Python37;C:\\Python37\\Scripts;%PATH%
# Using 4 processes is proven to be the most efficient integration tests config for AppVeyor
- IF %TOXENV%==integration-certbot SET PYTEST_ADDOPTS=--numprocesses=4
# Check env # Check env
- "python --version" - python --version
# Upgrade pip to avoid warnings # Upgrade pip to avoid warnings
- "python -m pip install --upgrade pip" - python -m pip install --upgrade pip
# Ready to install tox and coverage # Ready to install tox and coverage
# tools/pip_install.py is used to pin packages to a known working version. # tools/pip_install.py is used to pin packages to a known working version.
- "python tools\\pip_install.py tox codecov" - python tools\\pip_install.py tox codecov
build: off build: off

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env python
import sys
import os
hook_script_type = os.path.basename(os.path.dirname(sys.argv[1]))
if hook_script_type == 'deploy' and ('RENEWED_DOMAINS' not in os.environ or 'RENEWED_LINEAGE' not in os.environ):
sys.stderr.write('Environment variables not properly set!\n')
sys.exit(1)
with open(sys.argv[2], 'a') as file_h:
file_h.write(hook_script_type + '\n')

View File

@@ -1,6 +1,17 @@
"""This module contains advanced assertions for the certbot integration tests.""" """This module contains advanced assertions for the certbot integration tests."""
import os import os
import grp try:
import grp
POSIX_MODE = True
except ImportError:
import win32api
import win32security
import ntsecuritycon
POSIX_MODE = False
EVERYBODY_SID = 'S-1-1-0'
SYSTEM_SID = 'S-1-5-18'
ADMINS_SID = 'S-1-5-32-544'
def assert_hook_execution(probe_path, probe_content): def assert_hook_execution(probe_path, probe_content):
@@ -10,9 +21,10 @@ def assert_hook_execution(probe_path, probe_content):
:param probe_content: content expected when the hook is executed :param probe_content: content expected when the hook is executed
""" """
with open(probe_path, 'r') as file: with open(probe_path, 'r') as file:
lines = file.readlines() data = file.read()
assert '{0}{1}'.format(probe_content, os.linesep) in lines lines = [line.strip() for line in data.splitlines()]
assert probe_content in lines
def assert_saved_renew_hook(config_dir, lineage): def assert_saved_renew_hook(config_dir, lineage):
@@ -38,16 +50,51 @@ def assert_cert_count_for_lineage(config_dir, lineage, count):
assert len(certs) == count assert len(certs) == count
def assert_equals_permissions(file1, file2, mask): def assert_equals_group_permissions(file1, file2):
""" """
Assert that permissions on two files are identical in respect to a given umask. Assert that two files have the same permissions for group owner.
:param file1: first file path to compare :param file1: first file path to compare
:param file2: second file path to compare :param file2: second file path to compare
:param mask: 3-octal representation of a POSIX umask under which the two files mode
should match (eg. 0o074 will test RWX on group and R on world)
""" """
mode_file1 = os.stat(file1).st_mode & mask # On Windows there is no group, so this assertion does nothing on this platform
mode_file2 = os.stat(file2).st_mode & mask if POSIX_MODE:
mode_file1 = os.stat(file1).st_mode & 0o070
mode_file2 = os.stat(file2).st_mode & 0o070
assert mode_file1 == mode_file2
def assert_equals_world_read_permissions(file1, file2):
"""
Assert that two files have the same read permissions for everyone.
:param file1: first file path to compare
:param file2: second file path to compare
"""
if POSIX_MODE:
mode_file1 = os.stat(file1).st_mode & 0o004
mode_file2 = os.stat(file2).st_mode & 0o004
else:
everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
security1 = win32security.GetFileSecurity(file1, win32security.DACL_SECURITY_INFORMATION)
dacl1 = security1.GetSecurityDescriptorDacl()
mode_file1 = dacl1.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': everybody,
})
mode_file1 = mode_file1 & ntsecuritycon.FILE_GENERIC_READ
security2 = win32security.GetFileSecurity(file2, win32security.DACL_SECURITY_INFORMATION)
dacl2 = security2.GetSecurityDescriptorDacl()
mode_file2 = dacl2.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': everybody,
})
mode_file2 = mode_file2 & ntsecuritycon.FILE_GENERIC_READ
assert mode_file1 == mode_file2 assert mode_file1 == mode_file2
@@ -57,20 +104,57 @@ def assert_equals_group_owner(file1, file2):
Assert that two files have the same group owner. Assert that two files have the same group owner.
:param file1: first file path to compare :param file1: first file path to compare
:param file2: second file path to compare :param file2: second file path to compare
:return:
""" """
group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0] # On Windows there is no group, so this assertion does nothing on this platform
group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0] if POSIX_MODE:
group_owner_file1 = grp.getgrgid(os.stat(file1).st_gid)[0]
group_owner_file2 = grp.getgrgid(os.stat(file2).st_gid)[0]
assert group_owner_file1 == group_owner_file2 assert group_owner_file1 == group_owner_file2
def assert_world_permissions(file, mode): def assert_world_no_permissions(file):
""" """
Assert that a file has the expected world permission. Assert that the given file is not world-readable.
:param file: file path to check :param file: path of the file to check
:param mode: world permissions mode expected
""" """
mode_file_all = os.stat(file).st_mode & 0o007 if POSIX_MODE:
mode_file_all = os.stat(file).st_mode & 0o007
assert mode_file_all == 0
else:
security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
mode = dacl.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
})
assert mode_file_all == mode assert not mode
def assert_world_read_permissions(file):
"""
Assert that the given file is world-readable, but not world-writable or world-executable.
:param file: path of the file to check
"""
if POSIX_MODE:
mode_file_all = os.stat(file).st_mode & 0o007
assert mode_file_all == 4
else:
security = win32security.GetFileSecurity(file, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
mode = dacl.GetEffectiveRightsFromAcl({
'TrusteeForm': win32security.TRUSTEE_IS_SID,
'TrusteeType': win32security.TRUSTEE_IS_USER,
'Identifier': win32security.ConvertStringSidToSid(EVERYBODY_SID),
})
assert not mode & ntsecuritycon.FILE_GENERIC_WRITE
assert not mode & ntsecuritycon.FILE_GENERIC_EXECUTE
assert mode & ntsecuritycon.FILE_GENERIC_READ == ntsecuritycon.FILE_GENERIC_READ
def _get_current_user():
account_name = win32api.GetUserNameEx(win32api.NameSamCompatible)
return win32security.LookupAccountName(None, account_name)[0]

View File

@@ -1,4 +1,5 @@
"""Module to handle the context of integration tests.""" """Module to handle the context of integration tests."""
import logging
import os import os
import shutil import shutil
import sys import sys
@@ -30,7 +31,10 @@ class IntegrationTestsContext(object):
self.workspace = tempfile.mkdtemp() self.workspace = tempfile.mkdtemp()
self.config_dir = os.path.join(self.workspace, 'conf') self.config_dir = os.path.join(self.workspace, 'conf')
self.hook_probe = tempfile.mkstemp(dir=self.workspace)[1]
probe = tempfile.mkstemp(dir=self.workspace)
os.close(probe[0])
self.hook_probe = probe[1]
self.manual_dns_auth_hook = ( self.manual_dns_auth_hook = (
'{0} -c "import os; import requests; import json; ' '{0} -c "import os; import requests; import json; '

View File

@@ -11,8 +11,11 @@ from os.path import join, exists
import pytest import pytest
from certbot_integration_tests.certbot_tests import context as certbot_context from certbot_integration_tests.certbot_tests import context as certbot_context
from certbot_integration_tests.certbot_tests.assertions import ( from certbot_integration_tests.certbot_tests.assertions import (
assert_hook_execution, assert_saved_renew_hook, assert_cert_count_for_lineage, assert_hook_execution, assert_saved_renew_hook,
assert_world_permissions, assert_equals_group_owner, assert_equals_permissions, assert_cert_count_for_lineage,
assert_world_no_permissions, assert_world_read_permissions,
assert_equals_group_owner, assert_equals_group_permissions, assert_equals_world_read_permissions,
EVERYBODY_SID
) )
from certbot_integration_tests.utils import misc from certbot_integration_tests.utils import misc
@@ -84,9 +87,9 @@ def test_http_01(context):
context.certbot([ context.certbot([
'--domains', certname, '--preferred-challenges', 'http-01', 'run', '--domains', certname, '--preferred-challenges', 'http-01', 'run',
'--cert-name', certname, '--cert-name', certname,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'deploy') assert_hook_execution(context.hook_probe, 'deploy')
@@ -104,9 +107,9 @@ def test_manual_http_auth(context):
'--cert-name', certname, '--cert-name', certname,
'--manual-auth-hook', scripts[0], '--manual-auth-hook', scripts[0],
'--manual-cleanup-hook', scripts[1], '--manual-cleanup-hook', scripts[1],
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) '--renew-hook', misc.echo('renew', context.hook_probe),
]) ])
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -122,9 +125,9 @@ def test_manual_dns_auth(context):
'run', '--cert-name', certname, 'run', '--cert-name', certname,
'--manual-auth-hook', context.manual_dns_auth_hook, '--manual-auth-hook', context.manual_dns_auth_hook,
'--manual-cleanup-hook', context.manual_dns_cleanup_hook, '--manual-cleanup-hook', context.manual_dns_cleanup_hook,
'--pre-hook', 'echo wtf.pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('wtf_pre', context.hook_probe),
'--post-hook', 'echo wtf.post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('wtf_post', context.hook_probe),
'--renew-hook', 'echo renew >> "{0}"'.format(context.hook_probe) '--renew-hook', misc.echo('renew', context.hook_probe),
]) ])
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -173,21 +176,19 @@ def test_renew_files_permissions(context):
certname = context.get_domain('renew') certname = context.get_domain('renew')
context.certbot(['-d', certname]) context.certbot(['-d', certname])
privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
assert_world_permissions( assert_world_no_permissions(privkey1)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0)
context.certbot(['renew']) context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
assert_world_permissions( assert_world_no_permissions(privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0) assert_equals_group_owner(privkey1, privkey2)
assert_equals_group_owner( assert_equals_world_read_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), assert_equals_group_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'))
assert_equals_permissions(
join(context.config_dir, 'archive', certname, 'privkey1.pem'),
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074)
def test_renew_with_hook_scripts(context): def test_renew_with_hook_scripts(context):
@@ -211,15 +212,35 @@ def test_renew_files_propagate_permissions(context):
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
os.chmod(join(context.config_dir, 'archive', certname, 'privkey1.pem'), 0o444) privkey1 = join(context.config_dir, 'archive', certname, 'privkey1.pem')
privkey2 = join(context.config_dir, 'archive', certname, 'privkey2.pem')
if os.name != 'nt':
os.chmod(privkey1, 0o444)
else:
import win32security
import ntsecuritycon
# Get the current DACL of the private key
security = win32security.GetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION)
dacl = security.GetSecurityDescriptorDacl()
# Create a read permission for Everybody group
everybody = win32security.ConvertStringSidToSid(EVERYBODY_SID)
dacl.AddAccessAllowedAce(win32security.ACL_REVISION, ntsecuritycon.FILE_GENERIC_READ, everybody)
# Apply the updated DACL to the private key
security.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(privkey1, win32security.DACL_SECURITY_INFORMATION, security)
context.certbot(['renew']) context.certbot(['renew'])
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
assert_world_permissions( if os.name != 'nt':
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 4) # On Linux, read world permissions + all group permissions will be copied from the previous private key
assert_equals_permissions( assert_world_read_permissions(privkey2)
join(context.config_dir, 'archive', certname, 'privkey1.pem'), assert_equals_world_read_permissions(privkey1, privkey2)
join(context.config_dir, 'archive', certname, 'privkey2.pem'), 0o074) assert_equals_group_permissions(privkey1, privkey2)
else:
# On Windows, world will never have any permissions, and group permission is irrelevant for this platform
assert_world_no_permissions(privkey2)
def test_graceful_renew_it_is_not_time(context): def test_graceful_renew_it_is_not_time(context):
@@ -229,7 +250,7 @@ def test_graceful_renew_it_is_not_time(context):
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
force_renew=False) force_renew=False)
assert_cert_count_for_lineage(context.config_dir, certname, 1) assert_cert_count_for_lineage(context.config_dir, certname, 1)
@@ -250,7 +271,7 @@ def test_graceful_renew_it_is_time(context):
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file: with open(join(context.config_dir, 'renewal', '{0}.conf'.format(certname)), 'w') as file:
file.writelines(lines) file.writelines(lines)
context.certbot(['renew', '--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe)], context.certbot(['renew', '--deploy-hook', misc.echo('deploy', context.hook_probe)],
force_renew=False) force_renew=False)
assert_cert_count_for_lineage(context.config_dir, certname, 2) assert_cert_count_for_lineage(context.config_dir, certname, 2)
@@ -317,9 +338,9 @@ def test_renew_hook_override(context):
context.certbot([ context.certbot([
'certonly', '-d', certname, 'certonly', '-d', certname,
'--preferred-challenges', 'http-01', '--preferred-challenges', 'http-01',
'--pre-hook', 'echo pre >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('pre', context.hook_probe),
'--post-hook', 'echo post >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('post', context.hook_probe),
'--deploy-hook', 'echo deploy >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'pre') assert_hook_execution(context.hook_probe, 'pre')
@@ -330,14 +351,14 @@ def test_renew_hook_override(context):
open(context.hook_probe, 'w').close() open(context.hook_probe, 'w').close()
context.certbot([ context.certbot([
'renew', '--cert-name', certname, 'renew', '--cert-name', certname,
'--pre-hook', 'echo pre-override >> "{0}"'.format(context.hook_probe), '--pre-hook', misc.echo('pre_override', context.hook_probe),
'--post-hook', 'echo post-override >> "{0}"'.format(context.hook_probe), '--post-hook', misc.echo('post_override', context.hook_probe),
'--deploy-hook', 'echo deploy-override >> "{0}"'.format(context.hook_probe) '--deploy-hook', misc.echo('deploy_override', context.hook_probe),
]) ])
assert_hook_execution(context.hook_probe, 'pre-override') assert_hook_execution(context.hook_probe, 'pre_override')
assert_hook_execution(context.hook_probe, 'post-override') assert_hook_execution(context.hook_probe, 'post_override')
assert_hook_execution(context.hook_probe, 'deploy-override') assert_hook_execution(context.hook_probe, 'deploy_override')
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
assert_hook_execution(context.hook_probe, 'pre') assert_hook_execution(context.hook_probe, 'pre')
with pytest.raises(AssertionError): with pytest.raises(AssertionError):
@@ -349,11 +370,11 @@ def test_renew_hook_override(context):
open(context.hook_probe, 'w').close() open(context.hook_probe, 'w').close()
context.certbot(['renew', '--cert-name', certname]) context.certbot(['renew', '--cert-name', certname])
assert_hook_execution(context.hook_probe, 'pre-override') assert_hook_execution(context.hook_probe, 'pre_override')
assert_hook_execution(context.hook_probe, 'post-override') assert_hook_execution(context.hook_probe, 'post_override')
assert_hook_execution(context.hook_probe, 'deploy-override') assert_hook_execution(context.hook_probe, 'deploy_override')
def test_invalid_domain_with_dns_challenge(context): def test_invalid_domain_with_dns_challenge(context):
"""Test certificate issuance failure with DNS-01 challenge.""" """Test certificate issuance failure with DNS-01 challenge."""
# Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'. # Manual dns auth hooks from misc are designed to fail if the domain contains 'fail-*'.
@@ -512,7 +533,7 @@ def test_revoke_multiple_lineages(context):
data = file.read() data = file.read()
data = re.sub('archive_dir = .*\n', data = re.sub('archive_dir = .*\n',
'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1)), 'archive_dir = {0}\n'.format(join(context.config_dir, 'archive', cert1).replace('\\', '\\\\')),
data) data)
with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file: with open(join(context.config_dir, 'renewal', '{0}.conf'.format(cert2)), 'w') as file:

View File

@@ -3,9 +3,11 @@ Misc module contains stateless functions that could be used during pytest execut
or outside during setup/teardown of the integration tests environment. or outside during setup/teardown of the integration tests environment.
""" """
import contextlib import contextlib
import logging
import errno import errno
import multiprocessing import multiprocessing
import os import os
import re
import shutil import shutil
import stat import stat
import subprocess import subprocess
@@ -62,6 +64,10 @@ class GracefulTCPServer(socketserver.TCPServer):
allow_reuse_address = True allow_reuse_address = True
def _run_server(port):
GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
@contextlib.contextmanager @contextlib.contextmanager
def create_http_server(port): def create_http_server(port):
""" """
@@ -74,10 +80,7 @@ def create_http_server(port):
current_cwd = os.getcwd() current_cwd = os.getcwd()
webroot = tempfile.mkdtemp() webroot = tempfile.mkdtemp()
def run(): process = multiprocessing.Process(target=_run_server, args=(port,))
GracefulTCPServer(('', port), SimpleHTTPServer.SimpleHTTPRequestHandler).serve_forever()
process = multiprocessing.Process(target=run)
try: try:
# SimpleHTTPServer is designed to serve files from the current working directory at the # SimpleHTTPServer is designed to serve files from the current working directory at the
@@ -119,15 +122,9 @@ def generate_test_file_hooks(config_dir, hook_probe):
:param str config_dir: current certbot config directory :param str config_dir: current certbot config directory
:param hook_probe: path to the hook probe to test hook scripts execution :param hook_probe: path to the hook probe to test hook scripts execution
""" """
if sys.platform == 'win32': hook_path = pkg_resources.resource_filename('certbot_integration_tests', 'assets/hook.py')
extension = 'bat'
else:
extension = 'sh'
renewal_hooks_dirs = list_renewal_hooks_dirs(config_dir) for hook_dir in list_renewal_hooks_dirs(config_dir):
renewal_deploy_hook_path = os.path.join(renewal_hooks_dirs[1], 'hook.sh')
for hook_dir in renewal_hooks_dirs:
# We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of # We want an equivalent of bash `chmod -p $HOOK_DIR, that does not fail if one folder of
# the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an # the hierarchy already exists. It is not the case of os.makedirs. Python 3 has an
# optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not. # optional parameter `exists_ok` to not fail on existing dir, but Python 2.7 does not.
@@ -137,26 +134,25 @@ def generate_test_file_hooks(config_dir, hook_probe):
except OSError as error: except OSError as error:
if error.errno != errno.EEXIST: if error.errno != errno.EEXIST:
raise raise
hook_path = os.path.join(hook_dir, 'hook.{0}'.format(extension))
if extension == 'sh':
data = '''\
#!/bin/bash -xe
if [ "$0" = "{0}" ]; then
if [ -z "$RENEWED_DOMAINS" -o -z "$RENEWED_LINEAGE" ]; then
echo "Environment variables not properly set!" >&2
exit 1
fi
fi
echo $(basename $(dirname "$0")) >> "{1}"\
'''.format(renewal_deploy_hook_path, hook_probe)
else:
# TODO: Write the equivalent bat file for Windows
data = '''\
''' if os.name != 'nt':
with open(hook_path, 'w') as file: entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.sh')
file.write(data) entrypoint_script = '''\
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IEXEC) #!/usr/bin/env bash
set -e
"{0}" "{1}" "{2}" "{3}"
'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
else:
entrypoint_script_path = os.path.join(hook_dir, 'entrypoint.bat')
entrypoint_script = '''\
@echo off
"{0}" "{1}" "{2}" "{3}"
'''.format(sys.executable, hook_path, entrypoint_script_path, hook_probe)
with open(entrypoint_script_path, 'w') as file_h:
file_h.write(entrypoint_script)
os.chmod(entrypoint_script_path, os.stat(entrypoint_script_path).st_mode | stat.S_IEXEC)
@contextlib.contextmanager @contextlib.contextmanager
@@ -193,7 +189,7 @@ for _ in range(0, 10):
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
pass pass
raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url)) raise ValueError('Error, url did not respond after 10 attempts: {{0}}'.format(url))
'''.format(http_server_root, http_port)) '''.format(http_server_root.replace('\\', '\\\\'), http_port))
os.chmod(auth_script_path, 0o755) os.chmod(auth_script_path, 0o755)
cleanup_script_path = os.path.join(tempdir, 'cleanup.py') cleanup_script_path = os.path.join(tempdir, 'cleanup.py')
@@ -204,7 +200,7 @@ import os
import shutil import shutil
well_known = os.path.join('{0}', '.well-known') well_known = os.path.join('{0}', '.well-known')
shutil.rmtree(well_known) shutil.rmtree(well_known)
'''.format(http_server_root)) '''.format(http_server_root.replace('\\', '\\\\')))
os.chmod(cleanup_script_path, 0o755) os.chmod(cleanup_script_path, 0o755)
yield ('{0} {1}'.format(sys.executable, auth_script_path), yield ('{0} {1}'.format(sys.executable, auth_script_path),
@@ -287,4 +283,32 @@ def load_sample_data_path(workspace):
original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config') original = pkg_resources.resource_filename('certbot_integration_tests', 'assets/sample-config')
copied = os.path.join(workspace, 'sample-config') copied = os.path.join(workspace, 'sample-config')
shutil.copytree(original, copied, symlinks=True) shutil.copytree(original, copied, symlinks=True)
if os.name == 'nt':
# Fix the symlinks on Windows since GIT is not creating them upon checkout
for lineage in ['a.encryption-example.com', 'b.encryption-example.com']:
current_live = os.path.join(copied, 'live', lineage)
for name in os.listdir(current_live):
if name != 'README':
current_file = os.path.join(current_live, name)
with open(current_file) as file_h:
src = file_h.read()
os.unlink(current_file)
os.symlink(os.path.join(current_live, src), current_file)
return copied return copied
def echo(keyword, path=None):
"""
Generate a platform independent executable command
that echoes the given keyword into the given file.
:param keyword: the keyword to echo (must be a single keyword)
:param path: path to the file were keyword is echoed
:return: the executable command
"""
if not re.match(r'^\w+$', keyword):
raise ValueError('Error, keyword `{0}` is not a single keyword.'
.format(keyword))
return '{0} -c "from __future__ import print_function; print(\'{1}\')"{2}'.format(
os.path.basename(sys.executable), keyword, ' >> "{0}"'.format(path) if path else '')

View File

@@ -1,5 +1,4 @@
import json import json
import platform
import os import os
import stat import stat
@@ -13,9 +12,7 @@ ASSETS_PATH = pkg_resources.resource_filename('certbot_integration_tests', 'asse
def fetch(workspace): def fetch(workspace):
suffix = '{0}-{1}{2}'.format(platform.system().lower(), suffix = 'linux-amd64' if os.name != 'nt' else 'windows-amd64.exe'
platform.machine().lower().replace('x86_64', 'amd64'),
'.exe' if platform.system() == 'Windows' else '')
pebble_path = _fetch_asset('pebble', suffix) pebble_path = _fetch_asset('pebble', suffix)
challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix) challtestsrv_path = _fetch_asset('pebble-challtestsrv', suffix)

View File

@@ -1,5 +1,7 @@
from setuptools import setup import sys
from setuptools import find_packages
from distutils.version import StrictVersion
from setuptools import setup, find_packages, __version__ as setuptools_version
version = '0.32.0.dev0' version = '0.32.0.dev0'
@@ -17,6 +19,17 @@ install_requires = [
'six', 'six',
] ]
# Add pywin32 on Windows platforms to handle low-level system calls.
# This dependency needs to be added using environment markers to avoid its installation on Linux.
# However environment markers are supported only with setuptools >= 36.2.
# So this dependency is not added for old Linux distributions with old setuptools,
# in order to allow these systems to build certbot from sources.
if StrictVersion(setuptools_version) >= StrictVersion('36.2'):
install_requires.append("pywin32>=224 ; sys_platform == 'win32'")
elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Error, you are trying to build certbot wheels using an old version '
'of setuptools. Version 36.2+ of setuptools is required.')
setup( setup(
name='certbot-ci', name='certbot-ci',
version=version, version=version,

View File

@@ -59,7 +59,7 @@ install_requires = [
# So this dependency is not added for old Linux distributions with old setuptools, # So this dependency is not added for old Linux distributions with old setuptools,
# in order to allow these systems to build certbot from sources. # in order to allow these systems to build certbot from sources.
if StrictVersion(setuptools_version) >= StrictVersion('36.2'): if StrictVersion(setuptools_version) >= StrictVersion('36.2'):
install_requires.append("pywin32 ; sys_platform == 'win32'") install_requires.append("pywin32>=224 ; sys_platform == 'win32'")
elif 'bdist_wheel' in sys.argv[1:]: elif 'bdist_wheel' in sys.argv[1:]:
raise RuntimeError('Error, you are trying to build certbot wheels using an old version ' raise RuntimeError('Error, you are trying to build certbot wheels using an old version '
'of setuptools. Version 36.2+ of setuptools is required.') 'of setuptools. Version 36.2+ of setuptools is required.')

View File

@@ -61,6 +61,7 @@ pytest-sugar==0.9.2
pytest-rerunfailures==4.2 pytest-rerunfailures==4.2
python-dateutil==2.6.1 python-dateutil==2.6.1
python-digitalocean==1.11 python-digitalocean==1.11
pywin32==224
PyYAML==3.13 PyYAML==3.13
repoze.sphinx.autointerface==0.8 repoze.sphinx.autointerface==0.8
requests-file==1.4.2 requests-file==1.4.2

View File

@@ -232,6 +232,15 @@ commands =
coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74 coverage report --include 'certbot-nginx/*' --show-missing --fail-under=74
passenv = DOCKER_* passenv = DOCKER_*
[testenv:integration-certbot]
commands =
{[base]pip_install} acme . certbot-ci
pytest certbot-ci/certbot_integration_tests/certbot_tests \
--acme-server={env:ACME_SERVER:pebble} \
--cov=acme --cov=certbot --cov-report= \
--cov-config=certbot-ci/certbot_integration_tests/.coveragerc
coverage report --include 'certbot/*' --show-missing --fail-under=62
[testenv:integration-certbot-oldest] [testenv:integration-certbot-oldest]
commands = commands =
{[base]pip_install} . {[base]pip_install} .