Skip to content

Commit

Permalink
Merge pull request #65 from JayH5/http-01-challenge
Browse files Browse the repository at this point in the history
First (very incomplete) attempt at http-01 challenge type
  • Loading branch information
mithrandi authored Oct 4, 2016
2 parents a12af20 + 249de66 commit e29c1aa
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 4 deletions.
3 changes: 2 additions & 1 deletion src/txacme/challenges/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._http import HTTP01Responder
from ._tls import TLSSNI01Responder

__all__ = ['TLSSNI01Responder']
__all__ = ['HTTP01Responder', 'TLSSNI01Responder']
39 changes: 39 additions & 0 deletions src/txacme/challenges/_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
``http-01`` challenge implementation.
"""
from twisted.web.resource import Resource
from twisted.web.static import Data

from zope.interface import implementer

from txacme.interfaces import IResponder


@implementer(IResponder)
class HTTP01Responder(object):
"""
An ``http-01`` challenge responder for txsni.
"""
challenge_type = u'http-01'

def __init__(self):
self.resource = Resource()

def start_responding(self, server_name, challenge, response):
"""
Add the child resource.
"""
self.resource.putChild(
challenge.encode('token').encode('utf-8'),
Data(response.key_authorization.encode('utf-8'), 'text/plain'))

def stop_responding(self, server_name, challenge, response):
"""
Remove the child resource.
"""
encoded_token = challenge.encode('token').encode('utf-8')
if self.resource.getStaticEntity(encoded_token) is not None:
self.resource.delEntity(encoded_token)


__all__ = ['HTTP01Responder']
1 change: 1 addition & 0 deletions src/txacme/newsfragments/65.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
``txacme.challenges.HTTP01Responder``, an http-01 challenge responder that can be embedded into an existing twisted.web application.
67 changes: 64 additions & 3 deletions src/txacme/test/test_challenges.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
"""
Tests for `txacme.challenges`.
"""
from operator import methodcaller

from acme import challenges
from acme.jose import b64encode
from hypothesis import strategies as s
from hypothesis import example, given
from testtools import TestCase
from testtools.matchers import Contains, Equals, Is, MatchesPredicate, Not
from testtools.matchers import (
AfterPreprocessing, Contains, Equals, Is, MatchesAll, MatchesPredicate,
MatchesStructure, Not)
from testtools.twistedsupport import succeeded
from treq.testing import StubTreq
from twisted.python.url import URL
from twisted.web.resource import Resource
from zope.interface.verify import verifyObject

from txacme.challenges import TLSSNI01Responder
from txacme.challenges import HTTP01Responder, TLSSNI01Responder
from txacme.challenges._tls import _MergingMappingProxy
from txacme.interfaces import IResponder
from txacme.test.test_client import RSA_KEY_512, RSA_KEY_512_RAW
Expand Down Expand Up @@ -172,4 +180,57 @@ def test_contains(self, underlay, overlay, key):
Equals(proxy.get(key) is not None))


__all__ = ['TLSResponderTests']
class HTTPResponderTests(_CommonResponderTests, TestCase):
"""
`.HTTP01Responder` is a responder for http-01 challenges.
"""
_challenge_factory = challenges.HTTP01
_responder_factory = HTTP01Responder
_challenge_type = u'http-01'

@example(token=b'BWYcfxzmOha7-7LoxziqPZIUr99BCz3BfbN9kzSFnrU')
@given(token=s.binary(min_size=32, max_size=32).map(b64encode))
def test_start_responding(self, token):
"""
Calling ``start_responding`` makes an appropriate resource available.
"""
challenge = challenges.HTTP01(token=token)
response = challenge.response(RSA_KEY_512)

responder = HTTP01Responder()

challenge_resource = Resource()
challenge_resource.putChild(b'acme-challenge', responder.resource)
root = Resource()
root.putChild(b'.well-known', challenge_resource)
client = StubTreq(root)

encoded_token = challenge.encode('token')
challenge_url = URL(host=u'example.com', path=[
u'.well-known', u'acme-challenge', encoded_token]).asText()

self.assertThat(client.get(challenge_url),
succeeded(MatchesStructure(code=Equals(404))))

responder.start_responding(u'example.com', challenge, response)
self.assertThat(client.get(challenge_url), succeeded(MatchesAll(
MatchesStructure(
code=Equals(200),
headers=AfterPreprocessing(
methodcaller('getRawHeaders', b'content-type'),
Equals([b'text/plain']))),
AfterPreprocessing(methodcaller('content'), succeeded(
Equals(response.key_authorization.encode('utf-8'))))
)))

# Starting twice before stopping doesn't break things
responder.start_responding(u'example.com', challenge, response)
self.assertThat(client.get(challenge_url),
succeeded(MatchesStructure(code=Equals(200))))

responder.stop_responding(u'example.com', challenge, response)
self.assertThat(client.get(challenge_url),
succeeded(MatchesStructure(code=Equals(404))))


__all__ = ['HTTPResponderTests', 'TLSResponderTests']

0 comments on commit e29c1aa

Please sign in to comment.