Java tutorial
/* * PHEX - The pure-java Gnutella-servent. * Copyright (C) 2001 - 2008 Phex Development Group * * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * --- SVN Information --- * $Id: HttpThexDownload.java 4406 2009-03-21 16:52:19Z gregork $ */ package phex.download.handler; import org.apache.commons.httpclient.ChunkedInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import phex.download.DownloadEngine; import phex.download.HostBusyException; import phex.download.RemotelyQueuedException; import phex.download.ThexVerificationData; import phex.download.swarming.SWDownloadCandidate; import phex.download.swarming.SWDownloadCandidate.ThexStatus; import phex.download.swarming.SWDownloadFile; import phex.download.swarming.SWDownloadSet; import phex.host.UnusableHostException; import phex.http.*; import phex.net.connection.Connection; import phex.DownloadPrefs; import phex.thex.TTHashCalcUtils; import phex.util.IOUtil; import phex.util.LengthLimitedInputStream; import phex.util.bitzi.Base32; import phex.util.dime.DimeParser; import phex.util.dime.DimeRecord; import phex.xml.thex.ThexHashTree; import phex.xml.thex.ThexHashTreeCodec; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.net.MalformedURLException; import java.util.List; import java.util.Locale; public class HttpThexDownload extends AbstractHttpDownload { private static final Logger logger = LoggerFactory.getLogger(HttpThexDownload.class); private ThexVerificationData thexData; private InputStream inStream; private long replyContentLength; private boolean isDownloadSuccessful; public HttpThexDownload(DownloadEngine engine, ThexVerificationData thexData) { super(engine); if (thexData == null) { throw new NullPointerException("ThexData is null."); } this.thexData = thexData; } public void preProcess() { } public void processHandshake() throws IOException, HTTPMessageException, UnusableHostException { isDownloadSuccessful = false; Connection connection = downloadEngine.getConnection(); SWDownloadCandidate candidate = downloadEngine.getDownloadSet().downloadCandidate; OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream()); // reset to default input stream inStream = connection.getInputStream(); String requestUrl = candidate.getThexUri(); HTTPRequest request = new HTTPRequest("GET", requestUrl, true); request.addHeader(new HTTPHeader(HTTPHeaderNames.HOST, candidate.getHostAddress().getFullHostName())); request.addHeader(new HTTPHeader(GnutellaHeaderNames.X_QUEUE, "0.1")); // request a HTTP keep alive connection, needed for queuing to work. request.addHeader(new HTTPHeader(HTTPHeaderNames.CONNECTION, "Keep-Alive")); String httpRequestStr = request.buildHTTPRequestString(); logger.debug("HTTP Request to: {}\n{}", candidate.getHostAddress(), httpRequestStr); candidate.addToCandidateLog("HTTP Request:\n" + httpRequestStr); // write request... writer.write(httpRequestStr); writer.flush(); HTTPResponse response = HTTPProcessor.parseHTTPResponse(connection); if (logger.isDebugEnabled()) { logger.debug("HTTP Response from: {}\n{}", candidate.getHostAddress(), response.buildHTTPResponseString()); } if (DownloadPrefs.CandidateLogBufferSize.get().intValue() > 0) { candidate.addToCandidateLog("HTTP Response:\n" + response.buildHTTPResponseString()); } updateServerHeader(response); HTTPHeader header; header = response.getHeader(HTTPHeaderNames.TRANSFER_ENCODING); if (header != null) { if (header.getValue().equals("chunked")) { inStream = new ChunkedInputStream(connection.getInputStream()); } } replyContentLength = -1; header = response.getHeader(HTTPHeaderNames.CONTENT_LENGTH); if (header != null) { try { replyContentLength = header.longValue(); } catch (NumberFormatException exp) { //unknown } } updateKeepAliveSupport(response); int httpCode = response.getStatusCode(); if (httpCode >= 200 && httpCode < 300) {// code accepted // connection successfully finished logger.debug("HTTP Handshake successfull."); return; } // check error type else if (httpCode == 503) {// 503 -> host is busy (this can also be returned when remotely queued) header = response.getHeader(GnutellaHeaderNames.X_QUEUE); XQueueParameters xQueueParameters = null; if (header != null) { xQueueParameters = XQueueParameters.parseXQueueParameters(header.getValue()); } // check for persistent connection (gtk-gnutella uses queuing with 'Connection: close') if (xQueueParameters != null && isKeepAliveSupported) { throw new RemotelyQueuedException(xQueueParameters); } else { header = response.getHeader(HTTPHeaderNames.RETRY_AFTER); if (header != null) { int delta = HTTPRetryAfter.parseDeltaInSeconds(header); if (delta > 0) { throw new HostBusyException(delta); } } throw new HostBusyException(); } } else { throw new IOException("Unknown HTTP code: " + httpCode); } } public void processDownload() throws IOException { SWDownloadSet downloadSet = downloadEngine.getDownloadSet(); SWDownloadCandidate candidate = downloadSet.downloadCandidate; SWDownloadFile downloadFile = downloadSet.downloadFile; logger.debug("Download Engine starts download."); LengthLimitedInputStream downloadStream = null; try { // determine the length to download, we start with the MAX // which would cause a download until the stream ends. long downloadLengthLeft = Long.MAX_VALUE; // maybe we know a reply content length if (replyContentLength != -1) { downloadLengthLeft = Math.min(replyContentLength, downloadLengthLeft); } downloadStream = new LengthLimitedInputStream(inStream, downloadLengthLeft); DimeParser dimeParser = new DimeParser(downloadStream); List<DimeRecord> records = DimeParser.getAllRecords(dimeParser); if (records.size() < 2) { throw new IOException("Required dime records not found."); } DimeRecord xmlRecord = records.get(0); DimeRecord hashRecord = records.get(1); byte[] xmlData = xmlRecord.getData(); // it seems like Bearshare is appending artifacts to the xml dime record entry. // try to remove them if (candidate.getVendor().toLowerCase(Locale.US).contains("bearshare")) { int idx = xmlData.length - 1; while (xmlData[idx] != 0x3e) { idx--; } if (idx < xmlData.length - 1) { byte[] newXmlData = new byte[idx + 1]; System.arraycopy(xmlData, 0, newXmlData, 0, idx + 1); xmlData = newXmlData; } } ThexHashTree xmlTree; try { xmlTree = ThexHashTreeCodec.parseThexHashTreeXML(new ByteArrayInputStream(xmlData)); } catch (MalformedURLException exp) {// catch this exp for debugging purpose. logger.error("Failed to parse: '{}' from: {}", new String(xmlData, "UTF-8"), candidate.getVendor()); candidate.addToCandidateLog("Failed to parse: '" + new String(xmlData, "UTF-8") + '\''); logger.error(exp.toString()); throw new IOException("Parsing Thex HashTree failed."); } catch (IOException exp) { logger.error("Failed to parse: '{}' from: {}", new String(xmlData, "UTF-8"), candidate.getVendor()); candidate.addToCandidateLog("Failed to parse: '" + new String(xmlData, "UTF-8") + '\''); throw exp; } long fileSize; try { fileSize = Long.parseLong(xmlTree.getFileSize()); } catch (NumberFormatException exp) { throw new IOException("Invalid file size: " + xmlTree.getFileSize()); } if (fileSize != downloadFile.getTotalDataSize()) { throw new IOException("Invalid file size: " + fileSize + '/' + downloadFile.getTotalDataSize()); } byte[] hashData = hashRecord.getData(); // first 24 bytes is the root we can verify... if (hashData.length < 24) { throw new IOException("Invalid hash data size."); } byte[] rootHash = new byte[24]; System.arraycopy(hashData, 0, rootHash, 0, 24); String rootHashB32 = Base32.encode(rootHash); if (!candidate.getThexRoot().equals(rootHashB32) || !downloadFile.getThexVerificationData().getRootHash().equals(rootHashB32)) { throw new IOException("Root hash do not match."); } List<List<byte[]>> merkleNodes = TTHashCalcUtils.resolveMerkleNodes(hashData, fileSize); List<byte[]> lowestLevelNodes = merkleNodes.get(merkleNodes.size() - 1); thexData.setThexData(lowestLevelNodes, merkleNodes.size() - 1, fileSize); isDownloadSuccessful = true; } finally {// don't close managed file since it might be used by parallel threads. boolean isAcceptingNextSegment = isAcceptingNextRequest(); candidate.addToCandidateLog("Is accepting next segment: " + isAcceptingNextSegment); // this is for keep alive support... if (isAcceptingNextSegment && downloadStream != null) { // only need to close and consume left overs if we plan to // continue using this connection. downloadStream.close(); } else { stopDownload(); } } } /** * Performs thex download cleanup operations. In this case releasing the * running thex request state. */ public void postProcess() { // check if data is already released... if (thexData != null) { SWDownloadSet downloadSet = downloadEngine.getDownloadSet(); SWDownloadCandidate candidate = downloadSet.downloadCandidate; synchronized (thexData) { if (isDownloadSuccessful) { candidate.setThexStatus(ThexStatus.SUCCEDED); } else if (!candidate.isBusyOrQueued()) { candidate.setThexStatus(ThexStatus.FAILED); } thexData.setThexRequested(false); } thexData = null; } } public void stopDownload() { IOUtil.closeQuietly(inStream); } /** * Indicates whether the connection is kept alive and the next request can * be send. * * @return true if the next request can be send on this connection */ public boolean isAcceptingNextRequest() { return isDownloadSuccessful && isKeepAliveSupported && replyContentLength != -1; } }