]> code.delx.au - gnu-emacs-elpa/blob - packages/url-http-ntlm/url-http-ntlm.el
Merge commit 'ef509502cdd228c8ce0a562bbf411e5f98beaaf1'
[gnu-emacs-elpa] / packages / url-http-ntlm / url-http-ntlm.el
1 ;;; url-http-ntlm.el --- NTLM authentication for the url library
2
3 ;; Copyright (C) 2008, 2016 Free Software Foundation, Inc.
4
5 ;; Author: Tom Schutzer-Weissmann <tom.weissmann@gmail.com>
6 ;; Maintainer: Thomas Fitzsimmons <fitzsim@fitzsim.org>
7 ;; Version: 2.0.2
8 ;; Keywords: comm, data, processes, hypermedia
9 ;; Homepage: https://code.google.com/p/url-http-ntlm/
10 ;; Package-Requires: ((cl-lib "0.5") (ntlm "2.0.0"))
11
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
16
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
21
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
24
25 ;;; Commentary:
26 ;;
27 ;; This package provides a NTLM handler for the URL package.
28 ;;
29 ;; Installation:
30 ;;
31 ;; M-x package-install RET url-http-ntlm RET
32 ;;
33 ;; Acknowledgements:
34 ;;
35 ;; Taro Kawagishi <tarok@transpulse.org> wrote ntlm.el and md4.el,
36 ;; which are parts of FLIM (Faithful Library about Internet Message).
37 ;;
38 ;; http://stuff.mit.edu/afs/sipb/contrib/emacs/packages/flim-1.14.7/ntlm.el
39 ;; http://stuff.mit.edu/afs/sipb/contrib/emacs/packages/flim-1.14.7/md4.el
40
41 ;;; Code:
42 (require 'url-auth)
43 (require 'url-http)
44 (require 'url-util)
45 (require 'mail-parse)
46 (require 'cl-lib)
47 (require 'ntlm)
48
49 ;; Remove authorization after redirect.
50 (when (and (boundp 'emacs-major-version)
51 (< emacs-major-version 25))
52 (defvar url-http-ntlm--parsing-headers nil)
53 (defadvice url-http-parse-headers (around clear-authorization activate)
54 (let ((url-http-ntlm--parsing-headers t))
55 ad-do-it))
56 (defadvice url-http-handle-authentication (around clear-authorization
57 activate)
58 (let ((url-http-ntlm--parsing-headers nil))
59 ad-do-it))
60 (defadvice url-retrieve-internal (before clear-authorization activate)
61 (when (and url-http-ntlm--parsing-headers
62 (eq url-request-extra-headers url-http-extra-headers))
63 ;; This retrieval is presumably in response to a redirect.
64 ;; Do not automatically include an authorization header in the
65 ;; redirect. If needed it will be regenerated by the relevant
66 ;; auth scheme when the new request happens.
67 (setq url-http-extra-headers
68 (cl-remove "Authorization"
69 url-http-extra-headers :key #'car :test #'equal))
70 (setq url-request-extra-headers url-http-extra-headers))))
71
72 \f
73 ;;; Private variables.
74 (defvar url-http-ntlm--auth-storage nil
75 "Authentication storage.
76 An alist that maps a server name to a pair of \(<username> <ntlm
77 hashes>\).
78
79 The hashes are built using `ntlm-get-password-hashes'.")
80
81 (defvar url-http-ntlm--last-args nil
82 "The last `url-http-ntlm--get-stage' arguments and result.
83 This is used to detect multiple calls.")
84 (make-variable-buffer-local 'url-http-ntlm--last-args)
85
86 (defvar url-http-ntlm--loop-timer-counter nil
87 "A hash table used to detect NTLM negotiation errors.
88 Keys are urls, entries are (START-TIME . COUNTER).")
89
90 (defvar url-http-ntlm--default-users nil
91 "An alist that stores one default username per server.")
92
93 \f
94 ;;; Private functions.
95 (defun url-http-ntlm--detect-loop (url)
96 "Detect potential infinite loop when NTLM fails on URL."
97 (when (not url-http-ntlm--loop-timer-counter)
98 (setq url-http-ntlm--loop-timer-counter (make-hash-table :test 'equal)))
99 (let* ((url-string (url-recreate-url url))
100 (last-entry (gethash url-string url-http-ntlm--loop-timer-counter))
101 (start-time (car last-entry))
102 (counter (cdr last-entry)))
103 (if last-entry
104 (progn
105 (if (< (- (float-time) start-time) 10.0)
106 (if (< counter 20)
107 ;; Still within time window, so increment count.
108 (puthash url-string (cons start-time (1+ counter))
109 url-http-ntlm--loop-timer-counter)
110 ;; Error detected, so remove entry and clear.
111 (url-http-ntlm--authorization url-string :clear)
112 (remhash url-string url-http-ntlm--loop-timer-counter)
113 (error
114 (format (concat "Access rate to %s is too high,"
115 " indicating an NTLM failure;"
116 " to debug, re-run with url-debug set to 1")
117 url-string)))
118 ;; Timeout expired, so reset counter.
119 (puthash url-string (cons (float-time) 0)
120 url-http-ntlm--loop-timer-counter)))
121 ;; New access, so initialize counter to 0.
122 (puthash url-string (cons (float-time) 0)
123 url-http-ntlm--loop-timer-counter))))
124
125 (defun url-http-ntlm--ensure-user (url)
126 "Return URL with its user slot set.
127 If URL's user slot is nil, set it to the last user that made a
128 request to the host in URL's server slot."
129 (let ((new-url url))
130 (if (url-user new-url)
131 new-url
132 (setf (url-user new-url)
133 (cdr (assoc (url-host new-url) url-http-ntlm--default-users)))
134 new-url)))
135
136 (defun url-http-ntlm--ensure-keepalive ()
137 "Report an error if `url-http-attempt-keepalives' is not set."
138 (cl-assert url-http-attempt-keepalives
139 nil
140 (concat "NTLM authentication won't work unless"
141 " `url-http-attempt-keepalives' is set!")))
142
143 (defun url-http-ntlm--clean-headers ()
144 "Remove Authorization element from `url-http-extra-headers' alist."
145 (cl-declare (special url-http-extra-headers))
146 (setq url-http-extra-headers
147 (url-http-ntlm--rmssoc "Authorization" url-http-extra-headers)))
148
149 (defun url-http-ntlm--get-stage (args)
150 "Determine what stage of the NTLM handshake we are at.
151 PROMPT and ARGS come from `url-ntlm-auth''s caller,
152 `url-get-authentication'. Their meaning depends on the current
153 implementation - this function is well and truly coupled.
154
155 url-get-authentication' calls `url-ntlm-auth' once when checking
156 what authentication schemes are supported (PROMPT and ARGS are
157 nil), and then twice for every stage of the handshake: the first
158 time PROMPT is nil, the second, t; ARGS contains the server
159 response's \"WWW-Authenticate\" header, munged by
160 `url-parse-args'."
161 (cl-declare (special url-http-extra-headers))
162 (let* ((response-rxp "^NTLM TlRMTVNTUAADAAA")
163 (challenge-rxp "^TLRMTVNTUAACAAA")
164 (auth-header (assoc "Authorization" url-http-extra-headers))
165 (case-fold-search t)
166 stage)
167 (url-debug 'url-http-ntlm "Buffer: %s" (current-buffer))
168 (url-debug 'url-http-ntlm "Arguments: %s" args)
169 (url-debug 'url-http-ntlm "Previous arguments: %s" url-http-ntlm--last-args)
170 (if (eq args (car url-http-ntlm--last-args))
171 ;; multiple calls, return the same argument we returned last time
172 (progn
173 (url-debug 'url-http-ntlm "Returning previous result: %s"
174 (cdr url-http-ntlm--last-args))
175 (cdr url-http-ntlm--last-args))
176 (let ((stage
177 (cond ((and auth-header (string-match response-rxp
178 (cdr auth-header)))
179 :error)
180 ((and (= (length args) 2)
181 (cl-destructuring-bind (challenge ntlm) args
182 (and (string-equal "ntlm" (car ntlm))
183 (string-match challenge-rxp
184 (car challenge)))))
185 :response)
186 (t
187 :request))))
188 (url-http-ntlm--clean-headers)
189 (setq url-http-ntlm--last-args (cons args stage))
190 stage))))
191
192 (defun url-http-ntlm--authorization (url &optional clear realm)
193 "Get or clear NTLM authentication details for URL.
194 If CLEAR is non-nil, clear any saved credentials for server.
195 Otherwise, return the credentials, prompting the user if
196 necessary. REALM appears in the prompt.
197
198 If URL contains a username and a password, they are used and
199 stored credentials are not affected."
200 (let* ((href (if (stringp url)
201 (url-generic-parse-url url)
202 url))
203 (type (url-type href))
204 (user (url-user href))
205 (server (url-host href))
206 (port (url-portspec href))
207 (pass (url-password href))
208 (stored (assoc (list type user server port)
209 url-http-ntlm--auth-storage))
210 (both (and user pass)))
211 (if clear
212 ;; clear
213 (unless both
214 (setq url-http-ntlm--default-users
215 (url-http-ntlm--rmssoc server url-http-ntlm--default-users))
216 (setq url-http-ntlm--auth-storage
217 (url-http-ntlm--rmssoc '(type user* server port)
218 url-http-ntlm--auth-storage))
219 nil)
220 ;; get
221 (if (or both
222 (and stored user (not (equal user (cl-second (car stored)))))
223 (not stored))
224 (let* ((user* (or user
225 (url-do-auth-source-search server type :user)
226 (read-string (url-auth-user-prompt url realm)
227 (or user (user-real-login-name)))))
228 (pass* (if both
229 pass
230 (or (url-do-auth-source-search server type :secret)
231 (read-passwd (format "Password [for %s]: "
232 (url-recreate-url url))))))
233 (key (list type user* server port))
234 (entry `(,key . (,(ntlm-get-password-hashes pass*)))))
235 (unless both
236 (setq url-http-ntlm--default-users
237 (cons
238 `(,server . ,user*)
239 (url-http-ntlm--rmssoc server
240 url-http-ntlm--default-users)))
241 (setq url-http-ntlm--auth-storage
242 (cons entry
243 (url-http-ntlm--rmssoc
244 key
245 url-http-ntlm--auth-storage))))
246 entry)
247 stored))))
248
249 (defun url-http-ntlm--get-challenge ()
250 "Return the NTLM Type-2 message in the WWW-Authenticate header.
251 Return nil if the NTLM Type-2 message is not present."
252 (save-restriction
253 (mail-narrow-to-head)
254 (let ((www-authenticate (mail-fetch-field "www-authenticate")))
255 (when (string-match "NTLM\\s-+\\(\\S-+\\)"
256 www-authenticate)
257 (base64-decode-string (match-string 1 www-authenticate))))))
258
259 (defun url-http-ntlm--rmssoc (key alist)
260 "Remove all elements whose `car' match KEY from ALIST."
261 (cl-remove key alist :key 'car :test 'equal))
262
263 (defun url-http-ntlm--string (data)
264 "Return DATA encoded as an NTLM string."
265 (concat "NTLM " (base64-encode-string data :nobreak)))
266
267 \f
268 ;;; Public function called by `url-get-authentication'.
269 ;;;###autoload
270 (defun url-ntlm-auth (url &optional prompt overwrite realm args)
271 "Return an NTLM HTTP authorization header.
272 Get the contents of the Authorization header for a HTTP response
273 using NTLM authentication, to access URL. Because NTLM is a
274 two-step process, this function expects to be called twice, first
275 to generate the NTLM type 1 message (request), then to respond to
276 the server's type 2 message (challenge) with a suitable response.
277
278 PROMPT, OVERWRITE, and REALM are ignored.
279
280 ARGS is expected to contain the WWW-Authentication header from
281 the server's last response. These are used by
282 `url-http-get-stage' to determine what stage we are at."
283 (url-http-ntlm--ensure-keepalive)
284 (let* ((user-url (url-http-ntlm--ensure-user url))
285 (stage (url-http-ntlm--get-stage args)))
286 (url-debug 'url-http-ntlm "Stage: %s" stage)
287 (cl-case stage
288 ;; NTLM Type 1 message: the request
289 (:request
290 (url-http-ntlm--detect-loop user-url)
291 (cl-destructuring-bind (&optional key hash)
292 (url-http-ntlm--authorization user-url nil realm)
293 (when (cl-third key)
294 (url-http-ntlm--string
295 (ntlm-build-auth-request (cl-second key) (cl-third key))))))
296 ;; NTLM Type 3 message: the response
297 (:response
298 (url-http-ntlm--detect-loop user-url)
299 (let ((challenge (url-http-ntlm--get-challenge)))
300 (cl-destructuring-bind (key hash)
301 (url-http-ntlm--authorization user-url nil realm)
302 (url-http-ntlm--string
303 (ntlm-build-auth-response challenge
304 (cl-second key)
305 hash)))))
306 (:error
307 (url-http-ntlm--authorization user-url :clear)))))
308
309 \f
310 ;;; Register `url-ntlm-auth' HTTP authentication method.
311 ;;;###autoload
312 (url-register-auth-scheme "ntlm" nil 8)
313
314 (provide 'url-http-ntlm)
315
316 ;;; url-http-ntlm.el ends here