// // $Id: FileUploadThreadHTTP.java 473 2008-05-20 08:52:07Z etienne_sf $ // // jupload - A file upload applet. // Copyright 2007 The JUpload Team // // Created: 2007-03-07 // Creator: etienne_sf // Last modified: $Date: 2008-05-20 01:52:07 -0700 (Tue, 20 May 2008) $ // // This program is free software; you can redistribute it and/or modify it under // the terms of the GNU General Public License as published by the Free Software // Foundation; either version 2 of the License, or (at your option) any later // version. This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more // details. You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, Inc., // 675 Mass Ave, Cambridge, MA 02139, USA. package wjhk.jupload2.upload; import java.io.BufferedOutputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Proxy; import java.net.ProxySelector; import java.net.Socket; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.HashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.SSLSocket; import javax.swing.JProgressBar; import wjhk.jupload2.exception.JUploadException; import wjhk.jupload2.exception.JUploadIOException; import wjhk.jupload2.filedata.FileData; import wjhk.jupload2.policies.UploadPolicy; import wjhk.jupload2.upload.helper.ByteArrayEncoderHTTP; import wjhk.jupload2.upload.helper.ByteArrayEncoder; /** * This class implements the file upload via HTTP POST request. * * @author etienne_sf * @version $Revision: 473 $ */ public class FileUploadThreadHTTP extends DefaultFileUploadThread { private final static int CHUNKBUF_SIZE = 4096; private final static Pattern pChunked = Pattern.compile( "^Transfer-Encoding:\\s+chunked", Pattern.CASE_INSENSITIVE); private final static Pattern pClose = Pattern.compile( "^Connection:\\s+close", Pattern.CASE_INSENSITIVE); private final static Pattern pProxyClose = Pattern.compile( "^Proxy-Connection:\\s+close", Pattern.CASE_INSENSITIVE); private final static Pattern pHttpStatus = Pattern .compile("^HTTP/\\d\\.\\d\\s+((\\d+)\\s+.*)$"); private final static Pattern pContentLen = Pattern.compile( "^Content-Length:\\s+(\\d+)$", Pattern.CASE_INSENSITIVE); private final static Pattern pContentTypeCs = Pattern.compile( "^Content-Type:\\s+.*;\\s*charset=([^;\\s]+).*$", Pattern.CASE_INSENSITIVE); private final static Pattern pSetCookie = Pattern.compile( "^Set-Cookie:\\s+(.*)$", Pattern.CASE_INSENSITIVE); private final byte chunkbuf[] = new byte[CHUNKBUF_SIZE]; private CookieJar cookies = new CookieJar(); /** * http boundary, for the posting multipart post. */ private String boundary = "-----------------------------" + getRandomString(); /** * local head within the multipart post, for each file. This is * precalculated for all files, in case the upload is not chunked. The heads * length are counted in the total upload size, to check that it is less * than the maxChunkSize. tails are calculated once, as they depend not of * the file position in the upload. */ private ByteArrayEncoder heads[] = null; /** * same as heads, for the ... tail in the multipart post, for each file. But * tails depend on the file position (the boundary is added to the last * tail). So it's to be calculated fror each upload. */ private ByteArrayEncoder tails[] = null; /** * This stream is open by {@link #startRequest(long, boolean, int, boolean)}. * It is closed by the {@link #cleanRequest()} method. * * @see #startRequest(long, boolean, int, boolean) * @see #cleanRequest() * @see #getOutputStream() */ private DataOutputStream httpDataOut = null; /** * The network socket where the bytes should be written. */ private Socket sock = null; /** * This stream allows the applet to get the server response. It is opened * and closed as the {@link #httpDataOut}. */ private InputStream httpDataIn = null; /** * This StringBuffer contains the body for the server response. That is: the * server response without the http header. This the real functionnal * response from the server application, that would be outputed, for * instance, by any 'echo' PHP command. */ private StringBuffer sbHttpResponseBody = null; /** * Creates a new instance. * * @param filesDataParam The files to upload. * @param uploadPolicy The policy to be applied. * @param progress The progress bar to be updated. */ public FileUploadThreadHTTP(FileData[] filesDataParam, UploadPolicy uploadPolicy, JProgressBar progress) { super(filesDataParam, uploadPolicy, progress); uploadPolicy.displayDebug("Upload done by using the " + getClass().getName() + " class", 40); // Name the thread (useful for debugging) setName("FileUploadThreadHTTP"); this.heads = new ByteArrayEncoder[filesDataParam.length]; this.tails = new ByteArrayEncoder[filesDataParam.length]; } /** @see DefaultFileUploadThread#beforeRequest(int, int) */ @Override void beforeRequest(int firstFileToUploadParam, int nbFilesToUploadParam) throws JUploadException { setAllHead(firstFileToUploadParam, nbFilesToUploadParam, this.boundary); setAllTail(firstFileToUploadParam, nbFilesToUploadParam, this.boundary); } /** @see DefaultFileUploadThread#getAdditionnalBytesForUpload(int) */ @Override long getAdditionnalBytesForUpload(int index) throws JUploadIOException { return this.heads[index].getEncodedLength() + this.tails[index].getEncodedLength(); } /** @see DefaultFileUploadThread#afterFile(int) */ @Override void afterFile(int index) throws JUploadIOException { try { this.httpDataOut.write(tails[index].getEncodedByteArray()); this.uploadPolicy.displayDebug("--- filetail start (len=" + tails[index].getEncodedLength() + "):", 80); this.uploadPolicy.displayDebug(quoteCRLF(tails[index].getString()), 80); this.uploadPolicy.displayDebug("--- filetail end", 80); } catch (IOException e) { throw new JUploadIOException(e); } } /** @see DefaultFileUploadThread#beforeFile(int) */ @Override void beforeFile(int index) throws JUploadException { // heads[i] contains the header specific for the file, in the multipart // content. // It is initialized at the beginning of the run() method. It can be // override at the beginning // of this loop, if in chunk mode. try { this.httpDataOut.write(this.heads[index].getEncodedByteArray()); // Debug output: always called, so that the debug file is correctly // filled. this.uploadPolicy.displayDebug("--- fileheader start (len=" + this.heads[index].getEncodedLength() + "):", 80); this.uploadPolicy.displayDebug(quoteCRLF(this.heads[index] .getString()), 80); this.uploadPolicy.displayDebug("--- fileheader end", 80); } catch (Exception e) { throw new JUploadException(e); } } /** @see DefaultFileUploadThread#cleanAll() */ @SuppressWarnings("unused") @Override void cleanAll() throws JUploadException { // Nothing to do in HTTP mode. } /** @see DefaultFileUploadThread#cleanRequest() */ @Override void cleanRequest() throws JUploadException { JUploadException localException = null; try { // Throws java.io.IOException this.httpDataOut.close(); } catch (NullPointerException e) { // httpDataOut is already null ... } catch (IOException e) { localException = new JUploadException(e); this.uploadPolicy.displayErr(this.uploadPolicy .getString("errDuringUpload"), e); } finally { this.httpDataOut = null; } try { // Throws java.io.IOException this.httpDataIn.close(); } catch (NullPointerException e) { // httpDataIn is already null ... } catch (IOException e) { if (localException != null) { localException = new JUploadException(e); this.uploadPolicy.displayErr(this.uploadPolicy .getString("errDuringUpload"), localException); } } finally { this.httpDataIn = null; } try { // Throws java.io.IOException this.sock.close(); } catch (NullPointerException e) { // sock is already null ... } catch (IOException e) { if (localException != null) { localException = new JUploadException(e); this.uploadPolicy.displayErr(this.uploadPolicy .getString("errDuringUpload"), e); } } finally { this.sock = null; } if (localException != null) { throw localException; } } @Override int finishRequest() throws JUploadException { boolean readingHttpBody = false; boolean gotClose = false; boolean gotChunked = false; boolean gotContentLength = false; int status = 0; int clen = 0; String line = ""; byte[] body = new byte[0]; String charset = "ISO-8859-1"; this.sbHttpResponseBody = new StringBuffer(); try { // If the user requested abort, we are not going to send // anymore, so shutdown the outgoing half of the socket. // This helps the server to speed up with it's response. if (this.stop && !(this.sock instanceof SSLSocket)) this.sock.shutdownOutput(); // && is evaluated from left to right so !stop must come first! while (!this.stop && ((!gotContentLength) || (clen > 0))) { if (readingHttpBody) { // Read the http body if (gotChunked) { // Read the chunk header. // This is US-ASCII! (See RFC 2616, Section 2.2) line = readLine(httpDataIn, "US-ASCII", false); if (null == line) throw new JUploadException("unexpected EOF"); // Handle a single chunk of the response // We cut off possible chunk extensions and ignore them. // The length is hex-encoded (RFC 2616, Section 3.6.1) int len = Integer.parseInt(line.replaceFirst(";.*", "") .trim(), 16); this.uploadPolicy.displayDebug("Chunk: " + line + " dec: " + len, 80); if (len == 0) { // RFC 2616, Section 3.6.1: A length of 0 denotes // the last chunk of the body. // This code wrong if the server sends chunks // with trailers! (trailers are HTTP Headers that // are send *after* the body. These are announced // in the regular HTTP header "Trailer". // Fritz: Never seen them so far ... // TODO: Implement trailer-handling. break; } // Loop over the chunk (len == length of the chunk) while (len > 0) { int rlen = (len > CHUNKBUF_SIZE) ? CHUNKBUF_SIZE : len; int ofs = 0; if (rlen > 0) { while (ofs < rlen) { int res = this.httpDataIn.read( this.chunkbuf, ofs, rlen - ofs); if (res < 0) throw new JUploadException( "unexpected EOF"); len -= res; ofs += res; } if (ofs < rlen) throw new JUploadException("short read"); if (rlen < CHUNKBUF_SIZE) body = byteAppend(body, this.chunkbuf, rlen); else body = byteAppend(body, this.chunkbuf); } } // Got the whole chunk, read the trailing CRLF. readLine(httpDataIn, false); } else { // Not chunked. Use either content-length (if available) // or read until EOF. if (gotContentLength) { // Got a Content-Length. Read exactly that amount of // bytes. while (clen > 0) { int rlen = (clen > CHUNKBUF_SIZE) ? CHUNKBUF_SIZE : clen; int ofs = 0; if (rlen > 0) { while (ofs < rlen) { int res = this.httpDataIn.read( this.chunkbuf, ofs, rlen - ofs); if (res < 0) throw new JUploadException( "unexpected EOF"); clen -= res; ofs += res; } if (ofs < rlen) throw new JUploadException("short read"); if (rlen < CHUNKBUF_SIZE) body = byteAppend(body, this.chunkbuf, rlen); else body = byteAppend(body, this.chunkbuf); } } } else { // No Content-length available, read until EOF // while (true) { byte[] lbuf = readLine(httpDataIn, true); if (null == lbuf) break; body = byteAppend(body, lbuf); } break; } } } else { // readingHttpBody is false, so we are still in headers. // Headers are US-ASCII (See RFC 2616, Section 2.2) String tmp = readLine(httpDataIn, "US-ASCII", false); if (null == tmp) throw new JUploadException("unexpected EOF"); if (status == 0) { this.uploadPolicy.displayDebug( "-------- Response Headers Start --------", 80); Matcher m = pHttpStatus.matcher(tmp); if (m.matches()) { status = Integer.parseInt(m.group(2)); setResponseMsg(m.group(1)); } else { // The status line must be the first line of the // response. (See RFC 2616, Section 6.1) so this // is an error. // We first display the wrong line. this.uploadPolicy .displayDebug("First line of response: '" + tmp + "'", 80); // Then, we throw the exception. throw new JUploadException( "HTTP response did not begin with status line."); } } // Handle folded headers (RFC 2616, Section 2.2). This is // handled after the status line, because that line may // not be folded (RFC 2616, Section 6.1). if (tmp.startsWith(" ") || tmp.startsWith("\t")) line += " " + tmp.trim(); else line = tmp; this.uploadPolicy.displayDebug(line, 80); if (pClose.matcher(line).matches()) gotClose = true; if (pProxyClose.matcher(line).matches()) gotClose = true; if (pChunked.matcher(line).matches()) gotChunked = true; Matcher m = pContentLen.matcher(line); if (m.matches()) { gotContentLength = true; clen = Integer.parseInt(m.group(1)); } m = pContentTypeCs.matcher(line); if (m.matches()) charset = m.group(1); m = pSetCookie.matcher(line); if (m.matches()) this.cookies.parseCookieHeader(m.group(1)); if (line.length() == 0) { // RFC 2616, Section 6. Body is separated by the // header with an empty line. readingHttpBody = true; this.uploadPolicy.displayDebug( "--------- Response Headers End ---------", 80); } } } // while if (gotClose) { // RFC 2868, section 8.1.2.1 cleanRequest(); } // Convert the whole body according to the charset. // The default for charset ISO-8859-1, but overridden by // the charset attribute of the Content-Type header (if any). // See RFC 2616, Sections 3.4.1 and 3.7.1. this.sbHttpResponseBody.append(new String(body, charset)); } catch (JUploadException e) { throw e; } catch (Exception e) { e.printStackTrace(); throw new JUploadException(e); } return status; } /** @see DefaultFileUploadThread#getResponseBody() */ @Override String getResponseBody() { return this.sbHttpResponseBody.toString(); } /** @see DefaultFileUploadThread#getOutputStream() */ @SuppressWarnings("unused") @Override OutputStream getOutputStream() throws JUploadException { return this.httpDataOut; } /** @see DefaultFileUploadThread#startRequest(long, boolean, int, boolean) */ @Override void startRequest(long contentLength, boolean bChunkEnabled, int chunkPart, boolean bLastChunk) throws JUploadException { ByteArrayEncoder header = new ByteArrayEncoderHTTP(uploadPolicy, boundary); try { String chunkHttpParam = "jupart=" + chunkPart + "&jufinal=" + (bLastChunk ? "1" : "0"); this.uploadPolicy.displayDebug("chunkHttpParam: " + chunkHttpParam, 40); URL url = new URL(this.uploadPolicy.getPostURL()); // Add the chunking query params to the URL if there are any if (bChunkEnabled) { if (null != url.getQuery() && !"".equals(url.getQuery())) { url = new URL(url.toExternalForm() + "&" + chunkHttpParam); } else { url = new URL(url.toExternalForm() + "?" + chunkHttpParam); } } Proxy proxy = null; proxy = ProxySelector.getDefault().select(url.toURI()).get(0); boolean useProxy = ((proxy != null) && (proxy.type() != Proxy.Type.DIRECT)); boolean useSSL = url.getProtocol().equals("https"); // Header: Request line // Let's clear it. Useful only for chunked uploads. header.append("POST "); if (useProxy && (!useSSL)) { // with a proxy we need the absolute URL, but only if not // using SSL. (with SSL, we first use the proxy CONNECT method, // and then a plain request.) header.append(url.getProtocol()).append("://").append( url.getHost()); } header.append(url.getPath()); // Append the query params. // TODO: This probably can be removed as we now // have everything in POST data. However in order to be // backwards-compatible, it stays here for now. So we now provide // *both* GET and POST params. if (null != url.getQuery() && !"".equals(url.getQuery())) header.append("?").append(url.getQuery()); header.append(" ").append(this.uploadPolicy.getServerProtocol()) .append("\r\n"); // Header: General header.append("Host: ").append(url.getHost()).append( "\r\nAccept: */*\r\n"); // We do not want gzipped or compressed responses, so we must // specify that here (RFC 2616, Section 14.3) header.append("Accept-Encoding: identity\r\n"); // Seems like the Keep-alive doesn't work properly, at least on my // local dev (Etienne). if (!this.uploadPolicy.getAllowHttpPersistent()) { header.append("Connection: close\r\n"); } else { if (!bChunkEnabled || bLastChunk || useProxy || !this.uploadPolicy.getServerProtocol().equals( "HTTP/1.1")) { // RFC 2086, section 19.7.1 header.append("Connection: close\r\n"); } else { header.append("Keep-Alive: 300\r\n"); if (useProxy) header.append("Proxy-Connection: keep-alive\r\n"); else header.append("Connection: keep-alive\r\n"); } } // Get the GET parameters from the URL and convert them to // post form params ByteArrayEncoder formParams = getFormParamsForPostRequest(url); contentLength += formParams.getEncodedLength(); header.append("Content-Type: multipart/form-data; boundary=") .append(this.boundary.substring(2)).append("\r\n"); header.append("Content-Length: ").append( String.valueOf(contentLength)).append("\r\n"); // Get specific headers for this upload. this.uploadPolicy.onAppendHeader(header); // Blank line (end of header) header.append("\r\n"); // formParams are not really part of the main header, but we add // them here anyway. We write directly into the // ByteArrayOutputStream, as we already encoded them, to get the // encoded length. We need to flush the writer first, before // directly writting to the ByteArrayOutputStream. header.append(formParams); // Only connect, if sock is null!! // ... or if we don't persist HTTP connections (patch for IIS, based // on Marc Reidy's patch) if (this.sock == null || !uploadPolicy.getAllowHttpPersistent()) { this.sock = new HttpConnect(this.uploadPolicy).Connect(url, proxy); this.httpDataOut = new DataOutputStream( new BufferedOutputStream(this.sock.getOutputStream())); this.httpDataIn = this.sock.getInputStream(); } // The header is now constructed. header.close(); // Send http request to server this.httpDataOut.write(header.getEncodedByteArray()); // Debug output: always called, so that the debug file is correctly // filled. this.uploadPolicy.displayDebug("=== main header (len=" + header.getEncodedLength() + "):\n" + quoteCRLF(header.getString()), 80); this.uploadPolicy.displayDebug("=== main header end", 80); } catch (IOException e) { throw new JUploadIOException(e); } catch (KeyManagementException e) { throw new JUploadException(e); } catch (UnrecoverableKeyException e) { throw new JUploadException(e); } catch (NoSuchAlgorithmException e) { throw new JUploadException(e); } catch (KeyStoreException e) { throw new JUploadException(e); } catch (CertificateException e) { throw new JUploadException(e); } catch (IllegalArgumentException e) { throw new JUploadException(e); } catch (URISyntaxException e) { throw new JUploadException(e); } } // //////////////////////////////////////////////////////////////////////////////////// // /////////////////////// PRIVATE METHODS // /////////////////////////////////////// // //////////////////////////////////////////////////////////////////////////////////// /** * Construction of a random string, to separate the uploaded files, in the * HTTP upload request. */ private final String getRandomString() { StringBuffer sbRan = new StringBuffer(11); String alphaNum = "1234567890abcdefghijklmnopqrstuvwxyz"; int num; for (int i = 0; i < 11; i++) { num = (int) (Math.random() * (alphaNum.length() - 1)); sbRan.append(alphaNum.charAt(num)); } return sbRan.toString(); } /** * Creates a mime multipart string snippet, representing a POST variable. * * @param bae The ByteArrayEncoder, in which the variable is to be added. * @param bound The multipart boundary to use. * @param name The name of the POST variable * @param value The value of the POST variable * @throws JUploadIOException An exception that can be thrown while trying * to encode the given parameter to the current charset. * * private final ByteArrayEncoder addPostVariable(ByteArrayEncoder bae, * String bound, String name, String value) throws JUploadIOException { * bae.append(bound).append("\r\n"); bae.append("Content-Disposition: * form-data; name=\"").append(name) .append("\"\r\n"); * bae.append("Content-Transfer-Encoding: 8bit\r\n"); * bae.append("Content-Type: text/plain; UTF-8\r\n"); // An empty line * before the actual value. bae.append("\r\n"); // And then, the value! * bae.append(value).append("\r\n"); * * return bae; } */ /** * Creates a mime multipart string snippet, representing a FORM. Extracts * all form elements of a given HTML form and assembles a StringBuffer which * contains a sequence of mime multipart messages which represent the * elements of that form. * * @param bae The ByteArrayEncoder, in which the variable is to be added. * @param bound The multipart boundary to use. * @param formname The name of the form to evaluate. * @return the bae parameter, suitable for appending to the multipart * content. * @throws JUploadIOException An exception that can be thrown while trying * to encode the given parameter to the current charset. */ /** * Returns the header for this file, within the http multipart body. * * @param index Index of the file in the array that contains all files to * upload. * @param bound The boundary that separate files in the http multipart post * body. * @param chunkPart The numero of the current chunk (from 1 to n) * @return The encoded header for this file. The {@link ByteArrayEncoder} is * closed within this method. * @throws JUploadException */ private final ByteArrayEncoder getFileHeader(int index, String bound, @SuppressWarnings("unused") int chunkPart) throws JUploadException { String filenameEncoding = this.uploadPolicy.getFilenameEncoding(); String mimetype = this.filesToUpload[index].getMimeType(); String uploadFilename = this.filesToUpload[index] .getUploadFilename(index); String uploadFilenameForPost; ByteArrayEncoder bae = new ByteArrayEncoderHTTP(uploadPolicy, bound); // We'll encode the output stream into UTF-8. String form = this.uploadPolicy.getFormdata(); if (null != form) { bae.appendFormVariables(form); } // We ask the current FileData to add itself its properties. this.filesToUpload[index].appendFileProperties(bae); // boundary. bae.append(bound).append("\r\n"); // Content-Disposition. bae.append("Content-Disposition: form-data; name=\""); // TODO make this configurable... // should get a parameter from uploadPolicy, if that param not null, then do this. uploadFilenameForPost = this.filesToUpload[index].getUploadName(index); if (null != form) { // surround the file name with the form variable if necessary uploadFilenameForPost ="file_item["+uploadFilenameForPost+"]"; } //bae.append(this.filesToUpload[index].getUploadName(index)).append( bae.append(uploadFilenameForPost).append( "\"; filename=\""); if (filenameEncoding == null) { bae.append(uploadFilename); } else { try { this.uploadPolicy.displayDebug("Encoded filename: " + URLEncoder.encode(uploadFilename, filenameEncoding), 99); bae.append(URLEncoder.encode(uploadFilename, filenameEncoding)); } catch (UnsupportedEncodingException e) { this.uploadPolicy .displayWarn(e.getClass().getName() + ": " + e.getMessage() + " (in UploadFileData.getFileHeader)"); bae.append(uploadFilename); } } bae.append("\"\r\n"); // Line 3: Content-Type. bae.append("Content-Type: ").append(mimetype).append("\r\n"); // An empty line to finish the header. bae.append("\r\n"); // The ByteArrayEncoder is now filled. bae.close(); return bae; }// getFileHeader /** * Construction of the head for each file. * * @param firstFileToUpload The index of the first file to upload, in the * {@link #filesToUpload} area. * @param nbFilesToUpload Number of file to upload, in the next HTTP upload * request. These files are taken from the {@link #filesToUpload} * area * @param bound The String boundary between the post data in the HTTP * request. * @throws JUploadException */ private final void setAllHead(int firstFileToUpload, int nbFilesToUpload, String bound) throws JUploadException { for (int i = firstFileToUpload; i < firstFileToUpload + nbFilesToUpload; i++) { this.heads[i] = getFileHeader(i, bound, -1); } } /** * Construction of the tail for each file. * * @param firstFileToUpload The index of the first file to upload, in the * {@link #filesToUpload} area. * @param nbFilesToUpload Number of file to upload, in the next HTTP upload * request. These files are taken from the {@link #filesToUpload} * area * @param bound Current boundary, to apply for these tails. */ private final void setAllTail(int firstFileToUpload, int nbFilesToUpload, String bound) throws JUploadException { for (int i = firstFileToUpload; i < firstFileToUpload + nbFilesToUpload; i++) { // We'll encode the output stream into UTF-8. ByteArrayEncoder bae = new ByteArrayEncoderHTTP(uploadPolicy, bound); bae.append("\r\n"); bae.appendFileProperty("md5sum[]", this.filesToUpload[i].getMD5()); // The last tail gets an additional "--" in order to tell the // server we have finished. if (i == firstFileToUpload + nbFilesToUpload - 1) { bae.append(bound).append("--\r\n"); } // Let's store this tail. bae.close(); this.tails[i] = bae; } } /** * Converts the parameters in GET form to post form * * @param url the <code>URL</code> containing the query parameters * @return the parameters in a string in the correct form for a POST request * @throws JUploadIOException */ private final ByteArrayEncoder getFormParamsForPostRequest(final URL url) throws JUploadIOException { // Use a string buffer // We'll encode the output stream into UTF-8. ByteArrayEncoder bae = new ByteArrayEncoderHTTP(uploadPolicy, this.boundary); // Get the query string String query = url.getQuery(); if (null != query) { // Split this into parameters HashMap<String, String> requestParameters = new HashMap<String, String>(); String[] paramPairs = query.split("&"); String[] oneParamArray; // Put the parameters correctly to the Hashmap for (String param : paramPairs) { if (param.contains("=")) { oneParamArray = param.split("="); if (oneParamArray.length > 1) { // There is a value for this parameter requestParameters.put(oneParamArray[0], oneParamArray[1]); } else { // There is no value for this parameter requestParameters.put(oneParamArray[0], ""); } } } // Now add one multipart segment for each for (String key : requestParameters.keySet()) bae.appendFileProperty(key, requestParameters.get(key)); } // Return the body content bae.close(); return bae; }// getFormParamsForPostRequest // ////////////////////////////////////////////////////////////////////////////////////// // //////////////////// Various utilities // ////////////////////////////////////////////////////////////////////////////////////// /** * Similar like BufferedInputStream#readLine() but operates on raw bytes. * Line-Ending is <b>always</b> "\r\n". * * @param charset The input charset of the stream. * @param includeCR Set to true, if the terminating CR/LF should be included * in the returned byte array. */ static String readLine(InputStream inputStream, String charset, boolean includeCR) throws IOException { byte[] line = readLine(inputStream, includeCR); return (null == line) ? null : new String(line, charset); } /** * Similar like BufferedInputStream#readLine() but operates on raw bytes. * Line-Ending is <b>always</b> "\r\n". Update done by TedA (sourceforge * account: tedaaa). Allows to manage response from web server that send LF * instead of CRLF ! Here is a part of the RFC: <I>"we recommend that * applications, when parsing such headers, recognize a single LF as a line * terminator and ignore the leading CR"</I>. * * @param includeCR Set to true, if the terminating CR/LF should be included * in the returned byte array. */ static byte[] readLine(InputStream inputStream, boolean includeCR) throws IOException { int len = 0; int buflen = 128; // average line length byte[] buf = new byte[buflen]; byte[] ret = null; int b; while (true) { b = inputStream.read(); switch (b) { case -1: if (len > 0) { ret = new byte[len]; System.arraycopy(buf, 0, ret, 0, len); return ret; } return null; case 10: if (len > 0) { if (buf[len - 1] == 13) { if (includeCR) { ret = new byte[len + 1]; if (len > 0) System.arraycopy(buf, 0, ret, 0, len); ret[len] = 10; } else { len--; ret = new byte[len]; if (len > 0) System.arraycopy(buf, 0, ret, 0, len); } } else { ret = new byte[len]; if (len > 0) System.arraycopy(buf, 0, ret, 0, len); } } else // line feed for empty line between headers and body { ret = new byte[0]; } return ret; default: buf[len++] = (byte) b; if (len >= buflen) { buflen *= 2; byte[] tmp = new byte[buflen]; System.arraycopy(buf, 0, tmp, 0, len); buf = tmp; } } } } /** * Concatenates two byte arrays. * * @param buf1 The first array * @param buf2 The second array * @return A byte array, containing buf2 appended to buf2 */ static byte[] byteAppend(byte[] buf1, byte[] buf2) { byte[] ret = new byte[buf1.length + buf2.length]; System.arraycopy(buf1, 0, ret, 0, buf1.length); System.arraycopy(buf2, 0, ret, buf1.length, buf2.length); return ret; } /** * Concatenates two byte arrays. * * @param buf1 The first array * @param buf2 The second array * @param len Number of bytes to copy from buf2 * @return A byte array, containing buf2 appended to buf2 */ static byte[] byteAppend(byte[] buf1, byte[] buf2, int len) { if (len > buf2.length) len = buf2.length; byte[] ret = new byte[buf1.length + len]; System.arraycopy(buf1, 0, ret, 0, buf1.length); System.arraycopy(buf2, 0, ret, buf1.length, len); return ret; } }