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