Skip to content

Commit

Permalink
WebACMEClient
Browse files Browse the repository at this point in the history
  • Loading branch information
dvolodin7 committed Nov 17, 2023
1 parent 946996b commit e93f203
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 19 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ simplifies the protocol complexity with a straightforward and robust API.

Gufo ACME contains various clients which can be applied to your tasks:

* [ACMEClient]() - base client to implement any fulfillment functionality
* ACMEClient - base client to implement any fulfillment functionality
by creating subclasses.
* DAVACMEClient - http-01 fulfillment using WebDAV methods.
* WebACMEClient - http-01 static file fulfillment.

## Supported Certificate Authorities

Expand Down
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Gufo ACME contains various clients which can be applied to your tasks:

* [ACMEClient][gufo.acme.clients.base.ACMEClient] - base client to implement any fulfillment functionality
by creating subclasses.
* [DAVACMEClient][gufo.acme.clients.dav.DAVACMEClient] - http-01 fulfillment using WebDAV methods.
* [DAVACMEClient][gufo.acme.clients.dav.DAVACMEClient] - http-01 fulfillment using WebDAV methods.
* [WebACMEClient][gufo.acme.clients.web.WebACMEClient] - http-01 static file fulfillment.

## Supported Certificate Authorities

Expand Down
1 change: 1 addition & 0 deletions src/gufo/acme/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
* [base][gufo.acme.clients.base] - Base class
* [dav][gufo.acme.clients.dav] - http-01 WebDAV fulfillment.
* [web][gufo.acme.clients.web] - http-01 static file fulfillment.
"""
4 changes: 2 additions & 2 deletions src/gufo/acme/clients/dav.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# ---------------------------------------------------------------------
# Gufo ACME: ACMEClient implementation
# Gufo ACME: DAVACMEClient implementation
# ---------------------------------------------------------------------
# Copyright (C) 2023, Gufo Labs
# ---------------------------------------------------------------------
"""An DAVACMEClient implementation."""
"""A DAVACMEClient implementation."""

# Python modules
from typing import Any
Expand Down
95 changes: 95 additions & 0 deletions src/gufo/acme/clients/web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ---------------------------------------------------------------------
# Gufo ACME: WebACMEClient implementation
# ---------------------------------------------------------------------
# Copyright (C) 2023, Gufo Labs
# ---------------------------------------------------------------------
"""A WebACMEClient implementation."""

# Python modules
import os
from pathlib import Path
from typing import Any, Union

# Gufo ACME modules
from ..log import logger
from ..types import ACMEChallenge
from .base import ACMEClient


class WebACMEClient(ACMEClient):
"""
A webserver-backed ACME client.
Fulfills http-01 challenge by creating
and removing token files in predefined
directories.
Args:
path: Path mapped to /.well-known/acme-challenges
directory.
"""

def __init__(
self: "WebACMEClient",
directory_url: str,
*,
path: Union[str, Path],
**kwargs: Any,
) -> None:
super().__init__(directory_url, **kwargs)
self.path = Path(path)

def _get_path(self: "WebACMEClient", challenge: ACMEChallenge) -> Path:
"""
Get Path for challenge.
Args:
challenge: ACME challenge
Returns:
token path.
"""
return self.path / Path(challenge.token)

async def fulfill_http_01(
self: "WebACMEClient", domain: str, challenge: ACMEChallenge
) -> bool:
"""
Perform http-01 fullfilment.
Put token to <path>/<token> file.
Args:
domain: Domain name
challenge: ACMEChallenge instance, containing token.
Returns:
True - on succeess
Raises:
ACMEFulfillmentFailed: On error.
"""
path = self._get_path(challenge)
v = self.get_key_authorization(challenge)
logger.warning("Writing token to %s", path)
with open(path, "wb") as fp:
fp.write(v)
return True

async def clear_http_01(
self: "WebACMEClient", domain: str, challenge: ACMEChallenge
) -> None:
"""
Remove provisioned token.
Args:
domain: Domain name
challenge: ACMEChallenge instance, containing token.
Raises:
ACMEFulfillmentFailed: On error.
"""
path = self._get_path(challenge)
if os.path.exists(path):
logger.warning("Removing token from %s", path)
os.unlink(path)
16 changes: 1 addition & 15 deletions tests/clients/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,10 @@
from httpx import ConnectError, Response
from josepy.jwk import JWKRSA

from .utils import DIRECTORY, EMAIL, get_csr_pem, not_set, not_set_reason
from .utils import DIRECTORY, EMAIL, KEY, get_csr_pem, not_set, not_set_reason

ENV_CI_ACME_TEST_DOMAIN = "CI_ACME_TEST_DOMAIN"

KEY = JWKRSA.from_json(
{
"n": "gvvjoJPd1L4sq1bT0q2C94N3WV7W7lroA_MzF-SGMVYFasI2lvqw3kAkFRxG366JfHr3B1R-xlCzEPHNixbL6b0ccvPFZZsungnx5m_uGL2FMiisu186dMnfsk6YssveboxiQXEhGMxI9T6GjE6l6ec1PGY5uB70vP2wkGPxkvRLD2tGae_-7kCgRzvF2xOaGZjT-jxHcYpWutNN-qQzDoHnhLu0LIwWlXBazAs6zbkPvPW9PNZAUencWxxQ5hJtLkVSvgSYwzI1cxlrC8lCjg6rIR9LA8s5PLzee_nEotljlU0ljXz3eyD9W4fl4rC46v8-ufk5Ez9utQQ2sVjIMQ",
"e": "AQAB",
"d": "AUosSQ5trbCn8VG1_R4D4y6oZiERwBf1bwUF9rUziFC01dLW3WSXaV_TryDHtqACBu-Rx0Di7O5aXgdIfycsv7bizOO3OM927XvS9cI6Q6R5l1do0IFA-smKVifRl3icDoX7uXHdCeDIkuAgTGlBl1iVSMyHotdMsP_1PS27wSb6q0miPLJzZFPcMz2WcRaPaVFjsg_l9J8_Sy6d0HWx7_2nrxvOESlUNwf7sRn2WY2ZQaGwBzS6L17aHeKkQiAgUMAJK6gF3SGLK8kiHJac-p5bxFMu38fFY3FcVCW-QJMhRZMHaV36XZliIfP6DLFBzHqj99iZqgR8LvQ1SeMLgQ",
"p": "t8GsAuPj1WujpvId3eJwhPUsbxJuIcd0Zi6hLTlQvydOI4jUtfO9JzHvEFG9GSZtedaJ5Vga0OFQlpCyhNHLQe6JjJnriexazHK-dLlUr6cVmaCNSj9spa7azZF8ak8EtISAr-7zdzLGy2KiC-DsJwp36RlSOyD6APkDCthSoIE",
"q": "tnrgWqyT2alj9gtjgWb-xY07qxFGBSBZEO6dY5hJsydbkLRGumX5mvhqKy3BBbROz1smVNlMxcJa8fEWwQn8EORmr9-86i6TQUyZyCRMbh1pO63D4mJGjuhCHDgKyzzxYczeBPI2MxFVnZHPUAjWJwUGZp3sMEGzwej5g0iwT7E",
"dp": "jU8UVkylwlO6UAHU0fL2kGhyOSA1LSjS7FljfQGchMNXJaBt41aC2Ydezm_tOVAB1DYVaRbt2D_M11yCy_0Bj7w-bq9XIINv99UtfVmgNEwLIk8DGFvZ0ze572e4A5Csj51t0N2ywLF9ip5Y-0WGlSdJuynLwMjFOMZFfquILwE",
"dq": "Ck1dpUDhCATcM-PotkGOWLDkkX_kKB3vaVlPYXQTlR2_uaez5oojUXB87fsjTqMjX-mRfHDYOMIESGyIEFXz-TAr6_oBvGbswV8Fv5rtBbp7Wncw-_L4cNEECnvPgDHsnszmK_lQvglYgBDfV3FoRcOu3NRFpWPQNj5k99h-u8E",
"qi": "Z3Ipo4AnRJfszwEEb2Y-mZgkgrZJguoixPleH-QSmy9vJ17-9URMv62MWKv19X5HdluxZJYmKGSLbbuMWD9-MVntVFSb77YKNrE2kCGM8a--aWtv706dHUSZemRazib55HtcGn2H6D3laUigFSmPNCdfq8CjsWLeW8RVOyY5tgM",
"kty": "RSA",
}
)


def test_get_directory() -> None:
async def inner():
Expand Down
38 changes: 38 additions & 0 deletions tests/clients/test_web.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Python modules
import asyncio
import os
import tempfile
from pathlib import Path

# Gufo ACMEE modules
from gufo.acme.clients.web import WebACMEClient
from gufo.acme.types import ACMEChallenge

from .utils import DIRECTORY, KEY

DOMAIN = "example.com"
AUTH_KEY = (
b"qPsDhTbvumL6q2DryepIyzJS1nMkghS92IqI4mug-7c.IoRaMHpsTJikEnDg70XhS0d7Xx1ZU3Tz"
b"MzhTKFITfwY"
)


def test_web() -> None:
async def inner():
chall = ACMEChallenge(
type="http-01",
url="xxx",
token="qPsDhTbvumL6q2DryepIyzJS1nMkghS92IqI4mug-7c",
)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp)
client = WebACMEClient(DIRECTORY, key=KEY, path=path)
r = await client.fulfill_http_01(DOMAIN, chall)
assert r is True
with open(path / chall.token, "rb") as fp:
data = fp.read()
assert data == AUTH_KEY
await client.clear_http_01(DOMAIN, chall)
assert not os.path.exists(path / Path(chall.token))

asyncio.run(inner())
14 changes: 14 additions & 0 deletions tests/clients/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,23 @@

# Gufo ACME modules
from gufo.acme.clients.base import ACMEClient
from josepy.jwk import JWKRSA

EMAIL = "acme-000000000@gufolabs.com"
DIRECTORY = "https://acme-staging-v02.api.letsencrypt.org/directory"
KEY = JWKRSA.from_json(
{
"n": "gvvjoJPd1L4sq1bT0q2C94N3WV7W7lroA_MzF-SGMVYFasI2lvqw3kAkFRxG366JfHr3B1R-xlCzEPHNixbL6b0ccvPFZZsungnx5m_uGL2FMiisu186dMnfsk6YssveboxiQXEhGMxI9T6GjE6l6ec1PGY5uB70vP2wkGPxkvRLD2tGae_-7kCgRzvF2xOaGZjT-jxHcYpWutNN-qQzDoHnhLu0LIwWlXBazAs6zbkPvPW9PNZAUencWxxQ5hJtLkVSvgSYwzI1cxlrC8lCjg6rIR9LA8s5PLzee_nEotljlU0ljXz3eyD9W4fl4rC46v8-ufk5Ez9utQQ2sVjIMQ",
"e": "AQAB",
"d": "AUosSQ5trbCn8VG1_R4D4y6oZiERwBf1bwUF9rUziFC01dLW3WSXaV_TryDHtqACBu-Rx0Di7O5aXgdIfycsv7bizOO3OM927XvS9cI6Q6R5l1do0IFA-smKVifRl3icDoX7uXHdCeDIkuAgTGlBl1iVSMyHotdMsP_1PS27wSb6q0miPLJzZFPcMz2WcRaPaVFjsg_l9J8_Sy6d0HWx7_2nrxvOESlUNwf7sRn2WY2ZQaGwBzS6L17aHeKkQiAgUMAJK6gF3SGLK8kiHJac-p5bxFMu38fFY3FcVCW-QJMhRZMHaV36XZliIfP6DLFBzHqj99iZqgR8LvQ1SeMLgQ",
"p": "t8GsAuPj1WujpvId3eJwhPUsbxJuIcd0Zi6hLTlQvydOI4jUtfO9JzHvEFG9GSZtedaJ5Vga0OFQlpCyhNHLQe6JjJnriexazHK-dLlUr6cVmaCNSj9spa7azZF8ak8EtISAr-7zdzLGy2KiC-DsJwp36RlSOyD6APkDCthSoIE",
"q": "tnrgWqyT2alj9gtjgWb-xY07qxFGBSBZEO6dY5hJsydbkLRGumX5mvhqKy3BBbROz1smVNlMxcJa8fEWwQn8EORmr9-86i6TQUyZyCRMbh1pO63D4mJGjuhCHDgKyzzxYczeBPI2MxFVnZHPUAjWJwUGZp3sMEGzwej5g0iwT7E",
"dp": "jU8UVkylwlO6UAHU0fL2kGhyOSA1LSjS7FljfQGchMNXJaBt41aC2Ydezm_tOVAB1DYVaRbt2D_M11yCy_0Bj7w-bq9XIINv99UtfVmgNEwLIk8DGFvZ0ze572e4A5Csj51t0N2ywLF9ip5Y-0WGlSdJuynLwMjFOMZFfquILwE",
"dq": "Ck1dpUDhCATcM-PotkGOWLDkkX_kKB3vaVlPYXQTlR2_uaez5oojUXB87fsjTqMjX-mRfHDYOMIESGyIEFXz-TAr6_oBvGbswV8Fv5rtBbp7Wncw-_L4cNEECnvPgDHsnszmK_lQvglYgBDfV3FoRcOu3NRFpWPQNj5k99h-u8E",
"qi": "Z3Ipo4AnRJfszwEEb2Y-mZgkgrZJguoixPleH-QSmy9vJ17-9URMv62MWKv19X5HdluxZJYmKGSLbbuMWD9-MVntVFSb77YKNrE2kCGM8a--aWtv706dHUSZemRazib55HtcGn2H6D3laUigFSmPNCdfq8CjsWLeW8RVOyY5tgM",
"kty": "RSA",
}
)


def not_set(vars: Iterable[str]) -> bool:
Expand Down

0 comments on commit e93f203

Please sign in to comment.