package davmail.caldav; import davmail.AbstractConnection; import davmail.Settings; import davmail.exchange.ExchangeSession; import davmail.exchange.ExchangeSessionFactory; import davmail.tray.DavGatewayTray; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.auth.AuthenticationException; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import java.io.BufferedReader; import java.io.IOException; import java.io.StringReader; import java.net.Socket; import java.net.SocketException; import java.net.SocketTimeoutException; import java.text.SimpleDateFormat; import java.util.*; /** * Handle a caldav connection. */ public class CaldavConnection extends AbstractConnection { protected boolean closed = false; // Initialize the streams and start the thread public CaldavConnection(Socket clientSocket) { super("CaldavConnection", clientSocket, "UTF-8"); } protected Map parseHeaders() throws IOException { HashMap headers = new HashMap(); String line; while ((line = readClient()) != null && line.length() > 0) { int index = line.indexOf(':'); if (index <= 0) { throw new IOException("Invalid header: " + line); } headers.put(line.substring(0, index).toLowerCase(), line.substring(index + 1).trim()); } return headers; } protected String getContent(String contentLength) throws IOException { if (contentLength == null || contentLength.length() == 0) { return null; } else { int size; try { size = Integer.parseInt(contentLength); } catch (NumberFormatException e) { throw new IOException("Invalid content length: " + contentLength); } char[] buffer = new char[size]; int actualSize = in.read(buffer); if (actualSize < 0) { throw new IOException("End of stream reached reading content"); } return new String(buffer, 0, actualSize); } } protected void setSocketTimeout(String keepAliveValue) throws IOException { if (keepAliveValue != null && keepAliveValue.length() > 0) { int keepAlive; try { keepAlive = Integer.parseInt(keepAliveValue); } catch (NumberFormatException e) { throw new IOException("Invalid Keep-Alive: " + keepAliveValue); } if (keepAlive > 300) { keepAlive = 300; } client.setSoTimeout(keepAlive * 1000); DavGatewayTray.debug("Set socket timeout to " + keepAlive + " seconds"); } } public void run() { String line; StringTokenizer tokens; try { while (!closed) { line = readClient(); // unable to read line, connection closed ? if (line == null) { break; } tokens = new StringTokenizer(line); if (tokens.hasMoreTokens()) { String command = tokens.nextToken(); Map headers = parseHeaders(); if (tokens.hasMoreTokens()) { String path = tokens.nextToken(); String content = getContent(headers.get("content-length")); setSocketTimeout(headers.get("keep-alive")); if ("OPTIONS".equals(command)) { sendOptions(); } else if (!headers.containsKey("authorization")) { sendUnauthorized(); } else { decodeCredentials(headers.get("authorization")); // authenticate only once if (session == null) { // first check network connectivity ExchangeSessionFactory.checkConfig(); try { session = ExchangeSessionFactory.getInstance(userName, password); } catch (AuthenticationException e) { sendErr(HttpStatus.SC_UNAUTHORIZED, e.getMessage()); } } handleRequest(command, path, headers, content); } } else { sendErr(HttpStatus.SC_NOT_IMPLEMENTED, "Invalid URI"); } } os.flush(); } } catch (SocketTimeoutException e) { DavGatewayTray.debug("Closing connection on timeout"); } catch (SocketException e) { DavGatewayTray.debug("Connection closed"); } catch (IOException e) { DavGatewayTray.error(e); try { sendErr(HttpStatus.SC_INTERNAL_SERVER_ERROR, e); } catch (IOException e2) { DavGatewayTray.debug("Exception sending error to client", e2); } } finally { close(); } DavGatewayTray.resetIcon(); } protected int getDepth(Map headers) { int result = 0; String depthValue = headers.get("depth"); if (depthValue != null) { try { result = Integer.valueOf(depthValue); } catch (NumberFormatException e) { DavGatewayTray.warn("Invalid depth value: " + depthValue); } } return result; } public void handleRequest(String command, String path, Map headers, String body) throws IOException { int depth = getDepth(headers); // full debug trace DavGatewayTray.debug("command: " + command + " " + path + " Depth: " + depth + "\n" + body); if ("OPTIONS".equals(command)) { sendOptions(); } else if ("PROPFIND".equals(command) && ("/user/".equals(path) || "/user".equals(path)) && body != null) { CaldavRequest request = new CaldavRequest(body); StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append("\n"); buffer.append(" \n"); buffer.append(" /user\n"); buffer.append(" \n"); buffer.append(" \n"); if (request.hasProperty("calendar-home-set")) { buffer.append(" \n"); buffer.append(" /calendar\n"); buffer.append(" "); } if (request.hasProperty("calendar-user-address-set")) { buffer.append(" \n"); buffer.append(" mailto:").append(session.getEmail()).append("\n"); buffer.append(" "); } if (request.hasProperty("schedule-inbox-URL")) { buffer.append(" \n"); buffer.append(" /inbox\n"); buffer.append(" "); } if (request.hasProperty("schedule-outbox-URL")) { buffer.append(" \n"); buffer.append(" /outbox\n"); buffer.append(" "); } buffer.append(" \n"); buffer.append(" HTTP/1.1 200 OK\n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append("\n"); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } else if ("PROPFIND".equals(command) && ("/calendar/".equals(path) || "/calendar".equals(path)) && body != null) { CaldavRequest request = new CaldavRequest(body); StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append("\n"); buffer.append(" \n"); buffer.append(" /calendar\n"); buffer.append(" \n"); buffer.append(" \n"); if (request.hasProperty("resourcetype")) { buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); } if (request.hasProperty("owner")) { buffer.append(" \n"); buffer.append(" /user\n"); buffer.append(" \n"); } if (request.hasProperty("getctag")) { buffer.append(" ") .append(base64Encode(session.getCalendarEtag())) .append("\n"); } buffer.append(" \n"); buffer.append(" HTTP/1.1 200 OK\n"); buffer.append(" \n"); buffer.append(" \n"); // if depth is 1, also send events if (depth > 0) { appendEventsResponses(buffer, request, session.getAllEvents()); } buffer.append("\n"); HashMap responseHeaders = new HashMap(); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, responseHeaders, "text/xml;charset=UTF-8", buffer.toString(), true); // inbox is always empty } else if ("PROPFIND".equals(command) && ("/inbox/".equals(path) || "/inbox".equals(path)) && body != null) { CaldavRequest request = new CaldavRequest(body); StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append("\n"); buffer.append(" \n"); buffer.append(" /inbox\n"); buffer.append(" \n"); buffer.append(" \n"); if (request.hasProperty("resourcetype")) { buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); } if (request.hasProperty("getctag")) { buffer.append(" 0\n"); } buffer.append(" \n"); buffer.append(" HTTP/1.1 200 OK\n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append("\n"); HashMap responseHeaders = new HashMap(); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, responseHeaders, "text/xml;charset=UTF-8", buffer.toString(), true); } else if ("REPORT".equals(command) && ("/calendar/".equals(path) || "/calendar".equals(path)) && depth == 1 && body != null) { CaldavRequest request = new CaldavRequest(body); List events; List notFound = new ArrayList(); if (request.isMultiGet()) { events = new ArrayList(); for (String href : request.getHrefs()) { try { events.add(session.getEvent(href.substring("/calendar/".length()))); } catch (HttpException e) { notFound.add(href); } } } else { events = session.getAllEvents(); } StringBuilder buffer = new StringBuilder(); buffer.append("\n" + "\n"); appendEventsResponses(buffer, request, events); // send not found events errors for (String href : notFound) { buffer.append(" \n"); buffer.append(" ").append(href).append("\n"); buffer.append(" \n"); buffer.append(" HTTP/1.1 404 Not Found\n"); buffer.append(" \n"); buffer.append(" \n"); } buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } else if ("REPORT".equals(command) && ("/inbox/".equals(path) || "/inbox".equals(path))) { // inbox is always empty StringBuilder buffer = new StringBuilder(); buffer.append("\n" + "\n"); buffer.append(""); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, null, "text/xml;charset=UTF-8", buffer.toString(), true); } else if ("PUT".equals(command) && path.startsWith("/calendar/")) { String etag = headers.get("if-match"); int status = session.createOrUpdateEvent(path.substring("/calendar/".length()), body, etag); sendHttpResponse(status, true); } else if ("POST".equals(command) && path.startsWith("/outbox")) { Map valueMap = new HashMap(); Map keyMap = new HashMap(); BufferedReader reader = new BufferedReader(new StringReader(body)); String line; String key = null; while ((line = reader.readLine()) != null) { if (line.startsWith(" ") && "ATTENDEE".equals(key)) { valueMap.put(key, valueMap.get(key) + line.substring(1)); } else { int index = line.indexOf(':'); if (index <= 0) { throw new IOException("Invalid request: " + body); } String fullkey = line.substring(0, index); String value = line.substring(index + 1); int semicolonIndex = fullkey.indexOf(";"); if (semicolonIndex > 0) { key = fullkey.substring(0, semicolonIndex); } else { key = fullkey; } valueMap.put(key, value); keyMap.put(key, fullkey); } } String response = "\n" + " \n" + " \n" + " \n" + " " + valueMap.get("ATTENDEE") + "\n" + " \n" + " 2.0;Success\n" + " BEGIN:VCALENDAR\n" + "VERSION:2.0\n" + "PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN\n" + "METHOD:REPLY\n" + "BEGIN:VFREEBUSY\n" + "DTSTAMP:" + valueMap.get("DTSTAMP") + "\n" + "ORGANIZER:" + valueMap.get("ORGANIZER") + "\n" + "DTSTART:" + valueMap.get("DTSTART") + "\n" + "DTEND:" + valueMap.get("DTEND") + "\n" + "UID:" + valueMap.get("UID") + "\n" + keyMap.get("ATTENDEE") + ";" + valueMap.get("ATTENDEE") + "\n" + "FREEBUSY;FBTYPE=BUSY-UNAVAILABLE:" + session.getFreebusy(valueMap) + "\n" + "END:VFREEBUSY\n" + "END:VCALENDAR" + "\n" + " \n" + " "; sendHttpResponse(HttpStatus.SC_OK, null, "text/xml;charset=UTF-8", response, true); } else if ("DELETE".equals(command) && path.startsWith("/calendar/")) { int status = session.deleteEvent(path.substring("/calendar/".length())); sendHttpResponse(status, true); } else { DavGatewayTray.error("Unsupported command: " + command + " " + path + " Depth: " + depth + "\n" + body); sendErr(HttpStatus.SC_BAD_REQUEST, "Unsupported command: " + command); } } protected void appendEventsResponses(StringBuilder buffer, CaldavRequest request, List events) throws IOException { int size = events.size(); int count = 0; for (ExchangeSession.Event event : events) { DavGatewayTray.debug("Retrieving event "+(++count)+"/"+size); String eventPath = event.getPath().replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); buffer.append("\n"); buffer.append(" /calendar").append(eventPath).append("\n"); buffer.append(" \n"); buffer.append(" \n"); if (request.hasProperty("calendar-data")) { String ics = event.getICS(); if (ics != null && ics.length() > 0) { ics = ics.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); buffer.append(" "); buffer.append(ics); buffer.append("\n"); } } if (request.hasProperty("getetag")) { buffer.append(" ").append(event.getEtag()).append("\n"); } buffer.append(" \n"); buffer.append(" HTTP/1.1 200 OK\n"); buffer.append(" \n"); buffer.append(" \n"); } } public void sendErr(int status, Exception e) throws IOException { String message = e.getMessage(); if (message == null) { message = e.toString(); } sendErr(status, message); } public void sendErr(int status, String message) throws IOException { sendHttpResponse(status, null, "text/plain;charset=UTF-8", message, false); } public void sendOptions() throws IOException { HashMap headers = new HashMap(); headers.put("Allow", "OPTIONS, GET, PROPFIND, PUT, POST"); headers.put("DAV", "1, 2, 3, access-control, calendar-access, ticket, calendar-schedule"); sendHttpResponse(HttpStatus.SC_OK, headers, null, null, true); } public void sendUnauthorized() throws IOException { HashMap headers = new HashMap(); headers.put("WWW-Authenticate", "Basic realm=\"" + Settings.getProperty("davmail.url") + "\""); sendHttpResponse(HttpStatus.SC_UNAUTHORIZED, headers, null, null, true); } public void sendHttpResponse(int status, boolean keepAlive) throws IOException { sendHttpResponse(status, null, null, null, keepAlive); } public void sendHttpResponse(int status, Map headers, String contentType, String content, boolean keepAlive) throws IOException { sendClient("HTTP/1.1 " + status + " " + HttpStatus.getStatusText(status)); sendClient("Server: DavMail Gateway"); SimpleDateFormat formatter = new java.text.SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH); sendClient("Date: " + formatter.format(new java.util.Date())); if (headers != null) { for (Map.Entry header : headers.entrySet()) { sendClient(header.getKey() + ": " + header.getValue()); } } if (contentType != null) { sendClient("Content-Type: " + contentType); } sendClient("Connection: " + (keepAlive ? "keep-alive" : "close")); closed = !keepAlive; if (content != null && content.length() > 0) { sendClient("Content-Length: " + content.getBytes("UTF-8").length); } else { sendClient("Content-Length: 0"); } sendClient(""); if (content != null && content.length() > 0) { // full debug trace DavGatewayTray.debug("> "+content); sendClient(content.getBytes("UTF-8")); } } /** * Decode HTTP credentials * * @param authorization http authorization header value * @throws java.io.IOException if invalid credentials */ protected void decodeCredentials(String authorization) throws IOException { int index = authorization.indexOf(' '); if (index > 0) { String mode = authorization.substring(0, index).toLowerCase(); if (!"basic".equals(mode)) { throw new IOException("Unsupported authorization mode: " + mode); } String encodedCredentials = authorization.substring(index + 1); String decodedCredentials = base64Decode(encodedCredentials); index = decodedCredentials.indexOf(':'); if (index > 0) { userName = decodedCredentials.substring(0, index); password = decodedCredentials.substring(index + 1); } else { throw new IOException("Invalid credentials"); } } else { throw new IOException("Invalid credentials"); } } protected class CaldavRequest { protected HashSet properties = new HashSet(); protected HashSet hrefs; protected boolean isMultiGet; public CaldavRequest(String body) throws IOException { // parse body XMLStreamReader streamReader = null; try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); streamReader = inputFactory.createXMLStreamReader(new StringReader(body)); boolean inElement = false; boolean inProperties = false; String currentElement = null; while (streamReader.hasNext()) { int event = streamReader.next(); if (event == XMLStreamConstants.START_ELEMENT) { inElement = true; currentElement = streamReader.getLocalName(); if ("prop".equals(currentElement)) { inProperties = true; } else if ("calendar-multiget".equals(currentElement)) { isMultiGet = true; } else if (inProperties) { properties.add(currentElement); } } else if (event == XMLStreamConstants.END_ELEMENT) { if ("prop".equals(currentElement)) { inProperties = false; } } else if (event == XMLStreamConstants.CHARACTERS && inElement) { if ("href".equals(currentElement)) { if (hrefs == null) { hrefs = new HashSet(); } hrefs.add(streamReader.getText()); } inElement = false; } } } catch (XMLStreamException e) { throw new IOException(e.getMessage()); } finally { try { if (streamReader != null) { streamReader.close(); } } catch (XMLStreamException e) { DavGatewayTray.error(e); } } } public boolean hasProperty(String propertyName) { return properties.contains(propertyName); } public boolean isMultiGet() { return isMultiGet && hrefs != null; } public Set getHrefs() { return hrefs; } } }