/**
* The FreeBSD Copyright
* Copyright 1994-2008 The FreeBSD Project. All rights reserved.
* Copyright (C) 2013-2017 Philip Helger philip[at]helger[dot]com
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE FREEBSD PROJECT ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD PROJECT OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* The views and conclusions contained in the software and documentation
* are those of the authors and should not be interpreted as representing
* official policies, either expressed or implied, of the FreeBSD Project.
*/
package com.helger.as2lib.util.http;
import java.io.DataInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.mail.MessagingException;
import javax.mail.internet.InternetHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.helger.as2lib.message.IBaseMessage;
import com.helger.as2lib.message.IMessage;
import com.helger.as2lib.util.CAS2Header;
import com.helger.as2lib.util.IOHelper;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.collection.ext.CommonsArrayList;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.io.stream.NonBlockingByteArrayOutputStream;
import com.helger.commons.string.StringHelper;
import com.helger.commons.system.SystemProperties;
/**
* HTTP utility methods.
*
* @author Philip Helger
*/
public final class HTTPHelper
{
/** The request method used (POST or GET) */
public static final String MA_HTTP_REQ_TYPE = "HTTP_REQUEST_TYPE";
/** The request URL used - defaults to "/" */
public static final String MA_HTTP_REQ_URL = "HTTP_REQUEST_URL";
/** The HTTP version used. E.g. "HTTP/1.1" */
public static final String MA_HTTP_REQ_VERSION = "HTTP_REQUEST_VERSION";
/* End of line MUST be \r and \n */
public static final String EOL = "\r\n";
private static final Logger s_aLogger = LoggerFactory.getLogger (HTTPHelper.class);
private static IHTTPIncomingDumper s_aHTTPIncomingDumper = null;
private static IHTTPOutgoingDumper s_aHTTPOutgoingDumper = null;
static
{
final String sHttpDumpDirectory = SystemProperties.getPropertyValueOrNull ("AS2.httpDumpDirectory");
if (StringHelper.hasText (sHttpDumpDirectory))
{
final File aDumpDirectory = new File (sHttpDumpDirectory);
IOHelper.getFileOperationManager ().createDirIfNotExisting (aDumpDirectory);
setHTTPIncomingDumper (new HTTPIncomingDumperDirectoryBased (aDumpDirectory));
}
}
private HTTPHelper ()
{}
@Nonnull
@ReturnsMutableCopy
public static ICommonsList <String> getAllHTTPHeaderLines (@Nonnull final InternetHeaders aHeaders)
{
final ICommonsList <String> ret = new CommonsArrayList <> ();
final Enumeration <?> aEnum = aHeaders.getAllHeaderLines ();
while (aEnum.hasMoreElements ())
ret.add ((String) aEnum.nextElement ());
return ret;
}
@Nonnull
@Nonempty
public static String getHTTPResponseMessage (final int nResponseCode)
{
String sMsg;
switch (nResponseCode)
{
case 100:
sMsg = "Continue";
break;
case 101:
sMsg = "Switching Protocols";
break;
case 200:
sMsg = "OK";
break;
case 201:
sMsg = "Created";
break;
case 202:
sMsg = "Accepted";
break;
case 203:
sMsg = "Non-Authoritative Information";
break;
case 204:
sMsg = "No Content";
break;
case 205:
sMsg = "Reset Content";
break;
case 206:
sMsg = "Partial Content";
break;
case 300:
sMsg = "Multiple Choices";
break;
case 301:
sMsg = "Moved Permanently";
break;
case 302:
sMsg = "Found";
break;
case 303:
sMsg = "See Other";
break;
case 304:
sMsg = "Not Modified";
break;
case 305:
sMsg = "Use Proxy";
break;
case 307:
sMsg = "Temporary Redirect";
break;
case 400:
sMsg = "Bad Request";
break;
case 401:
sMsg = "Unauthorized";
break;
case 402:
sMsg = "Payment Required";
break;
case 403:
sMsg = "Forbidden";
break;
case 404:
sMsg = "Not Found";
break;
case 405:
sMsg = "Method Not Allowed";
break;
case 406:
sMsg = "Not Acceptable";
break;
case 407:
sMsg = "Proxy Authentication Required";
break;
case 408:
sMsg = "Request Time-out";
break;
case 409:
sMsg = "Conflict";
break;
case 410:
sMsg = "Gone";
break;
case 411:
sMsg = "Length Required";
break;
case 412:
sMsg = "Precondition Failed";
break;
case 413:
sMsg = "Request Entity Too Large";
break;
case 414:
sMsg = "Request-URI Too Large";
break;
case 415:
sMsg = "Unsupported Media Type";
break;
case 416:
sMsg = "Requested range not satisfiable";
break;
case 417:
sMsg = "Expectation Failed";
break;
case 500:
sMsg = "Internal Server Error";
break;
case 501:
sMsg = "Not Implemented";
break;
case 502:
sMsg = "Bad Gateway";
break;
case 503:
sMsg = "Service Unavailable";
break;
case 504:
sMsg = "Gateway Time-out";
break;
case 505:
sMsg = "HTTP Version not supported";
break;
default:
sMsg = "Unknown (" + nResponseCode + ")";
break;
}
return sMsg;
}
@Nonnull
public static byte [] readHttpPayload (@Nonnull final InputStream aIS,
@Nonnull final IAS2HttpResponseHandler aResponseHandler,
@Nonnull final IMessage aMsg) throws IOException
{
ValueEnforcer.notNull (aIS, "InputStream");
ValueEnforcer.notNull (aResponseHandler, "ResponseHandler");
ValueEnforcer.notNull (aMsg, "Msg");
final DataInputStream aDataIS = new DataInputStream (aIS);
// Retrieve the message content
byte [] aData = null;
final String sContentLength = aMsg.getHeader (CAS2Header.HEADER_CONTENT_LENGTH);
if (sContentLength == null)
{
// No "Content-Length" header present
final String sTransferEncoding = aMsg.getHeader (CAS2Header.HEADER_TRANSFER_ENCODING);
if (sTransferEncoding != null)
{
// Remove all whitespaces in the value
if (sTransferEncoding.replaceAll ("\\s+", "").equalsIgnoreCase ("chunked"))
{
// chunked encoding
int nLength = 0;
for (;;)
{
// First get hex chunk length; followed by CRLF
int nBlocklen = 0;
for (;;)
{
int ch = aDataIS.readByte ();
if (ch == '\n')
break;
if (ch >= 'a' && ch <= 'f')
ch -= ('a' - 10);
else
if (ch >= 'A' && ch <= 'F')
ch -= ('A' - 10);
else
if (ch >= '0' && ch <= '9')
ch -= '0';
else
continue;
nBlocklen = (nBlocklen * 16) + ch;
}
// Zero length is end of chunks
if (nBlocklen == 0)
break;
// Ok, now read new chunk
final int nNewlen = nLength + nBlocklen;
final byte [] aNewData = new byte [nNewlen];
if (nLength > 0)
System.arraycopy (aData, 0, aNewData, 0, nLength);
aDataIS.readFully (aNewData, nLength, nBlocklen);
aData = aNewData;
nLength = nNewlen;
// And now the CRLF after the chunk;
while (true)
{
final int n = aDataIS.readByte ();
if (n == '\n')
break;
}
}
aMsg.setHeader (CAS2Header.HEADER_CONTENT_LENGTH, Integer.toString (nLength));
}
else
{
// No "Content-Length" and unsupported "Transfer-Encoding"
sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_LENGTH_REQUIRED);
throw new IOException ("Transfer-Encoding unimplemented: " + sTransferEncoding);
}
}
else
{
// No "Content-Length" and no "Transfer-Encoding"
sendSimpleHTTPResponse (aResponseHandler, HttpURLConnection.HTTP_LENGTH_REQUIRED);
throw new IOException ("Content-Length missing");
}
}
else
{
// "Content-Length" is present
// Receive the transmission's data
// XX if a value > 2GB comes in, this will fail!!
final int nContentSize = Integer.parseInt (sContentLength);
aData = new byte [nContentSize];
aDataIS.readFully (aData);
}
return aData;
}
/**
* Read the first line of the HTTP request InputStream and parse out HTTP
* method (e.g. "GET" or "POST"), request URL (e.g "/as2") and HTTP version
* (e.g. "HTTP/1.1")
*
* @param aIS
* Stream to read the first line from
* @return An array with 3 elements, containing method, URL and HTTP version
* @throws IOException
* In case of IO error
*/
@Nonnull
@Nonempty
private static String [] _readRequestInfo (@Nonnull final InputStream aIS) throws IOException
{
int nByteBuf = aIS.read ();
final StringBuilder aSB = new StringBuilder ();
while (nByteBuf != -1 && nByteBuf != '\r')
{
aSB.append ((char) nByteBuf);
nByteBuf = aIS.read ();
}
if (nByteBuf != -1)
{
// read in the \n following the "\r"
aIS.read ();
}
final StringTokenizer aTokens = new StringTokenizer (aSB.toString (), " ");
final int nTokenCount = aTokens.countTokens ();
if (nTokenCount >= 3)
{
// Return all tokens
final String [] aRequestParts = new String [nTokenCount];
for (int i = 0; i < nTokenCount; i++)
aRequestParts[i] = aTokens.nextToken ();
return aRequestParts;
}
if (nTokenCount == 2)
{
// Default the request URL to "/"
final String [] aRequestParts = new String [3];
aRequestParts[0] = aTokens.nextToken ();
aRequestParts[1] = "/";
aRequestParts[2] = aTokens.nextToken ();
return aRequestParts;
}
throw new IOException ("Invalid HTTP Request (" + aSB.toString () + ")");
}
/**
* @return <code>true</code> if a dumper for incoming HTTP requests is
* installed, <code>false</code> otherwise
* @since 3.0.1
*/
public static boolean isHTTPIncomingDumpEnabled ()
{
return getHTTPIncomingDumper () != null;
}
/**
* @return the dumper for incoming HTTP requests or <code>null</code> if none
* is present
* @since 3.0.1
*/
@Nullable
public static IHTTPIncomingDumper getHTTPIncomingDumper ()
{
return s_aHTTPIncomingDumper;
}
/**
* Set the dumper for incoming HTTP requests or <code>null</code> if none
* should be used
*
* @param aHttpDumper
* The dumper to be used. May be <code>null</code>.
* @since 3.0.1
*/
public static void setHTTPIncomingDumper (@Nullable final IHTTPIncomingDumper aHttpDumper)
{
s_aHTTPIncomingDumper = aHttpDumper;
if (aHttpDumper != null)
s_aLogger.info ("Using the following handler to dump incoming requests: " + aHttpDumper);
else
s_aLogger.info ("Incoming request dumping is disabled.");
}
/**
* @return <code>true</code> if a dumper for outgoing HTTP requests is
* installed, <code>false</code> otherwise
* @since 3.0.1
*/
public static boolean isHTTPOutgoingDumpEnabled ()
{
return getHTTPOutgoingDumper () != null;
}
/**
* @return the dumper for outgoing HTTP requests or <code>null</code> if none
* is present
* @since 3.0.1
*/
@Nullable
public static IHTTPOutgoingDumper getHTTPOutgoingDumper ()
{
return s_aHTTPOutgoingDumper;
}
/**
* Set the dumper for outgoing HTTP requests or <code>null</code> if none
* should be used
*
* @param aHttpDumper
* The dumper to be used. May be <code>null</code>.
* @since 3.0.1
*/
public static void setHTTPOutgoingDumper (@Nullable final IHTTPOutgoingDumper aHttpDumper)
{
s_aHTTPOutgoingDumper = aHttpDumper;
if (aHttpDumper != null)
s_aLogger.info ("Using the following handler to dump outgoing requests: " + aHttpDumper);
else
s_aLogger.info ("Outgoing request dumping is disabled.");
}
/**
* @param aHeaderLines
* Header lines.
* @param aPayload
* Received payload.
* @deprecated Use
* {@link #dumpIncomingHttpRequest(ICommonsList,byte[],IBaseMessage)}
* instead
*/
@Deprecated
public static void dumpHttpRequest (@Nonnull final ICommonsList <String> aHeaderLines,
@Nonnull final byte [] aPayload)
{
dumpIncomingHttpRequest (aHeaderLines, aPayload, (IBaseMessage) null);
}
/**
* Dump an incoming HTTP request using the globally set HTTP dumper.
*
* @param aHeaderLines
* Header lines.
* @param aPayload
* Received payload.
* @param aMsg
* The message stub. May be <code>null</code> for legacy reasons.
* @see #getHTTPIncomingDumper()
* @see #setHTTPIncomingDumper(IHTTPIncomingDumper)
*/
public static void dumpIncomingHttpRequest (@Nonnull final ICommonsList <String> aHeaderLines,
@Nonnull final byte [] aPayload,
@Nullable final IBaseMessage aMsg)
{
if (s_aHTTPIncomingDumper != null)
s_aHTTPIncomingDumper.dumpIncomingRequest (aHeaderLines, aPayload, aMsg);
}
/**
* Read headers and payload from the passed input stream provider.
*
* @param aISP
* The abstract input stream provider to use. May not be
* <code>null</code>.
* @param aResponseHandler
* The HTTP response handler to be used. May not be <code>null</code>.
* @param aMsg
* The Message to be filled. May not be <code>null</code>.
* @return The payload of the HTTP request.
* @throws IOException
* In case of error reading from the InputStream
* @throws MessagingException
* In case header line parsing fails
*/
@Nonnull
public static byte [] readHttpRequest (@Nonnull final IAS2InputStreamProvider aISP,
@Nonnull final IAS2HttpResponseHandler aResponseHandler,
@Nonnull final IMessage aMsg) throws IOException, MessagingException
{
// Get the stream to read from
final InputStream aIS = aISP.getInputStream ();
if (aIS == null)
throw new IllegalStateException ("Failed to open InputStream from " + aISP);
// Read the HTTP meta data
final String [] aRequest = _readRequestInfo (aIS);
// Request method (e.g. "POST")
aMsg.setAttribute (MA_HTTP_REQ_TYPE, aRequest[0]);
// Request URL (e.g. "/as2")
aMsg.setAttribute (MA_HTTP_REQ_URL, aRequest[1]);
// HTTP version (e.g. "HTTP/1.1")
aMsg.setAttribute (MA_HTTP_REQ_VERSION, aRequest[2]);
// Parse all HTTP headers from stream
final InternetHeaders aHeaders = new InternetHeaders (aIS);
aMsg.setHeaders (aHeaders);
// Read the message body - no Content-Transfer-Encoding handling
final byte [] aPayload = readHttpPayload (aIS, aResponseHandler, aMsg);
// Dump on demand
if (isHTTPIncomingDumpEnabled ())
dumpIncomingHttpRequest (getAllHTTPHeaderLines (aHeaders), aPayload, aMsg);
return aPayload;
// Don't close the IS here!
}
/**
* Send a simple HTTP response that only contains the HTTP status code and the
* respective descriptive text.
*
* @param aResponseHandler
* The response handler to be used.
* @param nResponseCode
* The HTTP response code to use.
* @throws IOException
* In case sending fails for whatever reason
*/
public static void sendSimpleHTTPResponse (@Nonnull final IAS2HttpResponseHandler aResponseHandler,
@Nonnegative final int nResponseCode) throws IOException
{
try (final NonBlockingByteArrayOutputStream aData = new NonBlockingByteArrayOutputStream ())
{
final String sHTTPLine = Integer.toString (nResponseCode) + " " + getHTTPResponseMessage (nResponseCode) + EOL;
aData.write (sHTTPLine.getBytes (StandardCharsets.ISO_8859_1));
final InternetHeaders aHeaders = new InternetHeaders ();
aResponseHandler.sendHttpResponse (nResponseCode, aHeaders, aData);
}
}
/**
* Copy headers from an HTTP connection to an InternetHeaders object
*
* @param aConn
* Connection - source. May not be <code>null</code>.
* @param aHeaders
* Headers - destination. May not be <code>null</code>.
*/
public static void copyHttpHeaders (@Nonnull final HttpURLConnection aConn, @Nonnull final InternetHeaders aHeaders)
{
for (final Map.Entry <String, List <String>> aConnHeader : aConn.getHeaderFields ().entrySet ())
{
final String sHeaderName = aConnHeader.getKey ();
if (sHeaderName != null)
for (final String sHeaderValue : aConnHeader.getValue ())
{
if (aHeaders.getHeader (sHeaderName) == null)
aHeaders.setHeader (sHeaderName, sHeaderValue);
else
aHeaders.addHeader (sHeaderName, sHeaderValue);
}
}
}
}