View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2009  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program 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
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.http;
20  
21  import davmail.BundleMessage;
22  import davmail.Settings;
23  import davmail.exception.*;
24  import davmail.exchange.dav.ExchangeDavMethod;
25  import davmail.exchange.dav.ExchangeSearchMethod;
26  import davmail.ui.tray.DavGatewayTray;
27  import org.apache.commons.httpclient.*;
28  import org.apache.commons.httpclient.URI;
29  import org.apache.commons.httpclient.auth.AuthPolicy;
30  import org.apache.commons.httpclient.auth.AuthScope;
31  import org.apache.commons.httpclient.cookie.CookiePolicy;
32  import org.apache.commons.httpclient.methods.DeleteMethod;
33  import org.apache.commons.httpclient.methods.GetMethod;
34  import org.apache.commons.httpclient.params.HttpClientParams;
35  import org.apache.commons.httpclient.params.HttpMethodParams;
36  import org.apache.commons.httpclient.util.IdleConnectionTimeoutThread;
37  import org.apache.commons.httpclient.util.URIUtil;
38  import org.apache.jackrabbit.webdav.DavException;
39  import org.apache.jackrabbit.webdav.MultiStatusResponse;
40  import org.apache.jackrabbit.webdav.client.methods.CopyMethod;
41  import org.apache.jackrabbit.webdav.client.methods.DavMethodBase;
42  import org.apache.jackrabbit.webdav.client.methods.MoveMethod;
43  import org.apache.jackrabbit.webdav.client.methods.PropFindMethod;
44  import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
45  import org.apache.log4j.Logger;
46  
47  import java.io.IOException;
48  import java.net.*;
49  import java.util.ArrayList;
50  import java.util.Collection;
51  import java.util.List;
52  import java.util.regex.Matcher;
53  import java.util.regex.Pattern;
54  
55  /**
56   * Create HttpClient instance according to DavGateway Settings
57   */
58  public final class DavGatewayHttpClientFacade {
59      static final Logger LOGGER = Logger.getLogger("davmail.http.DavGatewayHttpClientFacade");
60  
61      static final String IE_USER_AGENT = "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0)";
62      static final int MAX_REDIRECTS = 10;
63      static final Object LOCK = new Object();
64      private static boolean needNTLM;
65  
66      static final long ONE_MINUTE = 60000;
67  
68      static String WORKSTATION_NAME = "UNKNOWN";
69  
70      private static IdleConnectionTimeoutThread httpConnectionManagerThread;
71  
72      static {
73          // disable Client-initiated TLS renegotiation
74          System.setProperty("jdk.tls.rejectClientInitiatedRenegotiation", "true");
75              // force strong ephemeral Diffie-Hellman parameter
76          System.setProperty("jdk.tls.ephemeralDHKeySize", "2048");
77  
78          DavGatewayHttpClientFacade.start();
79  
80          // register custom cookie policy
81          CookiePolicy.registerCookieSpec("DavMailCookieSpec", DavMailCookieSpec.class);
82  
83          AuthPolicy.registerAuthScheme(AuthPolicy.BASIC, LenientBasicScheme.class);
84          try {
85              WORKSTATION_NAME = InetAddress.getLocalHost().getHostName();
86          } catch (Throwable t) {
87              // ignore
88          }
89      }
90  
91  
92      private DavGatewayHttpClientFacade() {
93      }
94  
95      /**
96       * Create basic http client with default params.
97       *
98       * @return HttpClient instance
99       */
100     private static HttpClient getBaseInstance() {
101         HttpClient httpClient = new HttpClient();
102         httpClient.getParams().setParameter(HttpMethodParams.USER_AGENT, IE_USER_AGENT);
103         httpClient.getParams().setParameter(HttpClientParams.MAX_REDIRECTS, Settings.getIntProperty("davmail.httpMaxRedirects", MAX_REDIRECTS));
104         httpClient.getParams().setCookiePolicy("DavMailCookieSpec");
105         return httpClient;
106     }
107 
108     /**
109      * Create a configured HttpClient instance.
110      *
111      * @param url target url
112      * @return httpClient
113      * @throws DavMailException on error
114      */
115     public static HttpClient getInstance(String url) throws DavMailException {
116         // create an HttpClient instance
117         HttpClient httpClient = getBaseInstance();
118         configureClient(httpClient, url);
119         return httpClient;
120     }
121 
122     /**
123      * Set credentials on HttpClient instance.
124      *
125      * @param httpClient httpClient instance
126      * @param userName   user name
127      * @param password   user password
128      */
129     public static void setCredentials(HttpClient httpClient, String userName, String password) {
130         // some Exchange servers redirect to a different host for freebusy, use wide auth scope
131         AuthScope authScope = new AuthScope(null, -1);
132         int backSlashIndex = userName.indexOf('\\');
133         if (needNTLM && backSlashIndex >= 0) {
134             // separate domain from username in credentials
135             String domain = userName.substring(0, backSlashIndex);
136             userName = userName.substring(backSlashIndex + 1);
137             httpClient.getState().setCredentials(authScope, new NTCredentials(userName, password, WORKSTATION_NAME, domain));
138         } else {
139             httpClient.getState().setCredentials(authScope, new NTCredentials(userName, password, WORKSTATION_NAME, ""));
140         }
141     }
142 
143     /**
144      * Set http client current host configuration.
145      *
146      * @param httpClient current Http client
147      * @param url        target url
148      * @throws DavMailException on error
149      */
150     public static void setClientHost(HttpClient httpClient, String url) throws DavMailException {
151         try {
152             HostConfiguration hostConfig = httpClient.getHostConfiguration();
153             URI httpURI = new URI(url, true);
154             hostConfig.setHost(httpURI);
155         } catch (URIException e) {
156             throw new DavMailException("LOG_INVALID_URL", url);
157         }
158     }
159 
160     protected static boolean isNoProxyFor(java.net.URI uri) {
161         final String noProxyFor = Settings.getProperty("davmail.noProxyFor");
162         if (noProxyFor != null) {
163             final String urihost = uri.getHost().toLowerCase();
164             final String[] domains = noProxyFor.toLowerCase().split(",\\s*");
165             for (String domain : domains) {
166                 if (urihost.endsWith(domain)) {
167                     return true; //break;
168                 }
169             }
170         }
171         return false;
172     }
173 
174     /**
175      * Update http client configuration (proxy)
176      *
177      * @param httpClient current Http client
178      * @param url        target url
179      * @throws DavMailException on error
180      */
181     public static void configureClient(HttpClient httpClient, String url) throws DavMailException {
182         setClientHost(httpClient, url);
183 
184         // force NTLM in direct EWS mode
185         if (!needNTLM && url.toLowerCase().endsWith("/ews/exchange.asmx") && !Settings.getBooleanProperty("davmail.disableNTLM", false)) {
186             needNTLM = true;
187         }
188 
189         if (Settings.getBooleanProperty("davmail.enableKerberos", false)) {
190             AuthPolicy.registerAuthScheme("Negotiate", SpNegoScheme.class);
191             ArrayList<String> authPrefs = new ArrayList<String>();
192             authPrefs.add("Negotiate");
193             httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
194         } else if (!needNTLM) {
195             ArrayList<String> authPrefs = new ArrayList<String>();
196             authPrefs.add(AuthPolicy.DIGEST);
197             authPrefs.add(AuthPolicy.BASIC);
198             // exclude NTLM authentication scheme
199             httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
200         }
201 
202         boolean enableProxy = Settings.getBooleanProperty("davmail.enableProxy");
203         boolean useSystemProxies = Settings.getBooleanProperty("davmail.useSystemProxies", Boolean.FALSE);
204         String proxyHost = null;
205         int proxyPort = 0;
206         String proxyUser = null;
207         String proxyPassword = null;
208 
209         try {
210             java.net.URI uri = new java.net.URI(url);
211             if (isNoProxyFor(uri)) {
212                 LOGGER.debug("no proxy for " + uri.getHost());
213             } else if (useSystemProxies) {
214                 // get proxy for url from system settings
215                 System.setProperty("java.net.useSystemProxies", "true");
216                 List<Proxy> proxyList = getProxyForURI(uri);
217                 if (!proxyList.isEmpty() && proxyList.get(0).address() != null) {
218                     InetSocketAddress inetSocketAddress = (InetSocketAddress) proxyList.get(0).address();
219                     proxyHost = inetSocketAddress.getHostName();
220                     proxyPort = inetSocketAddress.getPort();
221 
222                     // we may still need authentication credentials
223                     proxyUser = Settings.getProperty("davmail.proxyUser");
224                     proxyPassword = Settings.getProperty("davmail.proxyPassword");
225                 }
226             } else if (enableProxy) {
227                 proxyHost = Settings.getProperty("davmail.proxyHost");
228                 proxyPort = Settings.getIntProperty("davmail.proxyPort");
229                 proxyUser = Settings.getProperty("davmail.proxyUser");
230                 proxyPassword = Settings.getProperty("davmail.proxyPassword");
231             }
232         } catch (URISyntaxException e) {
233             throw new DavMailException("LOG_INVALID_URL", url);
234         }
235 
236         // configure proxy
237         if (proxyHost != null && proxyHost.length() > 0) {
238             httpClient.getHostConfiguration().setProxy(proxyHost, proxyPort);
239             if (proxyUser != null && proxyUser.length() > 0) {
240 
241                 AuthScope authScope = new AuthScope(proxyHost, proxyPort, AuthScope.ANY_REALM);
242 
243                 // detect ntlm authentication (windows domain name in user name)
244                 int backslashindex = proxyUser.indexOf('\\');
245                 if (backslashindex > 0) {
246                     httpClient.getState().setProxyCredentials(authScope,
247                             new NTCredentials(proxyUser.substring(backslashindex + 1),
248                                     proxyPassword, WORKSTATION_NAME,
249                                     proxyUser.substring(0, backslashindex)));
250                 } else {
251                     httpClient.getState().setProxyCredentials(authScope,
252                             new NTCredentials(proxyUser, proxyPassword, WORKSTATION_NAME, ""));
253                 }
254             }
255         }
256 
257     }
258 
259     /**
260      * Make sure we close all connections immediately after a session creation failure.
261      *
262      * @param httpClient http client to close
263      */
264     public static void close(HttpClient httpClient) {
265         if (httpClient != null) {
266             ((MultiThreadedHttpConnectionManager) httpClient.getHttpConnectionManager()).shutdown();
267         }
268     }
269 
270     /**
271      * Retrieve Proxy Selector
272      *
273      * @param uri target uri
274      * @return proxy selector
275      */
276     private static List<Proxy> getProxyForURI(java.net.URI uri) {
277         LOGGER.debug("get Default proxy selector");
278         ProxySelector proxySelector = ProxySelector.getDefault();
279         LOGGER.debug("getProxyForURI(" + uri + ')');
280         List<Proxy> proxies = proxySelector.select(uri);
281         LOGGER.debug("got system proxies:" + proxies);
282         return proxies;
283     }
284 
285 
286     /**
287      * Get Http Status code for the given URL
288      *
289      * @param httpClient httpClient instance
290      * @param url        url string
291      * @return HttpStatus code
292      */
293     public static int getHttpStatus(HttpClient httpClient, String url) {
294         int status = 0;
295         HttpMethod testMethod = new GetMethod(url);
296         testMethod.setDoAuthentication(false);
297         try {
298             status = httpClient.executeMethod(testMethod);
299         } catch (IOException e) {
300             LOGGER.warn(e.getMessage(), e);
301         } finally {
302             testMethod.releaseConnection();
303         }
304         return status;
305     }
306 
307     /**
308      * Check if status is a redirect (various 30x values).
309      *
310      * @param status Http status
311      * @return true if status is a redirect
312      */
313     public static boolean isRedirect(int status) {
314         return status == HttpStatus.SC_MOVED_PERMANENTLY
315                 || status == HttpStatus.SC_MOVED_TEMPORARILY
316                 || status == HttpStatus.SC_SEE_OTHER
317                 || status == HttpStatus.SC_TEMPORARY_REDIRECT;
318     }
319 
320     /**
321      * Execute given url, manually follow redirects.
322      * Workaround for HttpClient bug (GET full URL over HTTPS and proxy)
323      *
324      * @param httpClient HttpClient instance
325      * @param url        url string
326      * @return executed method
327      * @throws IOException on error
328      */
329     public static HttpMethod executeFollowRedirects(HttpClient httpClient, String url) throws IOException {
330         HttpMethod method = new GetMethod(url);
331         method.setFollowRedirects(false);
332         return executeFollowRedirects(httpClient, method);
333     }
334 
335     private static int checkNTLM(HttpClient httpClient, HttpMethod currentMethod) throws IOException {
336         int status = currentMethod.getStatusCode();
337         if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
338                 && acceptsNTLMOnly(currentMethod) && !hasNTLMorNegotiate(httpClient)) {
339             LOGGER.debug("Received " + status + " unauthorized at " + currentMethod.getURI() + ", retrying with NTLM");
340             resetMethod(currentMethod);
341             addNTLM(httpClient);
342             status = httpClient.executeMethod(currentMethod);
343         }
344         return status;
345     }
346 
347     /**
348      * Checks if there is a Javascript redirect inside the page,
349      * and returns it.
350      * <p/>
351      * A Javascript redirect is usually found on OTP pre-auth page,
352      * when the pre-auth form is in a distinct page from the regular Exchange login one.
353      *
354      * @param method http method
355      * @return the redirect URL if found, or null if no Javascript redirect has been found
356      */
357     private static String getJavascriptRedirectUrl(HttpMethod method) throws IOException {
358         String responseBody = method.getResponseBodyAsString();
359         String jsRedirectionUrl = null;
360         if (responseBody.indexOf("javascript:go_url()") > 0) {
361             // Create a pattern to match a javascript redirect url
362             Pattern p = Pattern.compile("go_url\\(\\)[^{]+\\{[^l]+location.replace\\(\"(/[^\"]+)\"\\)");
363             Matcher m = p.matcher(responseBody);
364             if (m.find()) {
365                 // Javascript redirect found!
366                 jsRedirectionUrl = m.group(1);
367             }
368         }
369         return jsRedirectionUrl;
370     }
371 
372 
373     private static String getLocationValue(HttpMethod method) throws URIException {
374         String locationValue = null;
375         Header location = method.getResponseHeader("Location");
376         if (location != null && isRedirect(method.getStatusCode())) {
377             locationValue = location.getValue();
378             // Novell iChain workaround
379             if (locationValue.indexOf('"') >= 0) {
380                 locationValue = URIUtil.encodePath(locationValue);
381             }
382             // workaround for invalid relative location
383             if (locationValue.startsWith("./")) {
384                 locationValue = locationValue.substring(1);
385             }
386         }
387         return locationValue;
388     }
389 
390     /**
391      * Execute method with httpClient, follow 30x redirects.
392      *
393      * @param httpClient Http client instance
394      * @param method     Http method
395      * @return last http method after redirects
396      * @throws IOException on error
397      */
398     public static HttpMethod executeFollowRedirects(HttpClient httpClient, HttpMethod method) throws IOException {
399         HttpMethod currentMethod = method;
400         try {
401             DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS", currentMethod.getURI()));
402             httpClient.executeMethod(currentMethod);
403             checkNTLM(httpClient, currentMethod);
404 
405             String locationValue = getLocationValue(currentMethod);
406             // check javascript redirect (multiple authentication pages)
407             if (locationValue == null) {
408                 locationValue = getJavascriptRedirectUrl(currentMethod);
409             }
410 
411             int redirectCount = 0;
412             while (redirectCount++ < Settings.getIntProperty("davmail.httpMaxRedirects", MAX_REDIRECTS)
413                     && locationValue != null) {
414                 currentMethod.releaseConnection();
415                 currentMethod = new GetMethod(locationValue);
416                 currentMethod.setFollowRedirects(false);
417                 DavGatewayTray.debug(new BundleMessage("LOG_EXECUTE_FOLLOW_REDIRECTS_COUNT", currentMethod.getURI(), redirectCount));
418                 httpClient.executeMethod(currentMethod);
419                 checkNTLM(httpClient, currentMethod);
420                 locationValue = getLocationValue(currentMethod);
421             }
422             if (locationValue != null) {
423                 currentMethod.releaseConnection();
424                 throw new HttpException("Maximum redirections reached");
425             }
426         } catch (IOException e) {
427             currentMethod.releaseConnection();
428             throw e;
429         }
430         // caller will need to release connection
431         return currentMethod;
432     }
433 
434     /**
435      * Execute method with httpClient, do not follow 30x redirects.
436      *
437      * @param httpClient Http client instance
438      * @param method     Http method
439      * @return status
440      * @throws IOException on error
441      */
442     public static int executeNoRedirect(HttpClient httpClient, HttpMethod method) throws IOException {
443         int status;
444         try {
445             status = httpClient.executeMethod(method);
446             // check NTLM
447             if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
448                     && acceptsNTLMOnly(method) && !hasNTLMorNegotiate(httpClient)) {
449                 LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
450                 resetMethod(method);
451                 addNTLM(httpClient);
452                 status = httpClient.executeMethod(method);
453             }
454         } finally {
455             method.releaseConnection();
456         }
457         // caller will need to release connection
458         return status;
459     }
460 
461     /**
462      * Execute webdav search method.
463      *
464      * @param httpClient    http client instance
465      * @param path          <i>encoded</i> searched folder path
466      * @param searchRequest (SQL like) search request
467      * @param maxCount      max item count
468      * @return Responses enumeration
469      * @throws IOException on error
470      */
471     public static MultiStatusResponse[] executeSearchMethod(HttpClient httpClient, String path, String searchRequest,
472                                                             int maxCount) throws IOException {
473         ExchangeSearchMethod searchMethod = new ExchangeSearchMethod(path, searchRequest);
474         if (maxCount > 0) {
475             searchMethod.addRequestHeader("Range", "rows=0-" + (maxCount - 1));
476         }
477         return executeMethod(httpClient, searchMethod);
478     }
479 
480     /**
481      * Execute webdav propfind method.
482      *
483      * @param httpClient http client instance
484      * @param path       <i>encoded</i> searched folder path
485      * @param depth      propfind request depth
486      * @param properties propfind requested properties
487      * @return Responses enumeration
488      * @throws IOException on error
489      */
490     public static MultiStatusResponse[] executePropFindMethod(HttpClient httpClient, String path, int depth, DavPropertyNameSet properties) throws IOException {
491         PropFindMethod propFindMethod = new PropFindMethod(path, properties, depth);
492         return executeMethod(httpClient, propFindMethod);
493     }
494 
495     /**
496      * Execute a delete method on the given path with httpClient.
497      *
498      * @param httpClient Http client instance
499      * @param path       Path to be deleted
500      * @return http status
501      * @throws IOException on error
502      */
503     public static int executeDeleteMethod(HttpClient httpClient, String path) throws IOException {
504         DeleteMethod deleteMethod = new DeleteMethod(path);
505         deleteMethod.setFollowRedirects(false);
506 
507         int status = executeHttpMethod(httpClient, deleteMethod);
508         // do not throw error if already deleted
509         if (status != HttpStatus.SC_OK && status != HttpStatus.SC_NOT_FOUND) {
510             throw DavGatewayHttpClientFacade.buildHttpException(deleteMethod);
511         }
512         return status;
513     }
514 
515     /**
516      * Execute webdav request.
517      *
518      * @param httpClient http client instance
519      * @param method     webdav method
520      * @return Responses enumeration
521      * @throws IOException on error
522      */
523     public static MultiStatusResponse[] executeMethod(HttpClient httpClient, DavMethodBase method) throws IOException {
524         MultiStatusResponse[] responses = null;
525         try {
526             int status = httpClient.executeMethod(method);
527 
528             // need to follow redirects (once) on public folders
529             if (isRedirect(status)) {
530                 method.releaseConnection();
531                 URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true);
532                 checkExpiredSession(targetUri.getQuery());
533                 method.setURI(targetUri);
534                 status = httpClient.executeMethod(method);
535             }
536 
537             if (status != HttpStatus.SC_MULTI_STATUS) {
538                 throw buildHttpException(method);
539             }
540             responses = method.getResponseBodyAsMultiStatus().getResponses();
541 
542         } catch (DavException e) {
543             throw new IOException(e.getMessage());
544         } finally {
545             method.releaseConnection();
546         }
547         return responses;
548     }
549 
550     /**
551      * Execute webdav request.
552      *
553      * @param httpClient http client instance
554      * @param method     webdav method
555      * @return Responses enumeration
556      * @throws IOException on error
557      */
558     public static MultiStatusResponse[] executeMethod(HttpClient httpClient, ExchangeDavMethod method) throws IOException {
559         MultiStatusResponse[] responses = null;
560         try {
561             int status = httpClient.executeMethod(method);
562 
563             // need to follow redirects (once) on public folders
564             if (isRedirect(status)) {
565                 method.releaseConnection();
566                 URI targetUri = new URI(method.getResponseHeader("Location").getValue(), true);
567                 checkExpiredSession(targetUri.getQuery());
568                 method.setURI(targetUri);
569                 status = httpClient.executeMethod(method);
570             }
571 
572             if (status != HttpStatus.SC_MULTI_STATUS) {
573                 throw buildHttpException(method);
574             }
575             responses = method.getResponses();
576 
577         } finally {
578             method.releaseConnection();
579         }
580         return responses;
581     }
582 
583     /**
584      * Execute method with httpClient.
585      *
586      * @param httpClient Http client instance
587      * @param method     Http method
588      * @return Http status
589      * @throws IOException on error
590      */
591     public static int executeHttpMethod(HttpClient httpClient, HttpMethod method) throws IOException {
592         int status = 0;
593         try {
594             status = httpClient.executeMethod(method);
595         } finally {
596             method.releaseConnection();
597         }
598         return status;
599     }
600 
601     /**
602      * Test if NTLM auth scheme is enabled.
603      *
604      * @param httpClient HttpClient instance
605      * @return true if NTLM is enabled
606      */
607     public static boolean hasNTLMorNegotiate(HttpClient httpClient) {
608         Object authPrefs = httpClient.getParams().getParameter(AuthPolicy.AUTH_SCHEME_PRIORITY);
609         return authPrefs == null || (authPrefs instanceof List<?> &&
610                 (((Collection) authPrefs).contains(AuthPolicy.NTLM) || ((Collection) authPrefs).contains("Negotiate")));
611     }
612 
613     /**
614      * Enable NTLM authentication on http client
615      *
616      * @param httpClient HttpClient instance
617      */
618     public static void addNTLM(HttpClient httpClient) {
619         // disable preemptive authentication
620         httpClient.getParams().setParameter(HttpClientParams.PREEMPTIVE_AUTHENTICATION, false);
621 
622         // register the jcifs based NTLMv2 implementation
623         AuthPolicy.registerAuthScheme(AuthPolicy.NTLM, NTLMv2Scheme.class);
624 
625         ArrayList<String> authPrefs = new ArrayList<String>();
626         authPrefs.add(AuthPolicy.NTLM);
627         authPrefs.add(AuthPolicy.DIGEST);
628         authPrefs.add(AuthPolicy.BASIC);
629         httpClient.getParams().setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, authPrefs);
630 
631         // make sure NTLM is always active
632         needNTLM = true;
633 
634         // separate domain from username in credentials
635         AuthScope authScope = new AuthScope(null, -1);
636         NTCredentials credentials = (NTCredentials) httpClient.getState().getCredentials(authScope);
637         setCredentials(httpClient, credentials.getUserName(), credentials.getPassword());
638     }
639 
640     /**
641      * Test method header for supported authentication mode,
642      * return true if Basic authentication is not available
643      *
644      * @param getMethod http method
645      * @return true if only NTLM is enabled
646      */
647     public static boolean acceptsNTLMOnly(HttpMethod getMethod) {
648         Header authenticateHeader = null;
649         if (getMethod.getStatusCode() == HttpStatus.SC_UNAUTHORIZED) {
650             authenticateHeader = getMethod.getResponseHeader("WWW-Authenticate");
651         } else if (getMethod.getStatusCode() == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) {
652             authenticateHeader = getMethod.getResponseHeader("Proxy-Authenticate");
653         }
654         if (authenticateHeader == null) {
655             return false;
656         } else {
657             boolean acceptBasic = false;
658             boolean acceptNTLM = false;
659             HeaderElement[] headerElements = authenticateHeader.getElements();
660             for (HeaderElement headerElement : headerElements) {
661                 if ("NTLM".equalsIgnoreCase(headerElement.getName())) {
662                     acceptNTLM = true;
663                 }
664                 if ("Basic realm".equalsIgnoreCase(headerElement.getName())) {
665                     acceptBasic = true;
666                 }
667             }
668             return acceptNTLM && !acceptBasic;
669 
670         }
671     }
672 
673     /**
674      * Execute test method from checkConfig, with proxy credentials, but without Exchange credentials.
675      *
676      * @param httpClient Http client instance
677      * @param method     Http method
678      * @return Http status
679      * @throws IOException on error
680      */
681     public static int executeTestMethod(HttpClient httpClient, GetMethod method) throws IOException {
682         // do not follow redirects in expired sessions
683         method.setFollowRedirects(false);
684         int status = httpClient.executeMethod(method);
685         if (status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED
686                 && acceptsNTLMOnly(method) && !hasNTLMorNegotiate(httpClient)) {
687             resetMethod(method);
688             LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
689             addNTLM(httpClient);
690             status = httpClient.executeMethod(method);
691         }
692 
693         return status;
694     }
695 
696     /**
697      * Execute Get method, do not follow redirects.
698      *
699      * @param httpClient      Http client instance
700      * @param method          Http method
701      * @param followRedirects Follow redirects flag
702      * @throws IOException on error
703      */
704     public static void executeGetMethod(HttpClient httpClient, GetMethod method, boolean followRedirects) throws IOException {
705         // do not follow redirects in expired sessions
706         method.setFollowRedirects(followRedirects);
707         int status = httpClient.executeMethod(method);
708         if ((status == HttpStatus.SC_UNAUTHORIZED || status == HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED)
709                 && acceptsNTLMOnly(method) && !hasNTLMorNegotiate(httpClient)) {
710             resetMethod(method);
711             LOGGER.debug("Received " + status + " unauthorized at " + method.getURI() + ", retrying with NTLM");
712             addNTLM(httpClient);
713             status = httpClient.executeMethod(method);
714         }
715         if (status != HttpStatus.SC_OK && (followRedirects || !isRedirect(status))) {
716             LOGGER.warn("GET failed with status " + status + " at " + method.getURI());
717             if (status != HttpStatus.SC_NOT_FOUND && status != HttpStatus.SC_FORBIDDEN) {
718                 LOGGER.warn(method.getResponseBodyAsString());
719             }
720             throw DavGatewayHttpClientFacade.buildHttpException(method);
721         }
722         // check for expired session
723         if (followRedirects) {
724             String queryString = method.getQueryString();
725             checkExpiredSession(queryString);
726         }
727     }
728 
729     private static void resetMethod(HttpMethod method) {
730         // reset method state
731         method.releaseConnection();
732         method.getHostAuthState().invalidate();
733         method.getProxyAuthState().invalidate();
734     }
735 
736     private static void checkExpiredSession(String queryString) throws DavMailAuthenticationException {
737         if (queryString != null && (queryString.contains("reason=2") || queryString.contains("reason=0"))) {
738             LOGGER.warn("Request failed, session expired");
739             throw new DavMailAuthenticationException("EXCEPTION_SESSION_EXPIRED");
740         }
741     }
742 
743     /**
744      * Build Http Exception from methode status
745      *
746      * @param method Http Method
747      * @return Http Exception
748      */
749     public static HttpException buildHttpException(HttpMethod method) {
750         int status = method.getStatusCode();
751         StringBuilder message = new StringBuilder();
752         message.append(status).append(' ').append(method.getStatusText());
753         try {
754             message.append(" at ").append(method.getURI().getURI());
755             if (method instanceof CopyMethod || method instanceof MoveMethod) {
756                 message.append(" to ").append(method.getRequestHeader("Destination"));
757             }
758         } catch (URIException e) {
759             message.append(method.getPath());
760         }
761         // 440 means forbidden on Exchange
762         if (status == 440) {
763             return new LoginTimeoutException(message.toString());
764         } else if (status == HttpStatus.SC_FORBIDDEN) {
765             return new HttpForbiddenException(message.toString());
766         } else if (status == HttpStatus.SC_NOT_FOUND) {
767             return new HttpNotFoundException(message.toString());
768         } else if (status == HttpStatus.SC_PRECONDITION_FAILED) {
769             return new HttpPreconditionFailedException(message.toString());
770         } else if (status == HttpStatus.SC_INTERNAL_SERVER_ERROR) {
771             return new HttpServerErrorException(message.toString());
772         } else {
773             return new HttpException(message.toString());
774         }
775     }
776 
777     /**
778      * Test if the method response is gzip encoded
779      *
780      * @param method http method
781      * @return true if response is gzip encoded
782      */
783     public static boolean isGzipEncoded(HttpMethod method) {
784         Header[] contentEncodingHeaders = method.getResponseHeaders("Content-Encoding");
785         if (contentEncodingHeaders != null) {
786             for (Header header : contentEncodingHeaders) {
787                 if ("gzip".equals(header.getValue())) {
788                     return true;
789                 }
790             }
791         }
792         return false;
793     }
794 
795     /**
796      * Stop HttpConnectionManager.
797      */
798     public static void stop() {
799         synchronized (LOCK) {
800             if (httpConnectionManagerThread != null) {
801                 httpConnectionManagerThread.interrupt();
802                 httpConnectionManagerThread = null;
803             }
804             MultiThreadedHttpConnectionManager.shutdownAll();
805         }
806     }
807 
808     /**
809      * Create and set connection pool.
810      *
811      * @param httpClient httpClient instance
812      */
813     public static void createMultiThreadedHttpConnectionManager(HttpClient httpClient) {
814         MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
815         connectionManager.getParams().setDefaultMaxConnectionsPerHost(Settings.getIntProperty("davmail.exchange.maxConnections",100));
816         connectionManager.getParams().setConnectionTimeout(10000);
817         connectionManager.getParams().setSoTimeout(120000);
818         synchronized (LOCK) {
819             httpConnectionManagerThread.addConnectionManager(connectionManager);
820         }
821         httpClient.setHttpConnectionManager(connectionManager);
822     }
823 
824     /**
825      * Create and start a new HttpConnectionManager, close idle connections every minute.
826      */
827     public static void start() {
828         synchronized (LOCK) {
829             if (httpConnectionManagerThread == null) {
830                 httpConnectionManagerThread = new IdleConnectionTimeoutThread();
831                 httpConnectionManagerThread.setName(IdleConnectionTimeoutThread.class.getSimpleName());
832                 httpConnectionManagerThread.setConnectionTimeout(ONE_MINUTE);
833                 httpConnectionManagerThread.setTimeoutInterval(ONE_MINUTE);
834                 httpConnectionManagerThread.start();
835             }
836         }
837     }
838 }