|
1 | 1 | import logging |
2 | 2 | import re |
| 3 | +import socket |
3 | 4 | from base64 import b64decode, b64encode |
4 | 5 |
|
5 | 6 | import gssapi |
@@ -136,6 +137,47 @@ def __init__( |
136 | 137 | if channel_bindings not in (None, "tls-server-end-point"): |
137 | 138 | raise ValueError("channel_bindings must be None or 'tls-server-end-point'") |
138 | 139 | self.channel_bindings = channel_bindings |
| 140 | + self._dns_canonicalize_hostname = False |
| 141 | + self._use_reverse_dns = False |
| 142 | + |
| 143 | + def dns_canonicalize_hostname(self, value=None): |
| 144 | + """ |
| 145 | + Enables canonical hostname resolution via CNAME records. |
| 146 | +
|
| 147 | + >>> import requests |
| 148 | + >>> from requests_gssapi import HTTPSPNEGOAuth |
| 149 | + >>> gssapi_auth = HTTPSPNEGOAuth() |
| 150 | + >>> gssapi_auth.dns_canonicalize_hostname(True) |
| 151 | + >>> gssapi_auth.use_reverse_dns(True) |
| 152 | + >>> r = requests.get("http://example.org", auth=gssapi_auth) |
| 153 | +
|
| 154 | + .. warning::: |
| 155 | + Using an insecure DNS queries for principal name |
| 156 | + canonicalization can result in risc of a man-in-the-middle |
| 157 | + attack. Strictly speaking such queries are in violation of |
| 158 | + RFC 4120. Alas misconfigured realms exist and client libraries |
| 159 | + like MIT Kerberos provide means to canonicalize principal |
| 160 | + names via DNS queries. Be very careful when using thi option. |
| 161 | +
|
| 162 | + .. seealso::: |
| 163 | + `RFC 4120 <https://datatracker.ietf.org/doc/html/rfc4120>` |
| 164 | + `RFC 6808 <https://datatracker.ietf.org/doc/html/rfc6806>` |
| 165 | + """ |
| 166 | + if isinstance(value, bool): |
| 167 | + self._dns_canonicalize_hostname = value |
| 168 | + return self._dns_canonicalize_hostname |
| 169 | + |
| 170 | + def use_reverse_dns(self, value=None): |
| 171 | + """ |
| 172 | + Use rev-DNS query to resolve canonical host name when DNS |
| 173 | + canonicalization is enabled. |
| 174 | +
|
| 175 | + .. seealso:: |
| 176 | + See `dns_canonicalize_hostname` for further details and warnings. |
| 177 | + """ |
| 178 | + if isinstance(value, bool): |
| 179 | + self._use_reverse_dns = value |
| 180 | + return self._use_reverse_dns |
139 | 181 |
|
140 | 182 | def generate_request_header(self, response, host, is_preemptive=False): |
141 | 183 | """ |
@@ -179,12 +221,26 @@ def generate_request_header(self, response, host, is_preemptive=False): |
179 | 221 | "channel_bindings were requested, but a socket could not be retrieved from the response" |
180 | 222 | ) |
181 | 223 |
|
| 224 | + canonhost = host |
| 225 | + if self._dns_canonicalize_hostname and type(self.target_name) != gssapi.Name: |
| 226 | + try: |
| 227 | + ai = socket.getaddrinfo(host, 0, flags=socket.AI_CANONNAME) |
| 228 | + canonhost = ai[0][3] |
| 229 | + |
| 230 | + if self._use_reverse_dns: |
| 231 | + ni = socket.getnameinfo(ai[0][4], socket.NI_NAMEREQD) |
| 232 | + canonhost = ni[0] |
| 233 | + |
| 234 | + except socket.gaierror as e: |
| 235 | + if e.errno == socket.EAI_MEMORY: |
| 236 | + raise e |
| 237 | + |
182 | 238 | try: |
183 | 239 | gss_stage = "initiating context" |
184 | 240 | name = self.target_name |
185 | 241 | if type(name) != gssapi.Name: |
186 | 242 | if "@" not in name: |
187 | | - name = "%s@%s" % (name, host) |
| 243 | + name = "%s@%s" % (name, canonhost) |
188 | 244 |
|
189 | 245 | name = gssapi.Name(name, gssapi.NameType.hostbased_service) |
190 | 246 | self.context[host] = gssapi.SecurityContext( |
|
0 commit comments