1import logging
2
3from .base import default_ts_skew_in_seconds, HawkAuthority, Resource
4from .util import (calculate_mac,
5                   parse_authorization_header,
6                   validate_credentials)
7
8__all__ = ['Sender']
9log = logging.getLogger(__name__)
10
11
12class Sender(HawkAuthority):
13    """
14    A Hawk authority that will emit requests and verify responses.
15
16    :param credentials: Dict of credentials with keys ``id``, ``key``,
17                        and ``algorithm``. See :ref:`usage` for an example.
18    :type credentials: dict
19
20    :param url: Absolute URL of the request.
21    :type url: str
22
23    :param method: Method of the request. E.G. POST, GET
24    :type method: str
25
26    :param content=None: Byte string of request body.
27    :type content=None: str
28
29    :param content_type=None: content-type header value for request.
30    :type content_type=None: str
31
32    :param always_hash_content=True:
33        When True, ``content`` and ``content_type`` cannot be None.
34        Read :ref:`skipping-content-checks` to learn more.
35    :type always_hash_content=True: bool
36
37    :param nonce=None:
38        A string that when coupled with the timestamp will
39        uniquely identify this request to prevent replays.
40        If None, a nonce will be generated for you.
41    :type nonce=None: str
42
43    :param ext=None:
44        An external `Hawk`_ string. If not None, this value will be signed
45        so that the receiver can trust it.
46    :type ext=None: str
47
48    :param app=None:
49        A `Hawk`_ application string. If not None, this value will be signed
50        so that the receiver can trust it.
51    :type app=None: str
52
53    :param dlg=None:
54        A `Hawk`_ delegation string. If not None, this value will be signed
55        so that the receiver can trust it.
56    :type dlg=None: str
57
58    :param seen_nonce=None:
59        A callable that returns True if a nonce has been seen.
60        See :ref:`nonce` for details.
61    :type seen_nonce=None: callable
62
63    .. _`Hawk`: https://github.com/hueniverse/hawk
64    """
65    #: Value suitable for an ``Authorization`` header.
66    request_header = None
67
68    def __init__(self, credentials,
69                 url,
70                 method,
71                 content=None,
72                 content_type=None,
73                 always_hash_content=True,
74                 nonce=None,
75                 ext=None,
76                 app=None,
77                 dlg=None,
78                 seen_nonce=None,
79                 # For easier testing:
80                 _timestamp=None):
81
82        self.reconfigure(credentials)
83        self.request_header = None
84        self.seen_nonce = seen_nonce
85
86        log.debug('generating request header')
87        self.req_resource = Resource(url=url,
88                                     credentials=self.credentials,
89                                     ext=ext,
90                                     app=app,
91                                     dlg=dlg,
92                                     nonce=nonce,
93                                     method=method,
94                                     content=content,
95                                     always_hash_content=always_hash_content,
96                                     timestamp=_timestamp,
97                                     content_type=content_type)
98
99        mac = calculate_mac('header', self.req_resource,
100                            self.req_resource.gen_content_hash())
101        self.request_header = self._make_header(self.req_resource, mac)
102
103    def accept_response(self,
104                        response_header,
105                        content=None,
106                        content_type=None,
107                        accept_untrusted_content=False,
108                        localtime_offset_in_seconds=0,
109                        timestamp_skew_in_seconds=default_ts_skew_in_seconds,
110                        **auth_kw):
111        """
112        Accept a response to this request.
113
114        :param response_header:
115            A `Hawk`_ ``Server-Authorization`` header
116            such as one created by :class:`mohawk.Receiver`.
117        :type response_header: str
118
119        :param content=None: Byte string of the response body received.
120        :type content=None: str
121
122        :param content_type=None:
123            Content-Type header value of the response received.
124        :type content_type=None: str
125
126        :param accept_untrusted_content=False:
127            When True, allow responses that do not hash their content or
128            allow None type ``content`` and ``content_type``
129            arguments. Read :ref:`skipping-content-checks`
130            to learn more.
131        :type accept_untrusted_content=False: bool
132
133        :param localtime_offset_in_seconds=0:
134            Seconds to add to local time in case it's out of sync.
135        :type localtime_offset_in_seconds=0: float
136
137        :param timestamp_skew_in_seconds=60:
138            Max seconds until a message expires. Upon expiry,
139            :class:`mohawk.exc.TokenExpired` is raised.
140        :type timestamp_skew_in_seconds=60: float
141
142        .. _`Hawk`: https://github.com/hueniverse/hawk
143        """
144        log.debug('accepting response {header}'
145                  .format(header=response_header))
146
147        parsed_header = parse_authorization_header(response_header)
148
149        resource = Resource(ext=parsed_header.get('ext', None),
150                            content=content,
151                            content_type=content_type,
152                            # The following response attributes are
153                            # in reference to the original request,
154                            # not to the reponse header:
155                            timestamp=self.req_resource.timestamp,
156                            nonce=self.req_resource.nonce,
157                            url=self.req_resource.url,
158                            method=self.req_resource.method,
159                            app=self.req_resource.app,
160                            dlg=self.req_resource.dlg,
161                            credentials=self.credentials,
162                            seen_nonce=self.seen_nonce)
163
164        self._authorize(
165            'response', parsed_header, resource,
166            # Per Node lib, a responder macs the *sender's* timestamp.
167            # It does not create its own timestamp.
168            # I suppose a slow response could time out here. Maybe only check
169            # mac failures, not timeouts?
170            their_timestamp=resource.timestamp,
171            timestamp_skew_in_seconds=timestamp_skew_in_seconds,
172            localtime_offset_in_seconds=localtime_offset_in_seconds,
173            accept_untrusted_content=accept_untrusted_content,
174            **auth_kw)
175
176    def reconfigure(self, credentials):
177        validate_credentials(credentials)
178        self.credentials = credentials
179