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.util.URIUtil; 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.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"); } String result = new String(buffer, 0, actualSize); DavGatewayTray.debug("< " + result); return result; } } 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) { session = ExchangeSessionFactory.getInstance(userName, password); } 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 (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); if ("OPTIONS".equals(command)) { sendOptions(); } else if ("PROPFIND".equals(command) && "/user/".equals(path)) { StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append("\n"); buffer.append(" \n"); buffer.append(" /user\n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append(" /calendar\n"); buffer.append(" "); buffer.append(" \n"); buffer.append(" mailto:").append(session.getEmail()).append("\n"); buffer.append(" "); buffer.append(" \n"); buffer.append(" /inbox\n"); buffer.append(" "); 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)) { if (depth != 0 || body == null) { throw new IOException("Unsupported operation: " + command + " " + path); } try { StringBuilder buffer = new StringBuilder(); buffer.append("\n"); buffer.append("\n"); buffer.append(" \n"); buffer.append(" /calendar\n"); buffer.append(" \n"); buffer.append(" \n"); // TODO : parse request if (body.indexOf("resourcetype") >= 0) { buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); buffer.append(" \n"); } if (body.indexOf("owner") >= 0) { buffer.append(" \n"); buffer.append(" /user\n"); buffer.append(" \n"); } if (body.indexOf("getctag") >= 0) { 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"); buffer.append("\n"); HashMap responseHeaders = new HashMap(); sendHttpResponse(HttpStatus.SC_MULTI_STATUS, responseHeaders, "text/xml;charset=UTF-8", buffer.toString(), true); } catch (IOException e) { sendUnauthorized(); } } else if ("REPORT".equals(command)) { if (!"/calendar/".equals(path) || depth != 1 || body == null) { throw new IOException("Unsupported operation: " + command + " " + path); } HashSet properties = new HashSet(); // TODO : parse body if (body.indexOf("D:getetag") >= 0) { properties.add("getetag"); } if (body.indexOf("calendar-data") >= 0) { properties.add("calendar-data"); } List events; List notFound = new ArrayList(); if (body.indexOf("calendar-multiget") >= 0) { events = new ArrayList(); try { XMLInputFactory inputFactory = XMLInputFactory.newInstance(); inputFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE); inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, Boolean.TRUE); XMLStreamReader reader = inputFactory.createXMLStreamReader(new StringReader(body)); boolean inHref = false; while (reader.hasNext()) { int event = reader.next(); if (event == XMLStreamConstants.START_ELEMENT && "href".equals(reader.getLocalName())) { inHref = true; } else if (event == XMLStreamConstants.CHARACTERS && inHref) { try { events.add(session.getEvent(URIUtil.decode(reader.getText().substring("/calendar/".length())))); } catch (HttpException e) { notFound.add(reader.getText().substring("/calendar/".length())); } inHref = false; } } } catch (XMLStreamException e) { throw new IOException(e.getMessage()); } } else { events = session.getAllEvents(); } StringBuilder buffer = new StringBuilder(); buffer.append("\n" + "\n"); for (ExchangeSession.Event event : events) { String eventPath = event.getPath().replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">"); buffer.append("\n"); buffer.append(" /calendar").append(eventPath).append("\n"); buffer.append(" \n"); buffer.append(" \n"); if (properties.contains("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 (properties.contains("getetag")) { buffer.append(" ").append(event.getEtag()).append("\n"); } buffer.append(" \n"); buffer.append(" HTTP/1.1 200 OK\n"); buffer.append(" \n"); if (notFound.size() > 0) { buffer.append(" \n"); for (String href : notFound) { buffer.append(" ").append(href).append("\n"); } buffer.append(" HTTP/1.1 404 Not Found\n"); buffer.append(" \n"); } buffer.append(" ").append((char) 13).append((char) 10); } 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 { sendErr(HttpStatus.SC_BAD_REQUEST, "Unsupported command: " + command); } } 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 (String header : headers.keySet()) { sendClient(header + ": " + headers.get(header)); } } 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) { 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"); } } }