1 /* jcifs smb client library in Java
2  * Copyright (C) 2002  "Michael B. Allen" <jcifs at samba dot org>
3  *                   "Eric Glass" <jcifs at samba dot org>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  */
19 
20 package jcifs.http;
21 
22 import java.io.ByteArrayOutputStream;
23 import java.io.InputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 
27 import java.net.Authenticator;
28 import java.net.HttpURLConnection;
29 import java.net.PasswordAuthentication;
30 import java.net.ProtocolException;
31 import java.net.URL;
32 import java.net.URLDecoder;
33 
34 import java.security.Permission;
35 
36 import java.util.ArrayList;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.Iterator;
40 import java.util.List;
41 import java.util.Map;
42 
43 import jcifs.Config;
44 
45 import jcifs.ntlmssp.NtlmFlags;
46 import jcifs.ntlmssp.NtlmMessage;
47 import jcifs.ntlmssp.Type1Message;
48 import jcifs.ntlmssp.Type2Message;
49 import jcifs.ntlmssp.Type3Message;
50 
51 import jcifs.util.Base64;
52 
53 /**
54  * Wraps an <code>HttpURLConnection</code> to provide NTLM authentication
55  * services.
56  *
57  * Please read <a href="../../../httpclient.html">Using jCIFS NTLM Authentication for HTTP Connections</a>.
58  */
59 public class NtlmHttpURLConnection extends HttpURLConnection {
60 
61     private static final int MAX_REDIRECTS =
62             Integer.parseInt(System.getProperty("http.maxRedirects", "20"));
63 
64     private static final int LM_COMPATIBILITY =
65             Config.getInt("jcifs.smb.lmCompatibility", 0);
66 
67     private static final String DEFAULT_DOMAIN;
68 
69     private HttpURLConnection connection;
70 
71     private Map requestProperties;
72 
73     private Map headerFields;
74 
75     private ByteArrayOutputStream cachedOutput;
76 
77     private String authProperty;
78 
79     private String authMethod;
80 
81     private boolean handshakeComplete;
82 
83     static {
84         String domain = System.getProperty("http.auth.ntlm.domain");
85         if (domain == null) domain = Type3Message.getDefaultDomain();
86         DEFAULT_DOMAIN = domain;
87     }
88 
NtlmHttpURLConnection(HttpURLConnection connection)89     public NtlmHttpURLConnection(HttpURLConnection connection) {
90         super(connection.getURL());
91         this.connection = connection;
92         requestProperties = new HashMap();
93     }
94 
connect()95     public void connect() throws IOException {
96         if (connected) return;
97         connection.connect();
98         connected = true;
99     }
100 
handshake()101     private void handshake() throws IOException {
102         if (handshakeComplete) return;
103         doHandshake();
104         handshakeComplete = true;
105     }
106 
getURL()107     public URL getURL() {
108         return connection.getURL();
109     }
110 
getContentLength()111     public int getContentLength() {
112         try {
113             handshake();
114         } catch (IOException ex) { }
115         return connection.getContentLength();
116     }
117 
getContentType()118     public String getContentType() {
119         try {
120             handshake();
121         } catch (IOException ex) { }
122         return connection.getContentType();
123     }
124 
getContentEncoding()125     public String getContentEncoding() {
126         try {
127             handshake();
128         } catch (IOException ex) { }
129         return connection.getContentEncoding();
130     }
131 
getExpiration()132     public long getExpiration() {
133         try {
134             handshake();
135         } catch (IOException ex) { }
136         return connection.getExpiration();
137     }
138 
getDate()139     public long getDate() {
140         try {
141             handshake();
142         } catch (IOException ex) { }
143         return connection.getDate();
144     }
145 
getLastModified()146     public long getLastModified() {
147         try {
148             handshake();
149         } catch (IOException ex) { }
150         return connection.getLastModified();
151     }
152 
getHeaderField(String header)153     public String getHeaderField(String header) {
154         try {
155             handshake();
156         } catch (IOException ex) { }
157         return connection.getHeaderField(header);
158     }
159 
getHeaderFields0()160     private Map getHeaderFields0() {
161         if (headerFields != null) return headerFields;
162         Map map = new HashMap();
163         String key = connection.getHeaderFieldKey(0);
164         String value = connection.getHeaderField(0);
165         for (int i = 1; key != null || value != null; i++) {
166             List values = (List) map.get(key);
167             if (values == null) {
168                 values = new ArrayList();
169                 map.put(key, values);
170             }
171             values.add(value);
172             key = connection.getHeaderFieldKey(i);
173             value = connection.getHeaderField(i);
174         }
175         Iterator entries = map.entrySet().iterator();
176         while (entries.hasNext()) {
177             Map.Entry entry = (Map.Entry) entries.next();
178             entry.setValue(Collections.unmodifiableList((List)
179                     entry.getValue()));
180         }
181         return (headerFields = Collections.unmodifiableMap(map));
182     }
183 
getHeaderFields()184     public Map getHeaderFields() {
185         if (headerFields != null) return headerFields;
186         try {
187             handshake();
188         } catch (IOException ex) { }
189         return getHeaderFields0();
190     }
191 
getHeaderFieldInt(String header, int def)192     public int getHeaderFieldInt(String header, int def) {
193         try {
194             handshake();
195         } catch (IOException ex) { }
196         return connection.getHeaderFieldInt(header, def);
197     }
198 
getHeaderFieldDate(String header, long def)199     public long getHeaderFieldDate(String header, long def) {
200         try {
201             handshake();
202         } catch (IOException ex) { }
203         return connection.getHeaderFieldDate(header, def);
204     }
205 
getHeaderFieldKey(int index)206     public String getHeaderFieldKey(int index) {
207         try {
208             handshake();
209         } catch (IOException ex) { }
210         return connection.getHeaderFieldKey(index);
211     }
212 
getHeaderField(int index)213     public String getHeaderField(int index) {
214         try {
215             handshake();
216         } catch (IOException ex) { }
217         return connection.getHeaderField(index);
218     }
219 
getContent()220     public Object getContent() throws IOException {
221         try {
222             handshake();
223         } catch (IOException ex) { }
224         return connection.getContent();
225     }
226 
getContent(Class[] classes)227     public Object getContent(Class[] classes) throws IOException {
228         try {
229             handshake();
230         } catch (IOException ex) { }
231         return connection.getContent(classes);
232     }
233 
getPermission()234     public Permission getPermission() throws IOException {
235         return connection.getPermission();
236     }
237 
getInputStream()238     public InputStream getInputStream() throws IOException {
239         try {
240             handshake();
241         } catch (IOException ex) { }
242         return connection.getInputStream();
243     }
244 
getOutputStream()245     public OutputStream getOutputStream() throws IOException {
246         try {
247             connect();
248         } catch (IOException ex) { }
249         OutputStream output = connection.getOutputStream();
250         cachedOutput = new ByteArrayOutputStream();
251         return new CacheStream(output, cachedOutput);
252     }
253 
toString()254     public String toString() {
255         return connection.toString();
256     }
257 
setDoInput(boolean doInput)258     public void setDoInput(boolean doInput) {
259         connection.setDoInput(doInput);
260         this.doInput = doInput;
261     }
262 
getDoInput()263     public boolean getDoInput() {
264         return connection.getDoInput();
265     }
266 
setDoOutput(boolean doOutput)267     public void setDoOutput(boolean doOutput) {
268         connection.setDoOutput(doOutput);
269         this.doOutput = doOutput;
270     }
271 
getDoOutput()272     public boolean getDoOutput() {
273         return connection.getDoOutput();
274     }
275 
setAllowUserInteraction(boolean allowUserInteraction)276     public void setAllowUserInteraction(boolean allowUserInteraction) {
277         connection.setAllowUserInteraction(allowUserInteraction);
278         this.allowUserInteraction = allowUserInteraction;
279     }
280 
getAllowUserInteraction()281     public boolean getAllowUserInteraction() {
282         return connection.getAllowUserInteraction();
283     }
284 
setUseCaches(boolean useCaches)285     public void setUseCaches(boolean useCaches) {
286         connection.setUseCaches(useCaches);
287         this.useCaches = useCaches;
288     }
289 
getUseCaches()290     public boolean getUseCaches() {
291         return connection.getUseCaches();
292     }
293 
setIfModifiedSince(long ifModifiedSince)294     public void setIfModifiedSince(long ifModifiedSince) {
295         connection.setIfModifiedSince(ifModifiedSince);
296         this.ifModifiedSince = ifModifiedSince;
297     }
298 
getIfModifiedSince()299     public long getIfModifiedSince() {
300         return connection.getIfModifiedSince();
301     }
302 
getDefaultUseCaches()303     public boolean getDefaultUseCaches() {
304         return connection.getDefaultUseCaches();
305     }
306 
setDefaultUseCaches(boolean defaultUseCaches)307     public void setDefaultUseCaches(boolean defaultUseCaches) {
308         connection.setDefaultUseCaches(defaultUseCaches);
309     }
310 
setRequestProperty(String key, String value)311     public void setRequestProperty(String key, String value) {
312         if (key == null) throw new NullPointerException();
313         List values = new ArrayList();
314         values.add(value);
315         boolean found = false;
316         Iterator entries = requestProperties.entrySet().iterator();
317         while (entries.hasNext()) {
318             Map.Entry entry = (Map.Entry) entries.next();
319             if (key.equalsIgnoreCase((String) entry.getKey())) {
320                 entry.setValue(values);
321                 found = true;
322                 break;
323             }
324         }
325         if (!found) requestProperties.put(key, values);
326         connection.setRequestProperty(key, value);
327     }
328 
addRequestProperty(String key, String value)329     public void addRequestProperty(String key, String value) {
330         if (key == null) throw new NullPointerException();
331         List values = null;
332         Iterator entries = requestProperties.entrySet().iterator();
333         while (entries.hasNext()) {
334             Map.Entry entry = (Map.Entry) entries.next();
335             if (key.equalsIgnoreCase((String) entry.getKey())) {
336                 values = (List) entry.getValue();
337                 values.add(value);
338                 break;
339             }
340         }
341         if (values == null) {
342             values = new ArrayList();
343             values.add(value);
344             requestProperties.put(key, values);
345         }
346         // 1.3-compatible.
347         StringBuffer buffer = new StringBuffer();
348         Iterator propertyValues = values.iterator();
349         while (propertyValues.hasNext()) {
350             buffer.append(propertyValues.next());
351             if (propertyValues.hasNext()) {
352                 buffer.append(", ");
353             }
354         }
355         connection.setRequestProperty(key, buffer.toString());
356     }
357 
getRequestProperty(String key)358     public String getRequestProperty(String key) {
359         return connection.getRequestProperty(key);
360     }
361 
getRequestProperties()362     public Map getRequestProperties() {
363         Map map = new HashMap();
364         Iterator entries = requestProperties.entrySet().iterator();
365         while (entries.hasNext()) {
366             Map.Entry entry = (Map.Entry) entries.next();
367             map.put(entry.getKey(),
368                     Collections.unmodifiableList((List) entry.getValue()));
369         }
370         return Collections.unmodifiableMap(map);
371     }
372 
setInstanceFollowRedirects(boolean instanceFollowRedirects)373     public void setInstanceFollowRedirects(boolean instanceFollowRedirects) {
374         connection.setInstanceFollowRedirects(instanceFollowRedirects);
375     }
376 
getInstanceFollowRedirects()377     public boolean getInstanceFollowRedirects() {
378         return connection.getInstanceFollowRedirects();
379     }
380 
setRequestMethod(String requestMethod)381     public void setRequestMethod(String requestMethod)
382             throws ProtocolException {
383         connection.setRequestMethod(requestMethod);
384         this.method = requestMethod;
385     }
386 
getRequestMethod()387     public String getRequestMethod() {
388         return connection.getRequestMethod();
389     }
390 
getResponseCode()391     public int getResponseCode() throws IOException {
392         try {
393             handshake();
394         } catch (IOException ex) { }
395         return connection.getResponseCode();
396     }
397 
getResponseMessage()398     public String getResponseMessage() throws IOException {
399         try {
400             handshake();
401         } catch (IOException ex) { }
402         return connection.getResponseMessage();
403     }
404 
disconnect()405     public void disconnect() {
406         connection.disconnect();
407         handshakeComplete = false;
408         connected = false;
409     }
410 
usingProxy()411     public boolean usingProxy() {
412         return connection.usingProxy();
413     }
414 
getErrorStream()415     public InputStream getErrorStream() {
416         try {
417             handshake();
418         } catch (IOException ex) { }
419         return connection.getErrorStream();
420     }
421 
parseResponseCode()422     private int parseResponseCode() throws IOException {
423         try {
424             String response = connection.getHeaderField(0);
425             int index = response.indexOf(' ');
426             while (response.charAt(index) == ' ') index++;
427             return Integer.parseInt(response.substring(index, index + 3));
428         } catch (Exception ex) {
429             throw new IOException(ex.getMessage());
430         }
431     }
432 
doHandshake()433     private void doHandshake() throws IOException {
434         connect();
435         try {
436             int response = parseResponseCode();
437             if (response != HTTP_UNAUTHORIZED && response != HTTP_PROXY_AUTH) {
438                 return;
439             }
440             Type1Message type1 = (Type1Message) attemptNegotiation(response);
441             if (type1 == null) return; // no NTLM
442             int attempt = 0;
443             while (attempt < MAX_REDIRECTS) {
444                 connection.setRequestProperty(authProperty, authMethod + ' ' +
445                         Base64.encode(type1.toByteArray()));
446                 connection.connect(); // send type 1
447                 response = parseResponseCode();
448                 if (response != HTTP_UNAUTHORIZED &&
449                         response != HTTP_PROXY_AUTH) {
450                     return;
451                 }
452                 Type3Message type3 = (Type3Message)
453                         attemptNegotiation(response);
454                 if (type3 == null) return;
455                 connection.setRequestProperty(authProperty, authMethod + ' ' +
456                         Base64.encode(type3.toByteArray()));
457                 connection.connect(); // send type 3
458                 if (cachedOutput != null && doOutput) {
459                     OutputStream output = connection.getOutputStream();
460                     cachedOutput.writeTo(output);
461                     output.flush();
462                 }
463                 response = parseResponseCode();
464                 if (response != HTTP_UNAUTHORIZED &&
465                         response != HTTP_PROXY_AUTH) {
466                     return;
467                 }
468                 attempt++;
469                 if (allowUserInteraction && attempt < MAX_REDIRECTS) {
470                     reconnect();
471                 } else {
472                     break;
473                 }
474             }
475             throw new IOException("Unable to negotiate NTLM authentication.");
476         } finally {
477             cachedOutput = null;
478         }
479     }
480 
attemptNegotiation(int response)481     private NtlmMessage attemptNegotiation(int response) throws IOException {
482         authProperty = null;
483         authMethod = null;
484         InputStream errorStream = connection.getErrorStream();
485         if (errorStream != null && errorStream.available() != 0) {
486             int count;
487             byte[] buf = new byte[1024];
488             while ((count = errorStream.read(buf, 0, 1024)) != -1);
489         }
490         String authHeader;
491         if (response == HTTP_UNAUTHORIZED) {
492             authHeader = "WWW-Authenticate";
493             authProperty = "Authorization";
494         } else {
495             authHeader = "Proxy-Authenticate";
496             authProperty = "Proxy-Authorization";
497         }
498         String authorization = null;
499         List methods = (List) getHeaderFields0().get(authHeader);
500         if (methods == null) return null;
501         Iterator iterator = methods.iterator();
502         while (iterator.hasNext()) {
503             String currentAuthMethod = (String) iterator.next();
504             if (currentAuthMethod.startsWith("NTLM")) {
505                 if (currentAuthMethod.length() == 4) {
506                     authMethod = "NTLM";
507                     break;
508                 }
509                 if (currentAuthMethod.indexOf(' ') != 4) continue;
510                 authMethod = "NTLM";
511                 authorization = currentAuthMethod.substring(5).trim();
512                 break;
513             } else if (currentAuthMethod.startsWith("Negotiate")) {
514                 if (currentAuthMethod.length() == 9) {
515                     authMethod = "Negotiate";
516                     break;
517                 }
518                 if (currentAuthMethod.indexOf(' ') != 9) continue;
519                 authMethod = "Negotiate";
520                 authorization = currentAuthMethod.substring(10).trim();
521                 break;
522             }
523         }
524         if (authMethod == null) return null;
525         NtlmMessage message = (authorization != null) ?
526                 new Type2Message(Base64.decode(authorization)) : null;
527         reconnect();
528         if (message == null) {
529             message = new Type1Message();
530             if (LM_COMPATIBILITY > 2) {
531                 message.setFlag(NtlmFlags.NTLMSSP_REQUEST_TARGET, true);
532             }
533         } else {
534             String domain = DEFAULT_DOMAIN;
535             String user = Type3Message.getDefaultUser();
536             String password = Type3Message.getDefaultPassword();
537             String userInfo = url.getUserInfo();
538             if (userInfo != null) {
539                 userInfo = URLDecoder.decode(userInfo);
540                 int index = userInfo.indexOf(':');
541                 user = (index != -1) ? userInfo.substring(0, index) : userInfo;
542                 if (index != -1) password = userInfo.substring(index + 1);
543                 index = user.indexOf('\\');
544                 if (index == -1) index = user.indexOf('/');
545                 domain = (index != -1) ? user.substring(0, index) : domain;
546                 user = (index != -1) ? user.substring(index + 1) : user;
547             }
548             if (user == null) {
549                 if (!allowUserInteraction) return null;
550                 try {
551                     URL url = getURL();
552                     String protocol = url.getProtocol();
553                     int port = url.getPort();
554                     if (port == -1) {
555                         port = "https".equalsIgnoreCase(protocol) ? 443 : 80;
556                     }
557                     PasswordAuthentication auth =
558                             Authenticator.requestPasswordAuthentication(null,
559                                     port, protocol, "", authMethod);
560                     if (auth == null) return null;
561                     user = auth.getUserName();
562                     password = new String(auth.getPassword());
563                 } catch (Exception ex) { }
564             }
565             Type2Message type2 = (Type2Message) message;
566             message = new Type3Message(type2, password, domain, user,
567                     Type3Message.getDefaultWorkstation(), 0);
568         }
569         return message;
570     }
571 
reconnect()572     private void reconnect() throws IOException {
573         connection = (HttpURLConnection) connection.getURL().openConnection();
574         connection.setRequestMethod(method);
575         headerFields = null;
576         Iterator properties = requestProperties.entrySet().iterator();
577         while (properties.hasNext()) {
578             Map.Entry property = (Map.Entry) properties.next();
579             String key = (String) property.getKey();
580             StringBuffer value = new StringBuffer();
581             Iterator values = ((List) property.getValue()).iterator();
582             while (values.hasNext()) {
583                 value.append(values.next());
584                 if (values.hasNext()) value.append(", ");
585             }
586             connection.setRequestProperty(key, value.toString());
587         }
588         connection.setAllowUserInteraction(allowUserInteraction);
589         connection.setDoInput(doInput);
590         connection.setDoOutput(doOutput);
591         connection.setIfModifiedSince(ifModifiedSince);
592         connection.setUseCaches(useCaches);
593     }
594 
595     private static class CacheStream extends OutputStream {
596 
597         private final OutputStream stream;
598 
599         private final OutputStream collector;
600 
CacheStream(OutputStream stream, OutputStream collector)601         public CacheStream(OutputStream stream, OutputStream collector) {
602             this.stream = stream;
603             this.collector = collector;
604         }
605 
close()606         public void close() throws IOException {
607             stream.close();
608             collector.close();
609         }
610 
flush()611         public void flush() throws IOException {
612             stream.flush();
613             collector.flush();
614         }
615 
write(byte[] b)616         public void write(byte[] b) throws IOException {
617             stream.write(b);
618             collector.write(b);
619         }
620 
write(byte[] b, int off, int len)621         public void write(byte[] b, int off, int len) throws IOException {
622             stream.write(b, off, len);
623             collector.write(b, off, len);
624         }
625 
write(int b)626         public void write(int b) throws IOException {
627             stream.write(b);
628             collector.write(b);
629         }
630 
631     }
632 
633 }
634