/* * ==================================================================== * Copyright (c) 2004-2012 TMate Software Ltd. All rights reserved. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at http://svnkit.com/license.html * If newer versions of this license are posted there, you may use a * newer version instead, at your option. * ==================================================================== */ package org.tmatesoft.svn.core.internal.io.dav.http; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.tmatesoft.svn.core.SVNErrorCode; import org.tmatesoft.svn.core.SVNErrorMessage; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.internal.io.dav.DAVElement; import org.tmatesoft.svn.util.Version; import org.xml.sax.helpers.DefaultHandler; /** * @version 1.3 * @author TMate Software Ltd. */ class HTTPRequest { public static final char[] CRLF = { '\r', '\n' }; private boolean myIsSecured; private boolean myIsProxied; private HTTPConnection myConnection; private String myAuthentication; private String myProxyAuthentication; private HTTPHeader myResponseHeader; private HTTPStatus myStatus; private SVNErrorMessage myErrorMessage; private DefaultHandler myResponseHandler; private OutputStream myResponseStream; private byte[] myRequestBody; private InputStream myRequestStream; private boolean myIsProxyAuthForced; private boolean myIsKeepAlive; private String myCharset; private long myTimeout; private Map<String, List<String>> myCookieHeaders; public HTTPRequest(String charset) { myCharset = charset; } public void reset() { if (myRequestStream != null) { try { myRequestStream.reset(); } catch (IOException e) { } } myAuthentication = null; myProxyAuthentication = null; myResponseHeader = null; myStatus = null; myErrorMessage = null; } public void setProxied(boolean proxied) { myIsProxied = proxied; } public void setSecured(boolean secured) { myIsSecured = secured; } public void setConnection(HTTPConnection connection) { myConnection = connection; } public void initCredentials(HTTPAuthentication authentication, String method, String path) { if (authentication != null) { authentication.setChallengeParameter("method", method); authentication.setChallengeParameter("uri", composeRequestURI(method, path)); authentication.setChallengeParameter("host", myConnection.getHost().getHost()); } } public void setAuthentication(String auth) { myAuthentication = auth; } public void setProxyAuthentication(String auth) { myProxyAuthentication = auth; } public void setForceProxyAuth(boolean force) { myIsProxyAuthForced = force; } public void setResponseHandler(DefaultHandler handler) { myResponseHandler = handler; } public void setResponseStream(OutputStream os) { myResponseStream = os; } public void setRequestBody(byte[] body) { myRequestBody = body; } public void setRequestBody(StringBuffer sb) { try { myRequestBody = sb.toString().getBytes("UTF-8"); } catch (UnsupportedEncodingException e) { myRequestBody = sb.toString().getBytes(); } } public void setRequestBody(InputStream is) { myRequestStream = is; } /** * heart of http engine. * * features: * // all this should be moved outside this method: * - authentication callback to process 401 and 403 codes, failure results in returning error message. * - another callback to process 301 and 302 codes, failure results in returning error message. * - code that process ssl exceptions and re-prompts for client certificate when allowed. * // auth error, ssl exception and "moved" errors should be processed by the caller. * * - code to send request body. * - code to parse svn error response in case return code is not ok1 and ok2. * - standard http error should be returned otherwise. * * - body may be resetable inputStream + length - IMeasurable. * // this may throw IOException that will be converted to: timeout error, can't connect error, or ssl will re-prompt. */ public void dispatch(String request, String path, HTTPHeader header, int ok1, int ok2, SVNErrorMessage context) throws IOException { long length = 0; if (myRequestBody != null) { length = myRequestBody.length; } else if (myRequestStream instanceof ByteArrayInputStream) { length = ((ByteArrayInputStream) myRequestStream).available(); } else if (header != null && header.hasHeader(HTTPHeader.CONTENT_LENGTH_HEADER)) { try { length = Long.parseLong(header.getFirstHeaderValue(HTTPHeader.CONTENT_LENGTH_HEADER)); } catch (NumberFormatException nfe) { throw new IOException(nfe.getMessage()); } } StringBuffer headerText = composeHTTPHeader(request, path, header, length, myIsKeepAlive); myConnection.sendData(headerText.toString().getBytes(myCharset)); if (myRequestBody != null && length > 0) { myConnection.sendData(myRequestBody); } else if (myRequestStream != null && length > 0) { myConnection.sendData(myRequestStream, length); } // if method is "CONNECT", then just return normal status // only if there is nothing to read. // this may throw EOFException, then and only then we retry. myConnection.readHeader(this); // store last time for the next request in case it was keep-alive one. myTimeout = computeTimeout(myStatus, getResponseHeader()); context = context == null ? SVNErrorMessage.create(SVNErrorCode.RA_DAV_REQUEST_FAILED, "{0} of ''{1}''", new Object[] {request, path}) : context; // check status. if (myStatus.getCode() == HttpURLConnection.HTTP_MOVED_PERM || myStatus.getCode() == HttpURLConnection.HTTP_MOVED_TEMP || myStatus.getCode() == HttpURLConnection.HTTP_FORBIDDEN || myStatus.getCode() == HttpURLConnection.HTTP_UNAUTHORIZED || myStatus.getCode() == HttpURLConnection.HTTP_PROXY_AUTH) { // these errors are always processed by the caller, to allow retry. myErrorMessage = createDefaultErrorMessage(myConnection.getHost(), path, myStatus, context.getMessageTemplate(), context.getRelatedObjects()); myConnection.skipData(this); return; } boolean notExpected = false; if (ok1 >= 0) { if (ok1 == 0) { ok1 = "PROPFIND".equals(request) ? 207 : 200; } if (ok2 <= 0) { ok2 = ok1; } notExpected = !(myStatus.getCode() == ok1 || myStatus.getCode() == ok2); } else if ("CONNECT".equalsIgnoreCase(request) && myStatus.getCode() != HttpURLConnection.HTTP_OK) { notExpected = true; } if (notExpected) { // unexpected response code. myErrorMessage = readError(request, path, context); } else if (myStatus.getCode() == HttpURLConnection.HTTP_NO_CONTENT) { myConnection.skipData(this); } else if (myStatus.getCode() >= 300) { SVNErrorMessage error = readError(request, path, context); myStatus.setError(error); } else if (myResponseStream != null) { myErrorMessage = myConnection.readData(this, myResponseStream); } else if (myResponseHandler != null) { myErrorMessage = myConnection.readData(this, request, path, myResponseHandler); } else { if (!"CONNECT".equalsIgnoreCase(request)) { myConnection.skipData(this); } } } private static long computeTimeout(HTTPStatus status, HTTPHeader header) { if (header == null) { return -1; } String keepAlive = header.getFirstHeaderValue("Keep-Alive"); if (keepAlive == null && status.isHTTP11()) { // HTTP/1.1 // no keep-alive header, consider as infinite timeout. return Long.MAX_VALUE; } else if (keepAlive == null) { // HTTP/1.0 // keep-alive only if there is Connection: keep-alive header. String value = header.getFirstHeaderValue(HTTPHeader.CONNECTION_HEADER); if (value != null && value.toLowerCase().indexOf("keep-alive") >= 0) { return Long.MAX_VALUE; } Collection<String> connectionHeaders = header.getHeaderValues(HTTPHeader.CONNECTION_HEADER); if (connectionHeaders != null) { for (Iterator<String> headers = connectionHeaders.iterator(); headers.hasNext();) { value = (String) headers.next(); if (value != null && value.toLowerCase().indexOf("keep-alive") >= 0) { return Long.MAX_VALUE; } } } return -1; } // HTTP/1.1 String[] fields = keepAlive.split(","); for (int i = 0; i < fields.length; i++) { int index = fields[i].indexOf('='); if (index < 0) { continue; } String name = fields[i].substring(0, index).trim(); String value = fields[i].substring(index + 1).trim(); if ("timeout".equalsIgnoreCase(name)) { try { int seconds = Integer.parseInt(value); if (seconds >= 1) { return System.currentTimeMillis() + (seconds - 1)*1000; } } catch (NumberFormatException nfe){ } // error parsing timeout value, consider no keep-alive. return -1; } } return Long.MAX_VALUE; } private SVNErrorMessage readError(String request, String path, SVNErrorMessage context) { String contextMessage = context.getMessageTemplate(); Object[] contextObjects = context.getRelatedObjects(); if (myStatus.getCode() == HttpURLConnection.HTTP_NOT_FOUND) { contextMessage = "''{0}'' path not found"; contextObjects = new Object[] {path}; } SVNErrorMessage error = createDefaultErrorMessage(myConnection.getHost(), path, myStatus, contextMessage, contextObjects); SVNErrorMessage davError = myConnection.readError(this, request, path); if (davError != null) { if (error != null) { davError.setChildErrorMessage(error); } return davError; } return error; } public HTTPHeader getResponseHeader() { return myResponseHeader; } public long getNextRequestTimeout() { return myTimeout; } public HTTPStatus getStatus() { return myStatus; } public void setStatus(HTTPStatus status) { myStatus = status; } public void setResponseHeader(HTTPHeader header) { myResponseHeader = header; } public SVNErrorMessage getErrorMessage() { return myErrorMessage; } private StringBuffer composeHTTPHeader(String request, String path, HTTPHeader header, long length, boolean keepAlive) { StringBuffer sb = new StringBuffer(); sb.append(request); sb.append(' '); sb.append(composeRequestURI(request, path)); sb.append(' '); sb.append("HTTP/1.1"); sb.append(HTTPRequest.CRLF); sb.append("Host: "); sb.append(myConnection.getHost().getHost()); // only append if URL has port indeed. int defaultPort = "https".equals(myConnection.getHost().getProtocol()) ? 443 : 80; if (myConnection.getHost().getPort() != defaultPort) { sb.append(":"); sb.append(myConnection.getHost().getPort()); } sb.append(HTTPRequest.CRLF); sb.append("User-Agent: "); sb.append(Version.getUserAgent()); sb.append(HTTPRequest.CRLF); if (keepAlive) { sb.append("Keep-Alive:"); sb.append(HTTPRequest.CRLF); sb.append("Connection: TE, Keep-Alive"); sb.append(HTTPRequest.CRLF); } sb.append("TE: trailers"); sb.append(HTTPRequest.CRLF); if (myAuthentication != null) { sb.append("Authorization: "); sb.append(myAuthentication); sb.append(HTTPRequest.CRLF); } if ((myIsProxyAuthForced || (myIsProxied && !myIsSecured)) && myProxyAuthentication != null) { sb.append("Proxy-Authorization: "); sb.append(myProxyAuthentication); sb.append(HTTPRequest.CRLF); } if (header == null || !header.hasHeader(HTTPHeader.CONTENT_LENGTH_HEADER)) { sb.append("Content-Length: "); sb.append(length); sb.append(HTTPRequest.CRLF); } sb.append("Accept-Encoding: gzip"); sb.append(HTTPRequest.CRLF); if (header == null || !header.hasHeader(HTTPHeader.CONTENT_TYPE_HEADER)) { sb.append("Content-Type: text/xml; charset=\"utf-8\""); sb.append(HTTPRequest.CRLF); } // append capabilities sb.append(HTTPHeader.DAV_HEADER + ": "); sb.append(DAVElement.DEPTH_OPTION); sb.append(HTTPRequest.CRLF); sb.append(HTTPHeader.DAV_HEADER + ": "); sb.append(DAVElement.MERGE_INFO_OPTION); sb.append(HTTPRequest.CRLF); sb.append(HTTPHeader.DAV_HEADER + ": "); sb.append(DAVElement.LOG_REVPROPS_OPTION); sb.append(HTTPRequest.CRLF); if (header != null) { sb.append(header.toString()); } if(myCookieHeaders != null){ final Set<Map.Entry<String,List<String>>> entries = myCookieHeaders.entrySet(); for(Map.Entry<String,List<String>> entry:entries){ final String headerKey = entry.getKey(); for (String cookie:entry.getValue()){ sb.append(headerKey); //Cookie or Cookie2 sb.append(": "); sb.append(cookie); sb.append(HTTPRequest.CRLF); } } } sb.append(HTTPRequest.CRLF); return sb; } private String composeRequestURI(String request, String path) { StringBuffer sb = new StringBuffer(); if (myIsProxied && !myIsSecured) { // prepend path with host name. sb.append("http://"); sb.append(myConnection.getHost().getHost()); sb.append(":"); sb.append(myConnection.getHost().getPort()); } if (path == null) { path = "/"; } if (!"CONNECT".equals(request) && (path.length() == 0 || path.charAt(0) != '/')) { path = "/" + path; } HTTPParser.getCanonicalPath(path, sb); return sb.toString(); } public static SVNErrorMessage createDefaultErrorMessage(SVNURL host, String path, HTTPStatus status, String context, Object[] contextObjects) { SVNErrorCode errorCode = SVNErrorCode.RA_DAV_REQUEST_FAILED; String message = status != null ? status.getCode() + " " + status.getReason() : ""; if (status != null && status.getCode() == HttpURLConnection.HTTP_FORBIDDEN || status.getCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { errorCode = SVNErrorCode.RA_NOT_AUTHORIZED; message = status.getCode() + " " + status.getReason(); } else if (status != null && status.getCode() == HttpURLConnection.HTTP_NOT_FOUND) { errorCode = SVNErrorCode.FS_NOT_FOUND; } else if (status != null && (status.getCode() == HttpURLConnection.HTTP_MOVED_PERM || status.getCode() == HttpURLConnection.HTTP_MOVED_TEMP)) { final String location = status.getHeader() != null ? status.getHeader().getFirstHeaderValue("Location") : null; if (location != null) { message = status.getCode() == HttpURLConnection.HTTP_MOVED_PERM ? "Repository moved permanently to ''{0}''; please relocate" : "Repository moved temporarily to ''{0}''; please relocate"; return SVNErrorMessage.create(SVNErrorCode.RA_DAV_RELOCATED, message, location); } else { message = status.getCode() == HttpURLConnection.HTTP_MOVED_PERM ? "Repository moved permanently; please relocate" : "Repository moved temporarily; please relocate"; return SVNErrorMessage.create(SVNErrorCode.RA_DAV_RELOCATED, message); } } // extend context object to include host:port (empty location). Object[] messageObjects = contextObjects == null ? new Object[1] : new Object[contextObjects.length + 1]; int index = messageObjects.length - 1; messageObjects[messageObjects.length - 1] = host; if (messageObjects.length > 1) { System.arraycopy(contextObjects, 0, messageObjects, 0, contextObjects.length); } return SVNErrorMessage.create(errorCode, context + ": " + message + " ({" + index + "})", messageObjects); } public void setKeepAlive(boolean isKeepAlive) { myIsKeepAlive = isKeepAlive; } public void setCookies(Map<String, List<String>> cookieHeader) { myCookieHeaders = cookieHeader; } }