Don't get stuck in an infinite loop processing the principal URL.
[acal.git] / src / com / morphoss / acal / activity / serverconfig / TestPort.java
blobe180706a7dfa94fcb2475628649078b237aaee26
1 package com.morphoss.acal.activity.serverconfig;
3 import java.net.URLEncoder;
4 import java.util.ArrayList;
5 import java.util.Iterator;
6 import java.util.List;
8 import org.apache.http.Header;
9 import org.apache.http.message.BasicHeader;
11 import android.util.Log;
13 import com.morphoss.acal.Constants;
14 import com.morphoss.acal.providers.Servers;
15 import com.morphoss.acal.service.connector.AcalRequestor;
16 import com.morphoss.acal.service.connector.SendRequestFailedException;
17 import com.morphoss.acal.xml.DavNode;
19 public class TestPort {
20 private static final String TAG = "aCal TestPort";
21 private static final String pPathRequestData = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"+
22 "<propfind xmlns=\"DAV:\">"+
23 "<prop>"+
24 "<resourcetype/>"+
25 "<current-user-principal/>"+
26 "<principal-collection-set/>"+
27 "</prop>"+
28 "</propfind>";
30 private static final Header[] pPathHeaders = new Header[] {
31 new BasicHeader("Depth","0"),
32 new BasicHeader("Content-Type","text/xml; charset=UTF-8")
35 private final AcalRequestor requestor;
36 int port;
37 boolean useSSL;
38 private String hostName;
39 private String path;
40 int connectTimeOut;
41 int socketTimeOut;
42 private Boolean isOpen;
43 private Boolean authOK;
44 private Boolean hasDAV;
45 private Boolean hasCalDAV;
47 /**
48 * Construct based on values from the AcalRequestor
49 * @param requestorIn
51 public TestPort(AcalRequestor requestorIn) {
52 this.requestor = requestorIn;
53 this.path = requestor.getPath();
54 this.hostName = requestor.getHostName();
55 this.port = requestor.getPort();
56 this.useSSL = requestor.getProtocol().equals("https");
57 connectTimeOut = 200 + (useSSL ? 300 : 0);
58 socketTimeOut = 3000;
59 isOpen = null;
60 authOK = null;
61 hasDAV = null;
62 hasCalDAV = null;
66 /**
67 * Construct based on values from the AcalRequestor, but overriding port/SSL
68 * @param requestorIn
69 * @param port
70 * @param useSSL
72 TestPort(AcalRequestor requestorIn, int port, boolean useSSL) {
73 this(requestorIn);
74 this.port = port;
75 this.useSSL = useSSL;
79 /**
80 * <p>
81 * Test whether the port is open.
82 * </p>
83 * @return
85 boolean isOpen() {
86 if ( this.isOpen == null ) {
87 requestor.setTimeOuts(connectTimeOut,socketTimeOut);
88 requestor.setPath(path);
89 requestor.setHostName(hostName);
90 requestor.setPortProtocol( port, (useSSL?1:0) );
91 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Checking port open "+requestor.protocolHostPort());
92 this.isOpen = false;
93 try {
94 requestor.doRequest("HEAD", null, null, null);
95 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Probe "+requestor.fullUrl()+" success: status " + requestor.getStatusCode());
97 // No exception, so it worked!
98 this.isOpen = true;
99 if ( requestor.getStatusCode() == 401 ) this.authOK = false;
100 checkCalendarAccess(requestor.getResponseHeaders());
102 this.socketTimeOut = 15000;
103 this.connectTimeOut = 15000;
104 requestor.setTimeOuts(connectTimeOut,socketTimeOut);
106 catch (Exception e) {
107 if ( Constants.debugCheckServerDialog )
108 Log.println(Constants.LOGD, TAG, "Probe "+requestor.fullUrl()+" failed: " + e.getMessage());
111 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Port "+(isOpen ?"":"not")+" open on "+requestor.protocolHostPort() );
112 return this.isOpen;
117 * Increases the connection timeout and attempts another probe.
118 * @return
120 boolean reProbe() {
121 connectTimeOut += 1000;
122 connectTimeOut *= 2;
123 isOpen = null;
124 return isOpen();
129 * <p>
130 * Checks whether the calendar supports CalDAV by looking through the headers for a "DAV:" header which
131 * includes "calendar-access". Appends to the successMessage we will return to the user, as well as
132 * setting the hasCalendarAccess for later update to the DB.
133 * </p>
135 * @param headers
136 * @return true if the calendar does support CalDAV.
138 private boolean checkCalendarAccess(Header[] headers) {
139 if ( headers != null ) {
140 for (Header h : headers) {
141 if (h.getName().equalsIgnoreCase("DAV")) {
142 if (h.getValue().toLowerCase().contains("calendar-access")) {
143 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG,
144 "Discovered server supports CalDAV on URL "+requestor.fullUrl());
145 hasCalDAV = true;
146 hasDAV = true; // by implication
147 return true;
152 return false;
157 * Does a PROPFIND request on the given path.
158 * @param requestPath
159 * @return
161 private boolean doPropfindPrincipal( String requestPath ) {
162 if ( requestPath != null ) requestor.setPath(requestPath);
163 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG,
164 "Doing PROPFIND for current-user-principal on " + requestor.fullUrl() );
165 try {
166 DavNode root = requestor.doXmlRequest("PROPFIND", null, pPathHeaders, pPathRequestData);
168 int status = requestor.getStatusCode();
169 if ( Constants.debugCheckServerDialog )
170 Log.println(Constants.LOGD,TAG, "PROPFIND request " + status + " on " + requestor.fullUrl() );
172 checkCalendarAccess(requestor.getResponseHeaders());
174 if ( status == 207 ) {
175 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Checking for principal path in response...");
176 List<DavNode> unAuthenticated = root.getNodesFromPath("multistatus/response/propstat/prop/current-user-principal/unauthenticated");
177 if ( ! unAuthenticated.isEmpty() ) {
178 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Found unauthenticated principal");
179 requestor.setAuthRequired();
180 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "We are unauthenticated, so try forcing authentication on");
181 if ( requestor.getAuthType() == Servers.AUTH_NONE ) {
182 requestor.setAuthType(Servers.AUTH_BASIC);
183 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Guessing Basic Authentication");
185 else if ( requestor.getAuthType() == Servers.AUTH_BASIC ) {
186 requestor.setAuthType(Servers.AUTH_DIGEST);
187 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Guessing Digest Authentication");
189 return doPropfindPrincipal(requestPath);
192 String principalCollectionHref = null;
193 for ( DavNode response : root.getNodesFromPath("multistatus/response") ) {
194 String responseHref = response.getFirstNodeText("href");
195 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "Checking response for "+responseHref);
196 for ( DavNode propStat : response.getNodesFromPath("propstat") ) {
197 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "Checking in propstat for "+responseHref);
198 if ( propStat.getFirstNodeText("status").equalsIgnoreCase("HTTP/1.1 200 OK") ) {
199 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "Found propstat 200 OK response for "+responseHref);
200 for ( DavNode prop : propStat.getNodesFromPath("prop/*") ) {
201 String thisTag = prop.getTagName();
202 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "Examining tag "+thisTag);
203 if ( thisTag.equals("resourcetype") && ! prop.getNodesFromPath("principal").isEmpty() ) {
204 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "This is a principal URL :-)");
205 requestor.interpretUriString(responseHref);
206 setFieldsFromRequestor();
207 return true;
209 else if ( thisTag.equals("current-user-principal") ) {
210 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG, "Found the principal URL :-)");
211 requestor.interpretUriString(prop.getFirstNodeText("href"));
212 setFieldsFromRequestor();
213 return true;
215 else if ( thisTag.equals("principal-collection-set") ) {
216 principalCollectionHref = prop.getFirstNodeText("href");
217 String userName = URLEncoder.encode(requestor.getUserName(), "UTF-8");
218 if ( principalCollectionHref.length() > 0 && userName != null ) {
219 principalCollectionHref = principalCollectionHref +
220 (principalCollectionHref.length() > 0 && principalCollectionHref.charAt(principalCollectionHref.length()-1) == '/' ? "" : "/") +
221 userName + "/";
222 if ( !principalCollectionHref.equals(requestPath) ) {
223 return doPropfindPrincipal(principalCollectionHref);
225 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD, TAG,
226 "We've tried this URL already. Let's move on... "+requestPath);
228 // @todo Next: Try a Depth: 1 propfind on the principalCollectionHref trying to match it to this user
235 if ( principalCollectionHref != null ) {
239 if ( status < 300 ) authOK = true;
241 catch (Exception e) {
242 Log.e(TAG, "PROPFIND Error: " + e.getMessage());
243 Log.println(Constants.LOGD,TAG, Log.getStackTraceString(e));
245 return false;
249 private void setFieldsFromRequestor() {
250 useSSL = requestor.getProtocol().equals("https");
251 hostName = requestor.getHostName();
252 path = requestor.getPath();
253 port = requestor.getPort();
258 * Probes for whether the server has DAV support. It seems odd to use the PROPFIND
259 * for this, rather than OPTIONS which was intended for the purpose, but every working
260 * DAV server will support PROPFIND on every URL which supports DAV, whereas OPTIONS
261 * may only be available on some specific URLs in weird cases.
263 boolean hasDAV() {
264 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "Starting DAV discovery on "+requestor.fullUrl());
265 if ( !isOpen() ) return false;
266 if ( hasDAV == null ) {
267 hasDAV = false;
268 if ( doPropfindPrincipal(this.path) ) hasDAV = true;
269 else if ( !hasDAV && doPropfindPrincipal("/.well-known/caldav") ) hasDAV = true;
270 else if ( !hasDAV && doPropfindPrincipal("/") ) hasDAV = true;
272 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGD,TAG, "DAV "+(hasDAV?"":"not")+" found on "+requestor.fullUrl());
273 return hasDAV;
278 * Probes for CalDAV support on the server using previous path used for DAV.
280 boolean hasCalDAV() {
281 requestor.setTimeOuts(connectTimeOut,socketTimeOut);
282 requestor.setPath(path);
283 requestor.setHostName(hostName);
284 requestor.setPortProtocol( port, (useSSL?1:0) );
286 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG,
287 "Starting CalDAV dependency discovery on "+requestor.fullUrl());
288 if ( !isOpen() || !hasDAV() || !authOK() ) return false;
290 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG, "All CalDAV dependencies are present.");
291 if ( hasCalDAV == null ) {
292 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG, "Still discovering actual CalDAV support.");
293 hasCalDAV = false;
294 try {
295 path = requestor.getPath();
296 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG, "Starting OPTIONS on "+path);
297 requestor.doRequest("OPTIONS", path, null, null);
298 int status = requestor.getStatusCode();
299 if ( Constants.debugCheckServerDialog )
300 Log.println(Constants.LOGD,TAG, "OPTIONS request " + status + " on " + requestor.fullUrl() );
301 checkCalendarAccess(requestor.getResponseHeaders()); // Updates 'hasCalDAV' if it finds it
303 catch (SendRequestFailedException e) {
304 Log.println(Constants.LOGD,TAG, "OPTIONS Error connecting to server: " + e.getMessage());
306 catch (Exception e) {
307 Log.e(TAG,"OPTIONS Error: " + e.getMessage());
308 if ( Constants.debugCheckServerDialog )
309 Log.println(Constants.LOGD,TAG,Log.getStackTraceString(e));
312 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG,
313 "CalDAV "+(hasCalDAV?"":"not")+" found on "+requestor.fullUrl());
314 return hasCalDAV;
319 * Return whether the auth was OK. If nothing's managed to tell us it failed
320 * then we give it the benefit of the doubt.
321 * @return
323 public boolean authOK() {
324 if ( Constants.debugCheckServerDialog ) Log.println(Constants.LOGI,TAG,
325 "Checking authOK which was: "+(authOK == null ? "uncertain, assumed OK" : (authOK ? "OK" : "bad")));
326 return (authOK == null || authOK ? true : false);
330 * Returns a default ArrayList<TestPort> which can be used for probing a server to try
331 * and discover where the CalDAV / CardDAV server is hiding.
332 * @param requestor The requestor which will be used for probing.
333 * @return The ArrayList of default ports.
335 private static ArrayList<TestPort> testPortSet = null;
336 public static Iterator<TestPort> defaultIterator(AcalRequestor requestor) {
337 if ( testPortSet == null )
338 testPortSet = new ArrayList<TestPort>(10);
339 else
340 testPortSet.clear();
342 testPortSet.add( new TestPort(requestor,443,true) );
343 testPortSet.add( new TestPort(requestor,8443,true) );
344 testPortSet.add( new TestPort(requestor,80,false) );
345 testPortSet.add( new TestPort(requestor,8008,false) );
346 testPortSet.add( new TestPort(requestor,8843,true) );
347 testPortSet.add( new TestPort(requestor,4443,true) );
348 testPortSet.add( new TestPort(requestor,8043,true) );
349 testPortSet.add( new TestPort(requestor,8800,false) );
350 testPortSet.add( new TestPort(requestor,8888,false) );
351 testPortSet.add( new TestPort(requestor,7777,false) );
353 return testPortSet.iterator();
358 * Return a URL Prefix like 'https://'
359 * @return
361 public String getProtocolUrlPrefix() {
362 return "http" + (useSSL?"s":"") + "://";