diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java new file mode 100644 index 00000000..ba670646 --- /dev/null +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/HTTPUtil.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.DateUtil; +import com.twelvemonkeys.lang.StringUtil; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +/** + * HTTPUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: HTTPUtil.java,v 1.0 08.09.13 13:57 haraldk Exp$ + */ +public class HTTPUtil { + /** + * RFC 1123 date format, as recommended by RFC 2616 (HTTP/1.1), sec 3.3 + * NOTE: All date formats are private, to ensure synchronized access. + */ + private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + static { + HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + /** + * RFC 850 date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3 + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); + /** + * ANSI C asctime() date format, (almost) as described in RFC 2616 (HTTP/1.1), sec 3.3. + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US); + + private static long sNext50YearWindowChange = DateUtil.currentTimeDay(); + static { + HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3: + // - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date + // which appears to be more than 50 years in the future is in fact + // in the past (this helps solve the "year 2000" problem). + update50YearWindowIfNeeded(); + } + + private static void update50YearWindowIfNeeded() { + // Avoid class synchronization + long next = sNext50YearWindowChange; + + if (next < System.currentTimeMillis()) { + // Next check in one day + next += DateUtil.DAY; + sNext50YearWindowChange = next; + + Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR)); + //System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate); + synchronized (HTTP_RFC850_FORMAT) { + HTTP_RFC850_FORMAT.set2DigitYearStart(startDate); + } + synchronized (HTTP_ASCTIME_FORMAT) { + HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate); + } + } + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(long pTime) { + return formatHTTPDate(new Date(pTime)); + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(Date pTime) { + synchronized (HTTP_RFC1123_FORMAT) { + return HTTP_RFC1123_FORMAT.format(pTime); + } + } + + /** + * Parses a HTTP date string into a {@code long} representing milliseconds + * since January 1, 1970 GMT. + *

+ * Use this method with headers that contain dates, such as + * {@code If-Modified-Since} or {@code Last-Modified}. + *

+ * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() + * format, as described in + * RFC 2616 (HTTP/1.1), sec. 3.3 + * + * @param pDate the date to parse + * + * @return a {@code long} value representing the date, expressed as the + * number of milliseconds since January 1, 1970 GMT, + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + public static long parseHTTPDate(String pDate) throws NumberFormatException { + return parseHTTPDateImpl(pDate).getTime(); + } + + /** + * ParseHTTPDate implementation + * + * @param pDate the date string to parse + * + * @return a {@code Date} + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException { + if (pDate == null) { + throw new IllegalArgumentException("date == null"); + } + + if (StringUtil.isEmpty(pDate)) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + DateFormat format; + + if (pDate.indexOf('-') >= 0) { + format = HTTP_RFC850_FORMAT; + update50YearWindowIfNeeded(); + } + else if (pDate.indexOf(',') < 0) { + format = HTTP_ASCTIME_FORMAT; + update50YearWindowIfNeeded(); + } + else { + format = HTTP_RFC1123_FORMAT; + // NOTE: RFC1123 always uses 4-digit years + } + + Date date; + try { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (format) { + date = format.parse(pDate); + } + } + catch (ParseException e) { + NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + nfe.initCause(e); + throw nfe; + } + + if (date == null) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + return date; + } +} diff --git a/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java b/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java new file mode 100644 index 00000000..9bb83f53 --- /dev/null +++ b/common/common-io/src/test/java/com/twelvemonkeys/net/HTTPUtilTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2013, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * HTTPUtilTest + * + * @author Harald Kuhr + * @author last modified by $Author: haraldk$ + * @version $Id: HTTPUtilTest.java,v 1.0 08.09.13 13:57 haraldk Exp$ + */ +public class HTTPUtilTest { + @Test + public void testParseHTTPDateRFC1123() { + long time = HTTPUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + @Test + public void testParseHTTPDateRFC850() { + long time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + // NOTE: This test will fail some time, around 2044, + // as the 50 year window will slide... + time = HTTPUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + @Test + public void testParseHTTPDateAsctime() { + long time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994"); + assertEquals(784111777000l, time); + + time = HTTPUtil.parseHTTPDate("Sun Nov 6 08:49:37 94"); + assertEquals(784111777000l, time); + } + + @Test + public void testFormatHTTPDateRFC1123() { + long time = 784111777000l; + assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", HTTPUtil.formatHTTPDate(time)); + } +} diff --git a/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java b/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java deleted file mode 100755 index f4875444..00000000 --- a/common/common-io/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.twelvemonkeys.net; - -import junit.framework.TestCase; - -/** - * NetUtilTestCase - *

- * - * - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java#1 $ - */ -public class NetUtilTestCase extends TestCase { - public void setUp() throws Exception { - super.setUp(); - } - - public void tearDown() throws Exception { - super.tearDown(); - } - - public void testParseHTTPDateRFC1123() { - long time = NetUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - } - - public void testParseHTTPDateRFC850() { - long time = NetUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - - // NOTE: This test will fail some time, around 2044, - // as the 50 year window will slide... - time = NetUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); - assertEquals(784111777000l, time); - } - - public void testParseHTTPDateAsctime() { - long time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994"); - assertEquals(784111777000l, time); - - time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 94"); - assertEquals(784111777000l, time); - } - - public void testFormatHTTPDateRFC1123() { - long time = 784111777000l; - assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", NetUtil.formatHTTPDate(time)); - } -} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java index 6a3f993f..21171d22 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java @@ -1,46 +1,45 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 COPYRIGHT OWNER 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. - */ - -package com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface for filtering Authenticator requests, used by the - * SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr - * @version 1.0 - */ -public interface AuthenticatorFilter { - public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); - -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface for filtering Authenticator requests, used by the + * SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * @version 1.0 + */ +public interface AuthenticatorFilter { + public boolean accept(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java index 0ebbd67d..4d4346f8 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/BASE64.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/BASE64.java @@ -1,144 +1,143 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 COPYRIGHT OWNER 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.io.*; -import com.twelvemonkeys.io.enc.Base64Decoder; -import com.twelvemonkeys.io.enc.DecoderStream; - -import java.io.*; - - -/** - * This class does BASE64 encoding (and decoding). - * - * @author unascribed - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ - * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead - */ -class BASE64 { - - /** - * This array maps the characters to their 6 bit values - */ - private final static char[] PEM_ARRAY = { - //0 1 2 3 4 5 6 7 - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 - 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 - '4', '5', '6', '7', '8', '9', '+', '/' // 7 - }; - - /** - * Encodes the input data using the standard base64 encoding scheme. - * - * @param pData the bytes to encode to base64 - * @return a string with base64 encoded data - */ - public static String encode(byte[] pData) { - int offset = 0; - int len; - StringBuilder buf = new StringBuilder(); - - while ((pData.length - offset) > 0) { - byte a, b, c; - if ((pData.length - offset) > 2) { - len = 3; - } - else { - len = pData.length - offset; - } - - switch (len) { - case 1: - a = pData[offset]; - b = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append('='); - buf.append('='); - offset++; - break; - case 2: - a = pData[offset]; - b = pData[offset + 1]; - c = 0; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append('='); - offset += offset + 2; // ??? - break; - default: - a = pData[offset]; - b = pData[offset + 1]; - c = pData[offset + 2]; - buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); - buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - buf.append(PEM_ARRAY[c & 0x3F]); - offset = offset + 3; - break; - } - - } - return buf.toString(); - } - - public static byte[] decode(String pData) throws IOException { - InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); - FileUtil.copy(in, bytes); - - return bytes.toByteArray(); - } - - //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); - - public static void main(String[] pArgs) throws IOException { - if (pArgs.length == 1) { - System.out.println(encode(pArgs[0].getBytes())); - } - else - if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) - { - System.out.println(new String(decode(pArgs[1]))); - } - else { - System.err.println("BASE64 [ -d | --decode ] arg"); - System.err.println("Encodes or decodes a given string"); - System.exit(5); - } - } +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.*; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import java.io.*; + + +/** + * This class does BASE64 encoding (and decoding). + * + * @author unascribed + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ + * @deprecated Use {@link com.twelvemonkeys.io.enc.Base64Encoder}/{@link Base64Decoder} instead + */ +class BASE64 { + /** + * This array maps the characters to their 6 bit values + */ + private final static char[] PEM_ARRAY = { + //0 1 2 3 4 5 6 7 + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + + /** + * Encodes the input data using the standard base64 encoding scheme. + * + * @param pData the bytes to encode to base64 + * @return a string with base64 encoded data + */ + public static String encode(byte[] pData) { + int offset = 0; + int len; + StringBuilder buf = new StringBuilder(); + + while ((pData.length - offset) > 0) { + byte a, b, c; + if ((pData.length - offset) > 2) { + len = 3; + } + else { + len = pData.length - offset; + } + + switch (len) { + case 1: + a = pData[offset]; + b = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append('='); + buf.append('='); + offset++; + break; + case 2: + a = pData[offset]; + b = pData[offset + 1]; + c = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append('='); + offset += offset + 2; // ??? + break; + default: + a = pData[offset]; + b = pData[offset + 1]; + c = pData[offset + 2]; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append(PEM_ARRAY[c & 0x3F]); + offset = offset + 3; + break; + } + + } + return buf.toString(); + } + + public static byte[] decode(String pData) throws IOException { + InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); + FileUtil.copy(in, bytes); + + return bytes.toByteArray(); + } + + //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); + + public static void main(String[] pArgs) throws IOException { + if (pArgs.length == 1) { + System.out.println(encode(pArgs[0].getBytes())); + } + else + if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) + { + System.out.println(new String(decode(pArgs[1]))); + } + else { + System.err.println("BASE64 [ -d | --decode ] arg"); + System.err.println("Encodes or decodes a given string"); + System.exit(5); + } + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java index bd337235..62119c2b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java @@ -1,1102 +1,1101 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 COPYRIGHT OWNER 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.StringUtil; - -import java.io.*; -import java.net.*; -import java.util.*; - -/** - * A URLConnection with support for HTTP-specific features. See - * the spec for details. - * This version also supports read and connect timeouts, making it more useful - * for clients with limitted time. - *

- * Note that the timeouts are created on the socket level, and that - *

- * Note: This class should now work as expected, but it need more testing before - * it can enter production release. - *
- * --.k - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ - * @todo Write JUnit TestCase - * @todo ConnectionMananger! - * @see RFC 2616 - */ -public class HttpURLConnection extends java.net.HttpURLConnection { - - /** - * HTTP Status-Code 307: Temporary Redirect - */ - public final static int HTTP_REDIRECT = 307; - private final static int HTTP_DEFAULT_PORT = 80; - private final static String HTTP_HEADER_END = "\r\n\r\n"; - private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; - private final static int BUF_SIZE = 8192; - private int maxRedirects = (System.getProperty("http.maxRedirects") != null) - ? Integer.parseInt(System.getProperty("http.maxRedirects")) - : 20; - protected int timeout = -1; - protected int connectTimeout = -1; - private Socket socket = null; - protected InputStream errorStream = null; - protected InputStream inputStream = null; - protected OutputStream outputStream = null; - private String[] responseHeaders = null; - protected Properties responseHeaderFields = null; - protected Properties requestProperties = new Properties(); - - /** - * Creates a HttpURLConnection. - * - * @param pURL the URL to connect to. - */ - protected HttpURLConnection(URL pURL) { - this(pURL, 0, 0); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * and connect operations. - */ - protected HttpURLConnection(URL pURL, int pTimeout) { - this(pURL, pTimeout, pTimeout); - } - - /** - * Creates a HttpURLConnection with a given read and connect timeout. - * A timeout value of zero is interpreted as an - * infinite timeout. - * - * @param pURL the URL to connect to. - * @param pTimeout the maximum time the socket will block for read - * operations. - * @param pConnectTimeout the maximum time the socket will block for - * connection. - */ - protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { - super(pURL); - setTimeout(pTimeout); - connectTimeout = pConnectTimeout; - } - - /** - * Sets the general request property. If a property with the key already - * exists, overwrite its value with the new value. - *

- *

NOTE: HTTP requires all request properties which can - * legally have multiple instances with the same key - * to use a comma-seperated list syntax which enables multiple - * properties to be appended into a single property. - * - * @param pKey the keyword by which the request is known - * (e.g., "{@code accept}"). - * @param pValue the value associated with it. - * @see #getRequestProperty(java.lang.String) - */ - public void setRequestProperty(String pKey, String pValue) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - String oldValue = requestProperties.getProperty(pKey); - - if (oldValue == null) { - requestProperties.setProperty(pKey, pValue); - } - else { - requestProperties.setProperty(pKey, oldValue + ", " + pValue); - } - } - - /** - * Returns the value of the named general request property for this - * connection. - * - * @param pKey the keyword by which the request is known (e.g., "accept"). - * @return the value of the named general request property for this - * connection. - * @see #setRequestProperty(java.lang.String, java.lang.String) - */ - public String getRequestProperty(String pKey) { - if (connected) { - throw new IllegalAccessError("Already connected"); - } - return requestProperties.getProperty(pKey); - } - - /** - * Gets HTTP response status from responses like: - *

-     * HTTP/1.0 200 OK
-     * HTTP/1.0 401 Unauthorized
-     * 
- * Extracts the ints 200 and 401 respectively. - * Returns -1 if none can be discerned - * from the response (i.e., the response is not valid HTTP). - *

- * - * - * @return the HTTP Status-Code - * @throws IOException if an error occurred connecting to the server. - */ - public int getResponseCode() throws IOException { - if (responseCode != -1) { - return responseCode; - } - - // Make sure we've gotten the headers - getInputStream(); - String resp = getHeaderField(0); - - // should have no leading/trailing LWS - // expedite the typical case by assuming it has the - // form "HTTP/1.x 2XX " - int ind; - - try { - ind = resp.indexOf(' '); - while (resp.charAt(ind) == ' ') { - ind++; - } - responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); - responseMessage = resp.substring(ind + 4).trim(); - return responseCode; - } - catch (Exception e) { - return responseCode; - } - } - - /** - * Returns the name of the specified header field. - * - * @param pName the name of a header field. - * @return the value of the named header field, or {@code null} - * if there is no such field in the header. - */ - public String getHeaderField(String pName) { - return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); - } - - /** - * Returns the value for the {@code n}th header field. - * It returns {@code null} if there are fewer than - * {@code n} fields. - *

- * This method can be used in conjunction with the - * {@code getHeaderFieldKey} method to iterate through all - * the headers in the message. - * - * @param pIndex an index. - * @return the value of the {@code n}th header field. - * @see java.net.URLConnection#getHeaderFieldKey(int) - */ - public String getHeaderField(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). - if ((pIndex == 0) || (field == null)) { - return field; - } - int idx = field.indexOf(':'); - - return ((idx > 0) - ? field.substring(idx).trim() - : ""); // TODO: "" or null? - } - - /** - * Returns the key for the {@code n}th header field. - * - * @param pIndex an index. - * @return the key for the {@code n}th header field, - * or {@code null} if there are fewer than {@code n} - * fields. - */ - public String getHeaderFieldKey(int pIndex) { - // TODO: getInputStream() first, to make sure we have header fields - if (pIndex >= responseHeaders.length) { - return null; - } - String field = responseHeaders[pIndex]; - - if (StringUtil.isEmpty(field)) { - return null; - } - int idx = field.indexOf(':'); - - return StringUtil.toLowerCase(((idx > 0) - ? field.substring(0, idx) - : field)); - } - - /** - * Sets the read timeout for the undelying socket. - * A timeout of zero is interpreted as an - * infinite timeout. - * - * @param pTimeout the maximum time the socket will block for read - * operations, in milliseconds. - */ - public void setTimeout(int pTimeout) { - if (pTimeout < 0) { // Must be positive - throw new IllegalArgumentException("Timeout must be positive."); - } - timeout = pTimeout; - if (socket != null) { - try { - socket.setSoTimeout(pTimeout); - } - catch (SocketException se) { - // Not much to do about that... - } - } - } - - /** - * Gets the read timeout for the undelying socket. - * - * @return the maximum time the socket will block for read operations, in - * milliseconds. - * The default value is zero, which is interpreted as an - * infinite timeout. - */ - public int getTimeout() { - - try { - return ((socket != null) - ? socket.getSoTimeout() - : timeout); - } - catch (SocketException se) { - return timeout; - } - } - - /** - * Returns an input stream that reads from this open connection. - * - * @return an input stream that reads from this open connection. - * @throws IOException if an I/O error occurs while - * creating the input stream. - */ - public synchronized InputStream getInputStream() throws IOException { - if (!connected) { - connect(); - } - - // Nothing to return - if (responseCode == HTTP_NOT_FOUND) { - throw new FileNotFoundException(url.toString()); - } - int length; - - if (inputStream == null) { - return null; - } - - // "De-chunk" the output stream - else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { - if (!(inputStream instanceof ChunkedInputStream)) { - inputStream = new ChunkedInputStream(inputStream); - } - } - - // Make sure we don't wait forever, if the content-length is known - else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { - if (!(inputStream instanceof FixedLengthInputStream)) { - inputStream = new FixedLengthInputStream(inputStream, length); - } - } - return inputStream; - } - - /** - * Returns an output stream that writes to this connection. - * - * @return an output stream that writes to this connection. - * @throws IOException if an I/O error occurs while - * creating the output stream. - */ - public synchronized OutputStream getOutputStream() throws IOException { - - if (!connected) { - connect(); - } - return outputStream; - } - - /** - * Indicates that other requests to the server - * are unlikely in the near future. Calling disconnect() - * should not imply that this HttpURLConnection - * instance can be reused for other requests. - */ - public void disconnect() { - if (socket != null) { - try { - socket.close(); - } - catch (IOException ioe) { - - // Does not matter, I guess. - } - socket = null; - } - connected = false; - } - - /** - * Internal connect method. - */ - private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { - // Find correct port - final int port = (pURL.getPort() > 0) - ? pURL.getPort() - : HTTP_DEFAULT_PORT; - - // Create socket if we don't have one - if (socket == null) { - //socket = new Socket(pURL.getHost(), port); // Blocks... - socket = createSocket(pURL, port, connectTimeout); - socket.setSoTimeout(timeout); - } - - // Get Socket output stream - OutputStream os = socket.getOutputStream(); - - // Connect using HTTP - writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); - - // Get response input stream - InputStream sis = socket.getInputStream(); - BufferedInputStream is = new BufferedInputStream(sis); - - // Detatch reponse headers from reponse input stream - InputStream header = detatchResponseHeader(is); - - // Parse headers and set response code/message - responseHeaders = parseResponseHeader(header); - responseHeaderFields = parseHeaderFields(responseHeaders); - - //System.err.println("Headers fields:"); - //responseHeaderFields.list(System.err); - // Test HTTP response code, to see if further action is needed - switch (getResponseCode()) { - case HTTP_OK: - // 200 OK - inputStream = is; - errorStream = null; - break; - - /* - case HTTP_PROXY_AUTH: - // 407 Proxy Authentication Required - */ - case HTTP_UNAUTHORIZED: - // 401 Unauthorized - // Set authorization and try again.. Slightly more compatible - responseCode = -1; - - // IS THIS REDIRECTION?? - //if (instanceFollowRedirects) { ??? - String auth = getHeaderField(HEADER_WWW_AUTH); - - // Missing WWW-Authenticate header for 401 response is an error - if (StringUtil.isEmpty(auth)) { - throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); - } - - // Get real mehtod from WWW-Authenticate header - int SP = auth.indexOf(" "); - String method; - String realm = null; - - if (SP >= 0) { - method = auth.substring(0, SP); - if (auth.length() >= SP + 7) { - realm = auth.substring(SP + 7); // " realm=".lenght() == 7 - } - - // else no realm - } - else { - // Default mehtod is Basic - method = SimpleAuthenticator.BASIC; - } - - // Get PasswordAuthentication - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, - pURL.getProtocol(), realm, method); - - // Avoid infinite loop - if (pRetries++ <= 0) { - throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does - } - else if (pa != null) { - connect(pURL, pa, method, pRetries); - } - break; - case HTTP_MOVED_PERM: - // 301 Moved Permanently - case HTTP_MOVED_TEMP: - // 302 Found - case HTTP_SEE_OTHER: - // 303 See Other - /* - case HTTP_USE_PROXY: - // 305 Use Proxy - // How do we handle this? - */ - case HTTP_REDIRECT: - // 307 Temporary Redirect - //System.err.println("Redirecting " + getResponseCode()); - if (instanceFollowRedirects) { - // Redirect - responseCode = -1; // Because of the java.net.URLConnection - - // getResponseCode implementation... - // --- - // I think redirects must be get? - //setRequestMethod("GET"); - // --- - String location = getHeaderField("Location"); - URL newLoc = new URL(pURL, location); - - // Test if we can reuse the Socket - if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { - socket.close(); // Close the socket, won't need it anymore - socket = null; - } - if (location != null) { - //System.err.println("Redirecting to " + location); - // Avoid infinite loop - if (--pRetries <= 0) { - throw new ProtocolException("Server redirected too many times (5)"); - } - else { - connect(newLoc, pAuth, pAuthType, pRetries); - } - } - break; - } - - // ...else, fall through default (if no Location: header) - default : - // Not 200 OK, or any of the redirect responses - // Probably an error... - errorStream = is; - inputStream = null; - } - - // --- Need rethinking... - // No further questions, let the Socket wait forever (until the server - // closes the connection) - //socket.setSoTimeout(0); - // Probably not... The timeout should only kick if the read BLOCKS. - // Shutdown output, meaning any writes to the outputstream below will - // probably fail... - //socket.shutdownOutput(); - // Not a good idea at all... POSTs need the outputstream to send the - // form-data. - // --- /Need rethinking. - outputStream = os; - } - - private static interface SocketConnector extends Runnable { - - /** - * Method getSocket - * - * @return the socket - * @throws IOException - */ - public Socket getSocket() throws IOException; - } - - /** - * Creates a socket to the given URL and port, with the given connect - * timeout. If the socket waits more than the given timout to connect, - * an ConnectException is thrown. - * - * @param pURL the URL to connect to - * @param pPort the port to connect to - * @param pConnectTimeout the connect timeout - * @return the created Socket. - * @throws ConnectException if the connection is refused or otherwise - * times out. - * @throws UnknownHostException if the IP address of the host could not be - * determined. - * @throws IOException if an I/O error occurs when creating the socket. - * @todo Move this code to a SocetImpl or similar? - * @see Socket#Socket(String,int) - */ - private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { - Socket socket; - final Object current = this; - SocketConnector connector; - Thread t = new Thread(connector = new SocketConnector() { - - private IOException mConnectException = null; - private Socket mLocalSocket = null; - - public Socket getSocket() throws IOException { - - if (mConnectException != null) { - throw mConnectException; - } - return mLocalSocket; - } - - // Run method - public void run() { - - try { - mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... - } - catch (IOException ioe) { - - // Store this exception for later - mConnectException = ioe; - } - - // Signal that we are done - synchronized (current) { - current.notify(); - } - } - }); - - t.start(); - - // Wait for connect - synchronized (this) { - try { - - /// Only wait if thread is alive! - if (t.isAlive()) { - if (pConnectTimeout > 0) { - wait(pConnectTimeout); - } - else { - wait(); - } - } - } - catch (InterruptedException ie) { - - // Continue excecution on interrupt? Hmmm.. - } - } - - // Throw exception if the socket didn't connect fast enough - if ((socket = connector.getSocket()) == null) { - throw new ConnectException("Socket connect timed out!"); - } - return socket; - } - - /** - * Opens a communications link to the resource referenced by this - * URL, if such a connection has not already been established. - *

- * If the {@code connect} method is called when the connection - * has already been opened (indicated by the {@code connected} - * field having the value {@code true}), the call is ignored. - *

- * URLConnection objects go through two phases: first they are - * created, then they are connected. After being created, and - * before being connected, various options can be specified - * (e.g., doInput and UseCaches). After connecting, it is an - * error to try to set them. Operations that depend on being - * connected, like getContentLength, will implicitly perform the - * connection, if necessary. - * - * @throws IOException if an I/O error occurs while opening the - * connection. - * @see java.net.URLConnection#connected - * @see RFC 2616 - */ - public void connect() throws IOException { - if (connected) { - return; // Ignore - } - connected = true; - connect(url, null, null, maxRedirects); - } - - /** - * TODO: Proxy support is still missing. - * - * @return this method returns false, as proxy suport is not implemented. - */ - public boolean usingProxy() { - return false; - } - - /** - * Writes the HTTP request headers, for HTTP GET method. - * - * @see RFC 2616 - */ - private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, - PasswordAuthentication pAuth, String pAuthType) { - PrintWriter out = new PrintWriter(pOut, true); // autoFlush - - if (!pUsingProxy) { - out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) - ? pURL.getPath() - : "/") + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); // HTTP/1.1 - - // out.println("Connection: close"); // No persistent connections yet - - /* - System.err.println(pMethod + " " - + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") - + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") - + " HTTP/1.1"); // HTTP/1.1 - */ - - // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) - out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "")); - - /* - System.err.println("Host: " + pURL.getHost() - + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); - */ - } - else { - - ////-- PROXY (absolute) VERSION - out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) - ? ":" + pURL.getPort() - : "") + pURL.getPath() + ((pURL.getQuery() != null) - ? "?" + pURL.getQuery() - : "") + " HTTP/1.1"); - } - - // Check if we have authentication - if (pAuth != null) { - - // If found, set Authorization header - byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); - - // "Authorization" ":" credentials - out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); - - /* - System.err.println("Authorization: " + pAuthType + " " - + BASE64.encode(userPass)); - */ - } - - // Iterate over properties - - for (Map.Entry property : pProps.entrySet()) { - out.println(property.getKey() + ": " + property.getValue()); - - //System.err.println(property.getKey() + ": " + property.getValue()); - } - out.println(); // Empty line, marks end of request-header - } - - /** - * Finds the end of the HTTP response header in an array of bytes. - * - * @todo This one's a little dirty... - */ - private static int findEndOfHeader(byte[] pBytes, int pEnd) { - byte[] header = HTTP_HEADER_END.getBytes(); - - // Normal condition, check all bytes - for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match - if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { - - //System.err.println("FOUND END OF HEADER!"); - return i + 4; - } - } - - // Check last 3 bytes, to check if we have a partial match - if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { - - //System.err.println("FOUND LAST BYTE"); - return -2; // LAST BYTE - } - else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { - - //System.err.println("FOUND LAST TWO BYTES"); - return -3; // LAST TWO BYTES - } - else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { - - //System.err.println("FOUND LAST THREE BYTES"); - return -4; // LAST THREE BYTES - } - return -1; // NO BYTES MATCH - } - - /** - * Reads the header part of the response, and copies it to a different - * InputStream. - */ - private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { - // Store header in byte array - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - pIS.mark(BUF_SIZE); - byte[] buffer = new byte[BUF_SIZE]; - int length; - int headerEnd; - - // Read from iput, store in bytes - while ((length = pIS.read(buffer)) != -1) { - - // End of header? - headerEnd = findEndOfHeader(buffer, length); - if (headerEnd >= 0) { - - // Write rest - bytes.write(buffer, 0, headerEnd); - - // Go back to last mark - pIS.reset(); - - // Position stream to right after header, and exit loop - pIS.skip(headerEnd); - break; - } - else if (headerEnd < -1) { - - // Write partial (except matching header bytes) - bytes.write(buffer, 0, length - 4); - - // Go back to last mark - pIS.reset(); - - // Position stream to right before potential header end - pIS.skip(length - 4); - } - else { - - // Write all - bytes.write(buffer, 0, length); - } - - // Can't read more than BUF_SIZE ahead anyway - pIS.mark(BUF_SIZE); - } - return new ByteArrayInputStream(bytes.toByteArray()); - } - - /** - * Pareses the response header fields. - */ - private static Properties parseHeaderFields(String[] pHeaders) { - Properties headers = new Properties(); - - // Get header information - int split; - String field; - String value; - - for (String header : pHeaders) { - //System.err.println(pHeaders[i]); - if ((split = header.indexOf(":")) > 0) { - - // Read & parse..? - field = header.substring(0, split); - value = header.substring(split + 1); - - //System.err.println(field + ": " + value.trim()); - headers.setProperty(StringUtil.toLowerCase(field), value.trim()); - } - } - return headers; - } - - /** - * Parses the response headers. - */ - private static String[] parseResponseHeader(InputStream pIS) throws IOException { - List headers = new ArrayList(); - - // Wrap Stream in Reader - BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); - - // Get response status - String header; - - while ((header = in.readLine()) != null) { - //System.err.println(header); - headers.add(header); - } - return headers.toArray(new String[headers.size()]); - } - - /** - * A FilterInputStream that wraps HTTP streams, with given content-length. - */ - protected static class FixedLengthInputStream extends FilterInputStream { - - private int mBytesLeft = 0; - - protected FixedLengthInputStream(InputStream pIS, int pLength) { - super(pIS); - mBytesLeft = pLength; - } - - public int available() throws IOException { - int available = in.available(); - - return ((available < mBytesLeft) - ? available - : mBytesLeft); - } - - public int read() throws IOException { - if (mBytesLeft-- > 0) { - return in.read(); - } - return -1; - } - - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - int read; - - if (mBytesLeft <= 0) { - return -1; // EOF - } - else if (mBytesLeft < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mBytesLeft); - - //System.err.println("Reading partial: " + read); - mBytesLeft -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mBytesLeft -= read; - return read; - } - } - - /** - * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. - */ - protected static class ChunkedInputStream extends FilterInputStream { - - private int mAvailableInCurrentChunk = 0; - - /** - * Creates an input streams that removes the "chunk-headers" and - * makes it look like any other input stream. - */ - protected ChunkedInputStream(InputStream pIS) { - - super(pIS); - if (pIS == null) { - throw new IllegalArgumentException("InputStream may not be null!"); - } - } - - /** - * Returns the number of bytes that can be read from this input stream - * without blocking. - *

- * This version returns whatever is less of in.available() and the - * length of the current chunk. - * - * @return the number of bytes that can be read from the input stream - * without blocking. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int available() throws IOException { - - if (mAvailableInCurrentChunk == 0) { - mAvailableInCurrentChunk = parseChunkSize(); - } - int realAvail = in.available(); - - return (mAvailableInCurrentChunk < realAvail) - ? mAvailableInCurrentChunk - : realAvail; - } - - /** - * Reads up to len bytes of data from this input stream into an array - * of bytes. This method blocks until some input is available. - *

- * This version will read up to len bytes of data, or as much as is - * available in the current chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @param pBytes the buffer into which the data is read. - * @param pOffset the start offset of the data. - * @param pLength the maximum number of bytes read. - * @return the total number of bytes read into the buffer, or -1 if - * there is no more data because the end of the stream has been - * reached. - * @throws IOException if an I/O error occurs. - * @see #in - */ - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - - //System.err.println("Avail: " + mAvailableInCurrentChunk - // + " length: " + pLength); - int read; - - if (mAvailableInCurrentChunk == -1) { - return -1; // EOF - } - if (mAvailableInCurrentChunk == 0) { - - //System.err.println("Nothing to read, parsing size!"); - // If nothing is read so far, read chunk header - mAvailableInCurrentChunk = parseChunkSize(); - return read(pBytes, pOffset, pLength); - } - else if (mAvailableInCurrentChunk < pLength) { - - // Read all available - read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); - - //System.err.println("Reading partial: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - // Just read - read = in.read(pBytes, pOffset, pLength); - - //System.err.println("Reading all avail: " + read); - mAvailableInCurrentChunk -= read; - return read; - } - - /** - * Reads the next byte of data from this input stream. The value byte - * is returned as an int in the range 0 to 255. If no byte is available - * because the end of the stream has been reached, the value -1 is - * returned. This method blocks until input data is available, the end - * of the stream is detected, or an exception is thrown. - *

- * This version reads one byte of data from the current chunk as long - * as there is more data in the chunk. If there is no more data in the - * curernt chunk, the method will read the size of the next chunk, and - * read from that, until the last chunk is read (a chunk with a size of - * 0). - * - * @return the next byte of data, or -1 if the end of the stream is - * reached. - * @see #in - */ - public int read() throws IOException { - - // We have no data, parse chunk header - if (mAvailableInCurrentChunk == -1) { - return -1; - } - else if (mAvailableInCurrentChunk == 0) { - - // Next chunk! - mAvailableInCurrentChunk = parseChunkSize(); - return read(); - } - mAvailableInCurrentChunk--; - return in.read(); - } - - /** - * Reads the chunk size from the chunk header - * {@code chunk-size [SP chunk-extension] CRLF}. - * The chunk-extension is simply discarded. - * - * @return the length of the current chunk, or -1 if the current chunk - * is the last-chunk (a chunk with the size of 0). - */ - protected int parseChunkSize() throws IOException { - - StringBuilder buf = new StringBuilder(); - int b; - - // read chunk-size, chunk-extension (if any) and CRLF - while ((b = in.read()) > 0) { - if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF - break; // except for this one... - } - buf.append((char) b); - } - String line = buf.toString(); - - // Happens, as we don't read CRLF off the end of the chunk data... - if (line.length() == 0) { - return 0; - } - - // Discard any chunk-extensions, and read size (HEX). - int spIdx = line.indexOf(' '); - int size = Integer.parseInt(((spIdx >= 0) - ? line.substring(0, spIdx) - : line), 16); - - // This is the last chunk (=EOF) - if (size == 0) { - return -1; - } - return size; - } - } -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A URLConnection with support for HTTP-specific features. See + * the spec for details. + * This version also supports read and connect timeouts, making it more useful + * for clients with limitted time. + *

+ * Note that the timeouts are created on the socket level, and that + *

+ * Note: This class should now work as expected, but it needs more testing before + * it can enter production release. + *
+ * --.k + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java#1 $ + * @todo Write JUnit TestCase + * @todo ConnectionMananger! + * @see RFC 2616 + */ +public class HttpURLConnection extends java.net.HttpURLConnection { + /** + * HTTP Status-Code 307: Temporary Redirect + */ + public final static int HTTP_REDIRECT = 307; + private final static int HTTP_DEFAULT_PORT = 80; + private final static String HTTP_HEADER_END = "\r\n\r\n"; + private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; + private final static int BUF_SIZE = 8192; + private int maxRedirects = (System.getProperty("http.maxRedirects") != null) + ? Integer.parseInt(System.getProperty("http.maxRedirects")) + : 20; + protected int timeout = -1; + protected int connectTimeout = -1; + private Socket socket = null; + protected InputStream errorStream = null; + protected InputStream inputStream = null; + protected OutputStream outputStream = null; + private String[] responseHeaders = null; + protected Properties responseHeaderFields = null; + protected Properties requestProperties = new Properties(); + + /** + * Creates a HttpURLConnection. + * + * @param pURL the URL to connect to. + */ + protected HttpURLConnection(URL pURL) { + this(pURL, 0, 0); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * and connect operations. + */ + protected HttpURLConnection(URL pURL, int pTimeout) { + this(pURL, pTimeout, pTimeout); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * operations. + * @param pConnectTimeout the maximum time the socket will block for + * connection. + */ + protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { + super(pURL); + setTimeout(pTimeout); + connectTimeout = pConnectTimeout; + } + + /** + * Sets the general request property. If a property with the key already + * exists, overwrite its value with the new value. + *

+ *

NOTE: HTTP requires all request properties which can + * legally have multiple instances with the same key + * to use a comma-seperated list syntax which enables multiple + * properties to be appended into a single property. + * + * @param pKey the keyword by which the request is known + * (e.g., "{@code accept}"). + * @param pValue the value associated with it. + * @see #getRequestProperty(java.lang.String) + */ + public void setRequestProperty(String pKey, String pValue) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + String oldValue = requestProperties.getProperty(pKey); + + if (oldValue == null) { + requestProperties.setProperty(pKey, pValue); + } + else { + requestProperties.setProperty(pKey, oldValue + ", " + pValue); + } + } + + /** + * Returns the value of the named general request property for this + * connection. + * + * @param pKey the keyword by which the request is known (e.g., "accept"). + * @return the value of the named general request property for this + * connection. + * @see #setRequestProperty(java.lang.String, java.lang.String) + */ + public String getRequestProperty(String pKey) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + return requestProperties.getProperty(pKey); + } + + /** + * Gets HTTP response status from responses like: + *

+     * HTTP/1.0 200 OK
+     * HTTP/1.0 401 Unauthorized
+     * 
+ * Extracts the ints 200 and 401 respectively. + * Returns -1 if none can be discerned + * from the response (i.e., the response is not valid HTTP). + *

+ * + * + * @return the HTTP Status-Code + * @throws IOException if an error occurred connecting to the server. + */ + public int getResponseCode() throws IOException { + if (responseCode != -1) { + return responseCode; + } + + // Make sure we've gotten the headers + getInputStream(); + String resp = getHeaderField(0); + + // should have no leading/trailing LWS + // expedite the typical case by assuming it has the + // form "HTTP/1.x 2XX " + int ind; + + try { + ind = resp.indexOf(' '); + while (resp.charAt(ind) == ' ') { + ind++; + } + responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); + responseMessage = resp.substring(ind + 4).trim(); + return responseCode; + } + catch (Exception e) { + return responseCode; + } + } + + /** + * Returns the name of the specified header field. + * + * @param pName the name of a header field. + * @return the value of the named header field, or {@code null} + * if there is no such field in the header. + */ + public String getHeaderField(String pName) { + return responseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); + } + + /** + * Returns the value for the {@code n}th header field. + * It returns {@code null} if there are fewer than + * {@code n} fields. + *

+ * This method can be used in conjunction with the + * {@code getHeaderFieldKey} method to iterate through all + * the headers in the message. + * + * @param pIndex an index. + * @return the value of the {@code n}th header field. + * @see java.net.URLConnection#getHeaderFieldKey(int) + */ + public String getHeaderField(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). + if ((pIndex == 0) || (field == null)) { + return field; + } + int idx = field.indexOf(':'); + + return ((idx > 0) + ? field.substring(idx).trim() + : ""); // TODO: "" or null? + } + + /** + * Returns the key for the {@code n}th header field. + * + * @param pIndex an index. + * @return the key for the {@code n}th header field, + * or {@code null} if there are fewer than {@code n} + * fields. + */ + public String getHeaderFieldKey(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= responseHeaders.length) { + return null; + } + String field = responseHeaders[pIndex]; + + if (StringUtil.isEmpty(field)) { + return null; + } + int idx = field.indexOf(':'); + + return StringUtil.toLowerCase(((idx > 0) + ? field.substring(0, idx) + : field)); + } + + /** + * Sets the read timeout for the undelying socket. + * A timeout of zero is interpreted as an + * infinite timeout. + * + * @param pTimeout the maximum time the socket will block for read + * operations, in milliseconds. + */ + public void setTimeout(int pTimeout) { + if (pTimeout < 0) { // Must be positive + throw new IllegalArgumentException("Timeout must be positive."); + } + timeout = pTimeout; + if (socket != null) { + try { + socket.setSoTimeout(pTimeout); + } + catch (SocketException se) { + // Not much to do about that... + } + } + } + + /** + * Gets the read timeout for the undelying socket. + * + * @return the maximum time the socket will block for read operations, in + * milliseconds. + * The default value is zero, which is interpreted as an + * infinite timeout. + */ + public int getTimeout() { + + try { + return ((socket != null) + ? socket.getSoTimeout() + : timeout); + } + catch (SocketException se) { + return timeout; + } + } + + /** + * Returns an input stream that reads from this open connection. + * + * @return an input stream that reads from this open connection. + * @throws IOException if an I/O error occurs while + * creating the input stream. + */ + public synchronized InputStream getInputStream() throws IOException { + if (!connected) { + connect(); + } + + // Nothing to return + if (responseCode == HTTP_NOT_FOUND) { + throw new FileNotFoundException(url.toString()); + } + int length; + + if (inputStream == null) { + return null; + } + + // "De-chunk" the output stream + else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { + if (!(inputStream instanceof ChunkedInputStream)) { + inputStream = new ChunkedInputStream(inputStream); + } + } + + // Make sure we don't wait forever, if the content-length is known + else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { + if (!(inputStream instanceof FixedLengthInputStream)) { + inputStream = new FixedLengthInputStream(inputStream, length); + } + } + return inputStream; + } + + /** + * Returns an output stream that writes to this connection. + * + * @return an output stream that writes to this connection. + * @throws IOException if an I/O error occurs while + * creating the output stream. + */ + public synchronized OutputStream getOutputStream() throws IOException { + + if (!connected) { + connect(); + } + return outputStream; + } + + /** + * Indicates that other requests to the server + * are unlikely in the near future. Calling disconnect() + * should not imply that this HttpURLConnection + * instance can be reused for other requests. + */ + public void disconnect() { + if (socket != null) { + try { + socket.close(); + } + catch (IOException ioe) { + + // Does not matter, I guess. + } + socket = null; + } + connected = false; + } + + /** + * Internal connect method. + */ + private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { + // Find correct port + final int port = (pURL.getPort() > 0) + ? pURL.getPort() + : HTTP_DEFAULT_PORT; + + // Create socket if we don't have one + if (socket == null) { + //socket = new Socket(pURL.getHost(), port); // Blocks... + socket = createSocket(pURL, port, connectTimeout); + socket.setSoTimeout(timeout); + } + + // Get Socket output stream + OutputStream os = socket.getOutputStream(); + + // Connect using HTTP + writeRequestHeaders(os, pURL, method, requestProperties, usingProxy(), pAuth, pAuthType); + + // Get response input stream + InputStream sis = socket.getInputStream(); + BufferedInputStream is = new BufferedInputStream(sis); + + // Detatch reponse headers from reponse input stream + InputStream header = detatchResponseHeader(is); + + // Parse headers and set response code/message + responseHeaders = parseResponseHeader(header); + responseHeaderFields = parseHeaderFields(responseHeaders); + + //System.err.println("Headers fields:"); + //responseHeaderFields.list(System.err); + // Test HTTP response code, to see if further action is needed + switch (getResponseCode()) { + case HTTP_OK: + // 200 OK + inputStream = is; + errorStream = null; + break; + + /* + case HTTP_PROXY_AUTH: + // 407 Proxy Authentication Required + */ + case HTTP_UNAUTHORIZED: + // 401 Unauthorized + // Set authorization and try again.. Slightly more compatible + responseCode = -1; + + // IS THIS REDIRECTION?? + //if (instanceFollowRedirects) { ??? + String auth = getHeaderField(HEADER_WWW_AUTH); + + // Missing WWW-Authenticate header for 401 response is an error + if (StringUtil.isEmpty(auth)) { + throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); + } + + // Get real mehtod from WWW-Authenticate header + int SP = auth.indexOf(" "); + String method; + String realm = null; + + if (SP >= 0) { + method = auth.substring(0, SP); + if (auth.length() >= SP + 7) { + realm = auth.substring(SP + 7); // " realm=".lenght() == 7 + } + + // else no realm + } + else { + // Default mehtod is Basic + method = SimpleAuthenticator.BASIC; + } + + // Get PasswordAuthentication + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, + pURL.getProtocol(), realm, method); + + // Avoid infinite loop + if (pRetries++ <= 0) { + throw new ProtocolException("Server redirected too many times (" + maxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does + } + else if (pa != null) { + connect(pURL, pa, method, pRetries); + } + break; + case HTTP_MOVED_PERM: + // 301 Moved Permanently + case HTTP_MOVED_TEMP: + // 302 Found + case HTTP_SEE_OTHER: + // 303 See Other + /* + case HTTP_USE_PROXY: + // 305 Use Proxy + // How do we handle this? + */ + case HTTP_REDIRECT: + // 307 Temporary Redirect + //System.err.println("Redirecting " + getResponseCode()); + if (instanceFollowRedirects) { + // Redirect + responseCode = -1; // Because of the java.net.URLConnection + + // getResponseCode implementation... + // --- + // I think redirects must be get? + //setRequestMethod("GET"); + // --- + String location = getHeaderField("Location"); + URL newLoc = new URL(pURL, location); + + // Test if we can reuse the Socket + if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { + socket.close(); // Close the socket, won't need it anymore + socket = null; + } + if (location != null) { + //System.err.println("Redirecting to " + location); + // Avoid infinite loop + if (--pRetries <= 0) { + throw new ProtocolException("Server redirected too many times (5)"); + } + else { + connect(newLoc, pAuth, pAuthType, pRetries); + } + } + break; + } + + // ...else, fall through default (if no Location: header) + default : + // Not 200 OK, or any of the redirect responses + // Probably an error... + errorStream = is; + inputStream = null; + } + + // --- Need rethinking... + // No further questions, let the Socket wait forever (until the server + // closes the connection) + //socket.setSoTimeout(0); + // Probably not... The timeout should only kick if the read BLOCKS. + // Shutdown output, meaning any writes to the outputstream below will + // probably fail... + //socket.shutdownOutput(); + // Not a good idea at all... POSTs need the outputstream to send the + // form-data. + // --- /Need rethinking. + outputStream = os; + } + + private static interface SocketConnector extends Runnable { + + /** + * Method getSocket + * + * @return the socket + * @throws IOException + */ + public Socket getSocket() throws IOException; + } + + /** + * Creates a socket to the given URL and port, with the given connect + * timeout. If the socket waits more than the given timout to connect, + * an ConnectException is thrown. + * + * @param pURL the URL to connect to + * @param pPort the port to connect to + * @param pConnectTimeout the connect timeout + * @return the created Socket. + * @throws ConnectException if the connection is refused or otherwise + * times out. + * @throws UnknownHostException if the IP address of the host could not be + * determined. + * @throws IOException if an I/O error occurs when creating the socket. + * @todo Move this code to a SocetImpl or similar? + * @see Socket#Socket(String,int) + */ + private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { + Socket socket; + final Object current = this; + SocketConnector connector; + Thread t = new Thread(connector = new SocketConnector() { + + private IOException mConnectException = null; + private Socket mLocalSocket = null; + + public Socket getSocket() throws IOException { + + if (mConnectException != null) { + throw mConnectException; + } + return mLocalSocket; + } + + // Run method + public void run() { + + try { + mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... + } + catch (IOException ioe) { + + // Store this exception for later + mConnectException = ioe; + } + + // Signal that we are done + synchronized (current) { + current.notify(); + } + } + }); + + t.start(); + + // Wait for connect + synchronized (this) { + try { + + /// Only wait if thread is alive! + if (t.isAlive()) { + if (pConnectTimeout > 0) { + wait(pConnectTimeout); + } + else { + wait(); + } + } + } + catch (InterruptedException ie) { + + // Continue excecution on interrupt? Hmmm.. + } + } + + // Throw exception if the socket didn't connect fast enough + if ((socket = connector.getSocket()) == null) { + throw new ConnectException("Socket connect timed out!"); + } + return socket; + } + + /** + * Opens a communications link to the resource referenced by this + * URL, if such a connection has not already been established. + *

+ * If the {@code connect} method is called when the connection + * has already been opened (indicated by the {@code connected} + * field having the value {@code true}), the call is ignored. + *

+ * URLConnection objects go through two phases: first they are + * created, then they are connected. After being created, and + * before being connected, various options can be specified + * (e.g., doInput and UseCaches). After connecting, it is an + * error to try to set them. Operations that depend on being + * connected, like getContentLength, will implicitly perform the + * connection, if necessary. + * + * @throws IOException if an I/O error occurs while opening the + * connection. + * @see java.net.URLConnection#connected + * @see RFC 2616 + */ + public void connect() throws IOException { + if (connected) { + return; // Ignore + } + connected = true; + connect(url, null, null, maxRedirects); + } + + /** + * TODO: Proxy support is still missing. + * + * @return this method returns false, as proxy suport is not implemented. + */ + public boolean usingProxy() { + return false; + } + + /** + * Writes the HTTP request headers, for HTTP GET method. + * + * @see RFC 2616 + */ + private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, + PasswordAuthentication pAuth, String pAuthType) { + PrintWriter out = new PrintWriter(pOut, true); // autoFlush + + if (!pUsingProxy) { + out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) + ? pURL.getPath() + : "/") + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); // HTTP/1.1 + + // out.println("Connection: close"); // No persistent connections yet + + /* + System.err.println(pMethod + " " + + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") + + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") + + " HTTP/1.1"); // HTTP/1.1 + */ + + // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) + out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "")); + + /* + System.err.println("Host: " + pURL.getHost() + + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); + */ + } + else { + + ////-- PROXY (absolute) VERSION + out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "") + pURL.getPath() + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); + } + + // Check if we have authentication + if (pAuth != null) { + + // If found, set Authorization header + byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); + + // "Authorization" ":" credentials + out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); + + /* + System.err.println("Authorization: " + pAuthType + " " + + BASE64.encode(userPass)); + */ + } + + // Iterate over properties + + for (Map.Entry property : pProps.entrySet()) { + out.println(property.getKey() + ": " + property.getValue()); + + //System.err.println(property.getKey() + ": " + property.getValue()); + } + out.println(); // Empty line, marks end of request-header + } + + /** + * Finds the end of the HTTP response header in an array of bytes. + * + * @todo This one's a little dirty... + */ + private static int findEndOfHeader(byte[] pBytes, int pEnd) { + byte[] header = HTTP_HEADER_END.getBytes(); + + // Normal condition, check all bytes + for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match + if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { + + //System.err.println("FOUND END OF HEADER!"); + return i + 4; + } + } + + // Check last 3 bytes, to check if we have a partial match + if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { + + //System.err.println("FOUND LAST BYTE"); + return -2; // LAST BYTE + } + else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { + + //System.err.println("FOUND LAST TWO BYTES"); + return -3; // LAST TWO BYTES + } + else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { + + //System.err.println("FOUND LAST THREE BYTES"); + return -4; // LAST THREE BYTES + } + return -1; // NO BYTES MATCH + } + + /** + * Reads the header part of the response, and copies it to a different + * InputStream. + */ + private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { + // Store header in byte array + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + pIS.mark(BUF_SIZE); + byte[] buffer = new byte[BUF_SIZE]; + int length; + int headerEnd; + + // Read from iput, store in bytes + while ((length = pIS.read(buffer)) != -1) { + + // End of header? + headerEnd = findEndOfHeader(buffer, length); + if (headerEnd >= 0) { + + // Write rest + bytes.write(buffer, 0, headerEnd); + + // Go back to last mark + pIS.reset(); + + // Position stream to right after header, and exit loop + pIS.skip(headerEnd); + break; + } + else if (headerEnd < -1) { + + // Write partial (except matching header bytes) + bytes.write(buffer, 0, length - 4); + + // Go back to last mark + pIS.reset(); + + // Position stream to right before potential header end + pIS.skip(length - 4); + } + else { + + // Write all + bytes.write(buffer, 0, length); + } + + // Can't read more than BUF_SIZE ahead anyway + pIS.mark(BUF_SIZE); + } + return new ByteArrayInputStream(bytes.toByteArray()); + } + + /** + * Pareses the response header fields. + */ + private static Properties parseHeaderFields(String[] pHeaders) { + Properties headers = new Properties(); + + // Get header information + int split; + String field; + String value; + + for (String header : pHeaders) { + //System.err.println(pHeaders[i]); + if ((split = header.indexOf(":")) > 0) { + + // Read & parse..? + field = header.substring(0, split); + value = header.substring(split + 1); + + //System.err.println(field + ": " + value.trim()); + headers.setProperty(StringUtil.toLowerCase(field), value.trim()); + } + } + return headers; + } + + /** + * Parses the response headers. + */ + private static String[] parseResponseHeader(InputStream pIS) throws IOException { + List headers = new ArrayList(); + + // Wrap Stream in Reader + BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); + + // Get response status + String header; + + while ((header = in.readLine()) != null) { + //System.err.println(header); + headers.add(header); + } + return headers.toArray(new String[headers.size()]); + } + + /** + * A FilterInputStream that wraps HTTP streams, with given content-length. + */ + protected static class FixedLengthInputStream extends FilterInputStream { + + private int mBytesLeft = 0; + + protected FixedLengthInputStream(InputStream pIS, int pLength) { + super(pIS); + mBytesLeft = pLength; + } + + public int available() throws IOException { + int available = in.available(); + + return ((available < mBytesLeft) + ? available + : mBytesLeft); + } + + public int read() throws IOException { + if (mBytesLeft-- > 0) { + return in.read(); + } + return -1; + } + + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + int read; + + if (mBytesLeft <= 0) { + return -1; // EOF + } + else if (mBytesLeft < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mBytesLeft); + + //System.err.println("Reading partial: " + read); + mBytesLeft -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mBytesLeft -= read; + return read; + } + } + + /** + * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. + */ + protected static class ChunkedInputStream extends FilterInputStream { + + private int mAvailableInCurrentChunk = 0; + + /** + * Creates an input streams that removes the "chunk-headers" and + * makes it look like any other input stream. + */ + protected ChunkedInputStream(InputStream pIS) { + + super(pIS); + if (pIS == null) { + throw new IllegalArgumentException("InputStream may not be null!"); + } + } + + /** + * Returns the number of bytes that can be read from this input stream + * without blocking. + *

+ * This version returns whatever is less of in.available() and the + * length of the current chunk. + * + * @return the number of bytes that can be read from the input stream + * without blocking. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int available() throws IOException { + + if (mAvailableInCurrentChunk == 0) { + mAvailableInCurrentChunk = parseChunkSize(); + } + int realAvail = in.available(); + + return (mAvailableInCurrentChunk < realAvail) + ? mAvailableInCurrentChunk + : realAvail; + } + + /** + * Reads up to len bytes of data from this input stream into an array + * of bytes. This method blocks until some input is available. + *

+ * This version will read up to len bytes of data, or as much as is + * available in the current chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @param pBytes the buffer into which the data is read. + * @param pOffset the start offset of the data. + * @param pLength the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or -1 if + * there is no more data because the end of the stream has been + * reached. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + + //System.err.println("Avail: " + mAvailableInCurrentChunk + // + " length: " + pLength); + int read; + + if (mAvailableInCurrentChunk == -1) { + return -1; // EOF + } + if (mAvailableInCurrentChunk == 0) { + + //System.err.println("Nothing to read, parsing size!"); + // If nothing is read so far, read chunk header + mAvailableInCurrentChunk = parseChunkSize(); + return read(pBytes, pOffset, pLength); + } + else if (mAvailableInCurrentChunk < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); + + //System.err.println("Reading partial: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + /** + * Reads the next byte of data from this input stream. The value byte + * is returned as an int in the range 0 to 255. If no byte is available + * because the end of the stream has been reached, the value -1 is + * returned. This method blocks until input data is available, the end + * of the stream is detected, or an exception is thrown. + *

+ * This version reads one byte of data from the current chunk as long + * as there is more data in the chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @return the next byte of data, or -1 if the end of the stream is + * reached. + * @see #in + */ + public int read() throws IOException { + + // We have no data, parse chunk header + if (mAvailableInCurrentChunk == -1) { + return -1; + } + else if (mAvailableInCurrentChunk == 0) { + + // Next chunk! + mAvailableInCurrentChunk = parseChunkSize(); + return read(); + } + mAvailableInCurrentChunk--; + return in.read(); + } + + /** + * Reads the chunk size from the chunk header + * {@code chunk-size [SP chunk-extension] CRLF}. + * The chunk-extension is simply discarded. + * + * @return the length of the current chunk, or -1 if the current chunk + * is the last-chunk (a chunk with the size of 0). + */ + protected int parseChunkSize() throws IOException { + + StringBuilder buf = new StringBuilder(); + int b; + + // read chunk-size, chunk-extension (if any) and CRLF + while ((b = in.read()) > 0) { + if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF + break; // except for this one... + } + buf.append((char) b); + } + String line = buf.toString(); + + // Happens, as we don't read CRLF off the end of the chunk data... + if (line.length() == 0) { + return 0; + } + + // Discard any chunk-extensions, and read size (HEX). + int spIdx = line.indexOf(' '); + int size = Integer.parseInt(((spIdx >= 0) + ? line.substring(0, spIdx) + : line), 16); + + // This is the last chunk (=EOF) + if (size == 0) { + return -1; + } + return size; + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java similarity index 85% rename from common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java index ae7faefd..09d94938 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/NetUtil.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/NetUtil.java @@ -1,1422 +1,1258 @@ -package com.twelvemonkeys.net; - -import com.twelvemonkeys.io.FileUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.DateUtil; -import com.twelvemonkeys.util.CollectionUtil; - -import java.io.*; -import java.net.*; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.*; - -/** - * Utility class with network related methods. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ - */ -public final class NetUtil { - - private final static String VERSION_ID = "NetUtil/2.1"; - - private static Authenticator sAuthenticator = null; - - private final static int BUF_SIZE = 8192; - private final static String HTTP = "http://"; - private final static String HTTPS = "https://"; - - /** - * Field HTTP_PROTOCOL - */ - public final static String HTTP_PROTOCOL = "http"; - - /** - * Field HTTPS_PROTOCOL - */ - public final static String HTTPS_PROTOCOL = "https"; - - /** - * Field HTTP_GET - */ - public final static String HTTP_GET = "GET"; - - /** - * Field HTTP_POST - */ - public final static String HTTP_POST = "POST"; - - /** - * Field HTTP_HEAD - */ - public final static String HTTP_HEAD = "HEAD"; - - /** - * Field HTTP_OPTIONS - */ - public final static String HTTP_OPTIONS = "OPTIONS"; - - /** - * Field HTTP_PUT - */ - public final static String HTTP_PUT = "PUT"; - - /** - * Field HTTP_DELETE - */ - public final static String HTTP_DELETE = "DELETE"; - - /** - * Field HTTP_TRACE - */ - public final static String HTTP_TRACE = "TRACE"; - - /** - * RFC 1123 date format, as reccomended by RFC 2616 (HTTP/1.1), sec 3.3 - * NOTE: All date formats are private, to ensure synchronized access. - */ - private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); - static { - HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - } - - /** - * RFC 850 date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3 - * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). - */ - private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); - /** - * ANSI C asctime() date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3. - * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). - */ - private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US); - - private static long sNext50YearWindowChange = DateUtil.currentTimeDay(); - static { - HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); - - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3: - // - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date - // which appears to be more than 50 years in the future is in fact - // in the past (this helps solve the "year 2000" problem). - update50YearWindowIfNeeded(); - } - - private static void update50YearWindowIfNeeded() { - // Avoid class synchronization - long next = sNext50YearWindowChange; - - if (next < System.currentTimeMillis()) { - // Next check in one day - next += DateUtil.DAY; - sNext50YearWindowChange = next; - - Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR)); - //System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate); - synchronized (HTTP_RFC850_FORMAT) { - HTTP_RFC850_FORMAT.set2DigitYearStart(startDate); - } - synchronized (HTTP_ASCTIME_FORMAT) { - HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate); - } - } - } - - /** - * Creates a NetUtil. - * This class has only static methods and members, and should not be - * instantiated. - */ - private NetUtil() { - } - - public static void main1(String[] args) { - String timeStr = (args.length > 0 && !StringUtil.isNumber(args[0])) ? args[0] : null; - - long time = args.length > 0 ? - (timeStr != null ? parseHTTPDate(timeStr) : Long.parseLong(args[0])) - : System.currentTimeMillis(); - System.out.println(timeStr + " --> " + time + " --> " + formatHTTPDate(time)); - } - - /** - * Main method, reads data from a URL and, optionally, writes it to stdout or a file. - * @param pArgs command line arguemnts - * @throws java.io.IOException if an I/O exception occurs - */ - public static void main(String[] pArgs) throws IOException { - // params: - int timeout = 0; - boolean followRedirects = true; - boolean debugHeaders = false; - String requestPropertiesFile = null; - String requestHeaders = null; - String postData = null; - File putData = null; - int argIdx = 0; - boolean errArgs = false; - boolean writeToFile = false; - boolean writeToStdOut = false; - String outFileName = null; - - while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { - if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { - argIdx++; - try { - timeout = Integer.parseInt(pArgs[argIdx++]); - } - catch (NumberFormatException nfe) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { - debugHeaders = true; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { - followRedirects = false; - argIdx++; - } - else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { - argIdx++; - requestPropertiesFile = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { - argIdx++; - postData = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { - argIdx++; - putData = new File(pArgs[argIdx++]); - if (!putData.exists()) { - errArgs = true; - break; - } - } - else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { - argIdx++; - requestHeaders = pArgs[argIdx++]; - } - else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { - argIdx++; - writeToFile = true; - - // Get optional file name - if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { - outFileName = pArgs[argIdx++]; - } - } - else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { - argIdx++; - writeToStdOut = true; - } - else { - System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); - } - } - if (errArgs || (pArgs.length < (argIdx + 1))) { - System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); - System.exit(5); - } - String url = pArgs[argIdx/*++*/]; - - // DONE ARGS - // Get request properties - Properties requestProperties = new Properties(); - - if (requestPropertiesFile != null) { - - // Just read, no exception handling... - requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); - } - if (requestHeaders != null) { - - // Get request headers - String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); - - for (String headerPair : headerPairs) { - String[] pair = StringUtil.toStringArray(headerPair, ":"); - String key = (pair.length > 0) - ? pair[0].trim() - : null; - String value = (pair.length > 1) - ? pair[1].trim() - : ""; - - if (key != null) { - requestProperties.setProperty(key, value); - } - } - } - java.net.HttpURLConnection conn; - - // Create connection - URL reqURL = getURLAndSetAuthorization(url, requestProperties); - - conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); - - // POST - if (postData != null) { - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Set entity headers - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); - conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); - - // Write post data to the stream - writer.write(postData); - writer.write("\r\n"); - - //writer.flush(); - writer.close(); // Does this close the underlying stream? - } - // PUT - else if (putData != null) { - // HTTP PUT method - conn.setRequestMethod(HTTP_PUT); - - // Set entity headers - //conn.setRequestProperty("Content-Type", "???"); - // TODO: Set Content-Type to correct type? - // TODO: Set content-encoding? Or can binary data be sent directly? - conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - - System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); - - // Write put data to the stream - FileUtil.copy(new FileInputStream(putData), os); - - os.close(); - } - - // - InputStream is; - - if (conn.getResponseCode() == 200) { - - // Connect and get stream - is = conn.getInputStream(); - } - else { - is = conn.getErrorStream(); - } - - // - if (debugHeaders) { - System.err.println("Request (debug):"); - System.err.println(conn.getClass()); - System.err.println("Response (debug):"); - - // Headerfield 0 is response code - System.err.println(conn.getHeaderField(0)); - - // Loop from 1, as headerFieldKey(0) == null... - for (int i = 1; ; i++) { - String key = conn.getHeaderFieldKey(i); - - // Seems to be the way to loop through them all... - if (key == null) { - break; - } - System.err.println(key + ": " + conn.getHeaderField(key)); - } - } - - // Create output file if specified - OutputStream os; - - if (writeToFile) { - if (outFileName == null) { - outFileName = reqURL.getFile(); - if (StringUtil.isEmpty(outFileName)) { - outFileName = conn.getHeaderField("Location"); - if (StringUtil.isEmpty(outFileName)) { - outFileName = "index"; - - // Find a suitable extension - // TODO: Replace with MIME-type util with MIME/file ext mapping - String ext = conn.getContentType(); - - if (!StringUtil.isEmpty(ext)) { - int idx = ext.lastIndexOf('/'); - - if (idx >= 0) { - ext = ext.substring(idx + 1); - } - idx = ext.indexOf(';'); - if (idx >= 0) { - ext = ext.substring(0, idx); - } - outFileName += "." + ext; - } - } - } - int idx = outFileName.lastIndexOf('/'); - - if (idx >= 0) { - outFileName = outFileName.substring(idx + 1); - } - idx = outFileName.indexOf('?'); - if (idx >= 0) { - outFileName = outFileName.substring(0, idx); - } - } - File outFile = new File(outFileName); - - if (!outFile.createNewFile()) { - if (outFile.exists()) { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); - } - else { - System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); - } - System.exit(5); - } - os = new FileOutputStream(outFile); - } - else if (writeToStdOut) { - os = System.out; - } - else { - os = null; - } - - // Get data. - if ((writeToFile || writeToStdOut) && is != null) { - FileUtil.copy(is, os); - } - - /* - Hashtable postData = new Hashtable(); - postData.put("SearchText", "condition"); - - try { - InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, - props, true, 0); - out = new FileOutputStream(file); - FileUtil.copy(in, out); - } - catch (Exception e) { - System.err.println("Error: " + e); - e.printStackTrace(System.err); - continue; - } - */ - } - - /* - public static class Cookie { - String mName = null; - String mValue = null; - - public Cookie(String pName, String pValue) { - mName = pName; - mValue = pValue; - } - - public String toString() { - return mName + "=" + mValue; - } - */ - - /* - // Just a way to set cookies.. - if (pCookies != null) { - String cookieStr = ""; - for (int i = 0; i < pCookies.length; i++) - cookieStr += ((i == pCookies.length) ? pCookies[i].toString() - : pCookies[i].toString() + ";"); - - // System.out.println("Cookie: " + cookieStr); - - conn.setRequestProperty("Cookie", cookieStr); - } - */ - - /* - } - */ - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(String pURL) { - return ((pURL != null) && pURL.startsWith(HTTP)); - } - - /** - * Test if the given URL is using HTTP protocol. - * - * @param pURL the url to condition - * @return true if the protocol is HTTP. - */ - public static boolean isHttpURL(URL pURL) { - return ((pURL != null) && pURL.getProtocol().equals("http")); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws MalformedURLException if the urlName parameter is not a valid - * URL. Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static byte[] getBytesHttp(String pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the content from a given URL, and returns it as a byte array. - * - * @param pURL the URL to get. - * @return a byte array with the URL contents. If an error occurs, the - * returned array may be zero-length, but not null. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(String) - */ - public static byte[] getBytesHttp(URL pURL) throws IOException { - return getBytesHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * NOTE: If you supply a username and password for HTTP - * authentication, this method uses the java.net.Authenticator's static - * {@code setDefault()} method, that can only be set ONCE. This - * means that if the default Authenticator is allready set, this method - * will fail. - * It also means if any other piece of code tries to register a new default - * Authenticator within the current VM, it will fail. - * - * @param pURL A String containing the URL, on the form - * [http://][:@]servername[/file.ext] - * where everything in brackets are optional. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws MalformedURLException if the urlName parameter specifies an - * unknown protocol, or does not form a valid URL. - * Note that the protocol cannot be anything but HTTP. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see java.net.Authenticator - * @see SimpleAuthenticator - */ - public static InputStream getInputStreamHttp(String pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL. - * - * @param pURL the URL to get. - * @return an input stream that reads from the connection created by the - * given URL. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(String) - */ - public static InputStream getInputStreamHttp(URL pURL) throws IOException { - return getInputStreamHttp(pURL, 0); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) - throws IOException { - - // Make sure we have properties - Properties properties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, properties); - - //unregisterPassword(url); - return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); - } - - /** - * Registers the password from the URL string, and returns the URL object. - * - * @param pURL the string representation of the URL, possibly including authorization part - * @param pProperties the - * @return the URL created from {@code pURL}. - * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} - */ - private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { - String url = pURL; - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - int httpIdx = url.indexOf(HTTPS); - - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - url = url.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = url.indexOf(HTTP); - if (httpIdx >= 0) { - url = url.substring(httpIdx + HTTP.length()); - } - } - - // Get authorization part - int atIdx = url.indexOf("@"); - - if (atIdx >= 0) { - userPass = url.substring(0, atIdx); - url = url.substring(atIdx + 1); - } - - // Set authorization if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); - } - - // Return URL - return new URL(protocolPrefix + url); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { - return getInputStreamHttp(pURL, null, true, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP GET method - conn.setRequestMethod(HTTP_GET); - - // This is where the connect happens - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - pProperties = pProperties != null ? pProperties : new Properties(); - - //URL url = getURLAndRegisterPassword(pURL); - URL url = getURLAndSetAuthorization(pURL, pProperties); - - //unregisterPassword(url); - return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); - } - - /** - * Gets the InputStream from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. This - * might happen BEFORE OR AFTER this method returns, as the HTTP headers - * will be read and parsed from the InputStream before this method returns, - * while further read operations on the returned InputStream might be - * performed at a later stage. - *
- *
- * - * @param pURL the URL to get. - * @param pPostData the post data. - * @param pProperties the request header properties. - * @param pFollowRedirects specifying wether redirects should be followed. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - */ - public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - // Open the connection, and get the stream - java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); - - // HTTP POST method - conn.setRequestMethod(HTTP_POST); - - // Iterate over and create post data string - StringBuilder postStr = new StringBuilder(); - - if (pPostData != null) { - Iterator data = pPostData.entrySet().iterator(); - - while (data.hasNext()) { - Map.Entry entry = (Map.Entry) data.next(); - - // Properties key/values can be safely cast to strings - // Encode the string - postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); - postStr.append('='); - postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); - - if (data.hasNext()) { - postStr.append('&'); - } - } - } - - // Set entity headers - String encoding = conn.getRequestProperty("Content-Encoding"); - if (StringUtil.isEmpty(encoding)) { - encoding = "UTF-8"; - } - conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); - conn.setRequestProperty("Content-Encoding", encoding); - - // Get outputstream (this is where the connect actually happens) - OutputStream os = conn.getOutputStream(); - OutputStreamWriter writer = new OutputStreamWriter(os, encoding); - - // Write post data to the stream - writer.write(postStr.toString()); - writer.write("\r\n"); - writer.close(); // Does this close the underlying stream? - - // Get the inputstream - InputStream is = conn.getInputStream(); - - // We only accept the 200 OK message - // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? - if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); - } - return is; - } - - /** - * Creates a HTTP connection to the given URL. - * - * @param pURL the URL to get. - * @param pProperties connection properties. - * @param pFollowRedirects specifies whether we should follow redirects. - * @param pTimeout the specified timeout, in milliseconds. - * @return a HttpURLConnection - * @throws UnknownHostException if the hostname in the URL cannot be found. - * @throws IOException if an I/O exception occurs. - */ - public static java.net.HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) - throws IOException { - - // Open the connection, and get the stream - java.net.HttpURLConnection conn; - - if (pTimeout > 0) { - // Supports timeout - conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); - } - else { - // Faster, more compatible - conn = (java.net.HttpURLConnection) pURL.openConnection(); - } - - // Set user agent - if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { - conn.setRequestProperty("User-Agent", - VERSION_ID - + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " - + System.getProperty("os.arch") + "; " - + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); - } - - // Set request properties - if (pProperties != null) { - for (Map.Entry entry : pProperties.entrySet()) { - // Properties key/values can be safely cast to strings - conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); - } - } - - try { - // Breaks with JRE1.2? - conn.setInstanceFollowRedirects(pFollowRedirects); - } - catch (LinkageError le) { - // This is the best we can do... - java.net.HttpURLConnection.setFollowRedirects(pFollowRedirects); - System.err.println("You are using an old Java Spec, consider upgrading."); - System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); - - //le.printStackTrace(System.err); - } - - conn.setDoInput(true); - conn.setDoOutput(true); - - //conn.setUseCaches(true); - return conn; - } - - /** - * This is a hack to get around the protected constructors in - * HttpURLConnection, should maybe consider registering and do things - * properly... - */ - - /* - private static class TimedHttpURLConnection - extends com.twelvemonkeys.net.HttpURLConnection { - TimedHttpURLConnection(URL pURL, int pTimeout) { - super(pURL, pTimeout); - } - } - */ - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. Supports basic HTTP - * authentication, using a URL string similar to most browsers. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return a byte array that is read from the socket connection, created - * from the given URL. - * @throws MalformedURLException if the url parameter specifies an - * unknown protocol, or does not form a valid URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getBytesHttp(URL,int) - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Gets the content from a given URL, with the given timeout. - * The timeout must be > 0. A timeout of zero is interpreted as an - * infinite timeout. - *

- * Implementation note: If the timeout parameter is greater than 0, - * this method uses my own implementation of - * java.net.HttpURLConnection, that uses plain sockets, to create an - * HTTP connection to the given URL. The {@code read} methods called - * on the returned InputStream, will block only for the specified timeout. - * If the timeout expires, a java.io.InterruptedIOException is raised. - *
- *
- * - * @param pURL the URL to get. - * @param pTimeout the specified timeout, in milliseconds. - * @return an input stream that reads from the socket connection, created - * from the given URL. - * @throws UnknownHostException if the IP address for the given URL cannot - * be resolved. - * @throws FileNotFoundException if there is no file at the given URL. - * @throws IOException if an error occurs during transfer. - * @see #getInputStreamHttp(URL,int) - * @see com.twelvemonkeys.net.HttpURLConnection - * @see java.net.Socket - * @see java.net.Socket#setSoTimeout(int) setSoTimeout - * @see java.net.HttpURLConnection - * @see java.io.InterruptedIOException - * @see RFC 2616 - */ - public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); - - // Get all the bytes in loop - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes.write(buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes.toByteArray(); - } - - /** - * Unregisters the password asscociated with this URL - */ - - /* - private static void unregisterPassword(URL pURL) { - Authenticator auth = registerAuthenticator(); - if (auth != null && auth instanceof SimpleAuthenticator) - ((SimpleAuthenticator) auth) - .unregisterPasswordAuthentication(pURL); - } - */ - - /** - * Registers the password from the URL string, and returns the URL object. - */ - - /* - private static URL getURLAndRegisterPassword(String pURL) - throws MalformedURLException - { - // Split user/password away from url - String userPass = null; - String protocolPrefix = HTTP; - - int httpIdx = pURL.indexOf(HTTPS); - if (httpIdx >= 0) { - protocolPrefix = HTTPS; - pURL = pURL.substring(httpIdx + HTTPS.length()); - } - else { - httpIdx = pURL.indexOf(HTTP); - if (httpIdx >= 0) - pURL = pURL.substring(httpIdx + HTTP.length()); - } - - int atIdx = pURL.indexOf("@"); - if (atIdx >= 0) { - userPass = pURL.substring(0, atIdx); - pURL = pURL.substring(atIdx + 1); - } - - // Set URL - URL url = new URL(protocolPrefix + pURL); - - // Set Authenticator if user/password is present - if (userPass != null) { - // System.out.println("Setting password ("+ userPass + ")!"); - - int colIdx = userPass.indexOf(":"); - if (colIdx < 0) - throw new MalformedURLException("Error in username/password!"); - - String userName = userPass.substring(0, colIdx); - String passWord = userPass.substring(colIdx + 1); - - // Try to register the authenticator - // System.out.println("Trying to register authenticator!"); - Authenticator auth = registerAuthenticator(); - - // System.out.println("Got authenticator " + auth + "."); - - // Register our username/password with it - if (auth != null && auth instanceof SimpleAuthenticator) { - ((SimpleAuthenticator) auth) - .registerPasswordAuthentication(url, - new PasswordAuthentication(userName, - passWord.toCharArray())); - } - else { - // Not supported! - throw new RuntimeException("Could not register PasswordAuthentication"); - } - } - - return url; - } - */ - - /** - * Registers the Authenticator given in the system property - * {@code java.net.Authenticator}, or the default implementation - * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). - *

- * BUG: What if authenticator has allready been set outside this class? - * - * @return The Authenticator created and set as default, or null, if it - * was not set as the default. However, there is no (clean) way to - * be sure the authenticator was set (the SimpleAuthenticator uses - * a hack to get around this), so it might be possible that the - * returned authenticator was not set as default... - * @see Authenticator#setDefault(Authenticator) - * @see SimpleAuthenticator - */ - public synchronized static Authenticator registerAuthenticator() { - if (sAuthenticator != null) { - return sAuthenticator; - } - - // Get the system property - String authenticatorName = System.getProperty("java.net.Authenticator"); - - // Try to get the Authenticator from the system property - if (authenticatorName != null) { - try { - Class authenticatorClass = Class.forName(authenticatorName); - - sAuthenticator = (Authenticator) authenticatorClass.newInstance(); - } - catch (ClassNotFoundException cnfe) { - // We should maybe rethrow this? - } - catch (InstantiationException ie) { - // Ignore - } - catch (IllegalAccessException iae) { - // Ignore - } - } - - // Get the default authenticator - if (sAuthenticator == null) { - sAuthenticator = SimpleAuthenticator.getInstance(); - } - - // Register authenticator as default - Authenticator.setDefault(sAuthenticator); - return sAuthenticator; - } - - /** - * Creates the InetAddress object from the given URL. - * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} - * except that it returns null, instead of throwing UnknownHostException. - * - * @param pURL the URL to look up. - * @return the createad InetAddress, or null if the host was unknown. - * @see java.net.InetAddress - * @see java.net.URL - */ - public static InetAddress createInetAddressFromURL(URL pURL) { - try { - return InetAddress.getByName(pURL.getHost()); - } - catch (UnknownHostException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using the given - * protocol. - * Equivalent to calling - * {@code new URL(protocol, InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @param pProtocol the protocol to use in the new URL - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { - try { - return new URL(pProtocol, pIP.getHostName(), ""); - } - catch (MalformedURLException e) { - return null; - } - } - - /** - * Creates an URL from the given InetAddress object, using HTTP protocol. - * Equivalent to calling - * {@code new URL("http", InetAddress.getHostName(), "")} - * except that it returns null, instead of throwing MalformedURLException. - * - * @param pIP the IP address to look up - * @return the created URL or null, if the URL could not be created. - * @see java.net.URL - * @see java.net.InetAddress - */ - public static URL createURLFromInetAddress(InetAddress pIP) { - return createURLFromInetAddress(pIP, HTTP); - } - - /* - * TODO: Benchmark! - */ - static byte[] getBytesHttpOld(String pURL) throws IOException { - // Get the input stream from the url - InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); - - // Get all the bytes in loop - byte[] bytes = new byte[0]; - int count; - byte[] buffer = new byte[BUF_SIZE]; - - try { - while ((count = in.read(buffer)) != -1) { - - // NOTE: According to the J2SE API doc, read(byte[]) will read - // at least 1 byte, or return -1, if end-of-file is reached. - bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); - } - } - finally { - - // Close the buffer - in.close(); - } - return bytes; - } - - /** - * Formats the time to a HTTP date, using the RFC 1123 format, as described - * in RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @param pTime the time - * @return a {@code String} representation of the time - */ - public static String formatHTTPDate(long pTime) { - return formatHTTPDate(new Date(pTime)); - } - - /** - * Formats the time to a HTTP date, using the RFC 1123 format, as described - * in RFC 2616 (HTTP/1.1), sec. 3.3. - * - * @param pTime the time - * @return a {@code String} representation of the time - */ - public static String formatHTTPDate(Date pTime) { - synchronized (HTTP_RFC1123_FORMAT) { - return HTTP_RFC1123_FORMAT.format(pTime); - } - } - - /** - * Parses a HTTP date string into a {@code long} representing milliseconds - * since January 1, 1970 GMT. - *

- * Use this method with headers that contain dates, such as - * {@code If-Modified-Since} or {@code Last-Modified}. - *

- * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() - * format, as described in - * RFC 2616 (HTTP/1.1), sec. 3.3 - * - * @param pDate the date to parse - * - * @return a {@code long} value representing the date, expressed as the - * number of milliseconds since January 1, 1970 GMT, - * @throws NumberFormatException if the date parameter is not parseable. - * @throws IllegalArgumentException if the date paramter is {@code null} - */ - public static long parseHTTPDate(String pDate) throws NumberFormatException { - return parseHTTPDateImpl(pDate).getTime(); - } - - /** - * ParseHTTPDate implementation - * - * @param pDate the date string to parse - * - * @return a {@code Date} - * @throws NumberFormatException if the date parameter is not parseable. - * @throws IllegalArgumentException if the date paramter is {@code null} - */ - private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException { - if (pDate == null) { - throw new IllegalArgumentException("date == null"); - } - - if (StringUtil.isEmpty(pDate)) { - throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - } - - DateFormat format; - - if (pDate.indexOf('-') >= 0) { - format = HTTP_RFC850_FORMAT; - update50YearWindowIfNeeded(); - } - else if (pDate.indexOf(',') < 0) { - format = HTTP_ASCTIME_FORMAT; - update50YearWindowIfNeeded(); - } - else { - format = HTTP_RFC1123_FORMAT; - // NOTE: RFC1123 always uses 4-digit years - } - - Date date; - try { - //noinspection SynchronizationOnLocalVariableOrMethodParameter - synchronized (format) { - date = format.parse(pDate); - } - } - catch (ParseException e) { - NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - nfe.initCause(e); - throw nfe; - } - - if (date == null) { - throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); - } - - return date; - } +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.CollectionUtil; + +import java.io.*; +import java.net.*; +import java.net.HttpURLConnection; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +/** + * Utility class with network related methods. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java#2 $ + */ +public final class NetUtil { + + private final static String VERSION_ID = "NetUtil/2.1"; + + private static Authenticator sAuthenticator = null; + + private final static int BUF_SIZE = 8192; + private final static String HTTP = "http://"; + private final static String HTTPS = "https://"; + + /** + * Field HTTP_PROTOCOL + */ + public final static String HTTP_PROTOCOL = "http"; + + /** + * Field HTTPS_PROTOCOL + */ + public final static String HTTPS_PROTOCOL = "https"; + + /** + * Field HTTP_GET + */ + public final static String HTTP_GET = "GET"; + + /** + * Field HTTP_POST + */ + public final static String HTTP_POST = "POST"; + + /** + * Field HTTP_HEAD + */ + public final static String HTTP_HEAD = "HEAD"; + + /** + * Field HTTP_OPTIONS + */ + public final static String HTTP_OPTIONS = "OPTIONS"; + + /** + * Field HTTP_PUT + */ + public final static String HTTP_PUT = "PUT"; + + /** + * Field HTTP_DELETE + */ + public final static String HTTP_DELETE = "DELETE"; + + /** + * Field HTTP_TRACE + */ + public final static String HTTP_TRACE = "TRACE"; + + /** + * Creates a NetUtil. + * This class has only static methods and members, and should not be + * instantiated. + */ + private NetUtil() { + } + + /** + * Main method, reads data from a URL and, optionally, writes it to stdout or a file. + * @param pArgs command line arguemnts + * @throws java.io.IOException if an I/O exception occurs + */ + public static void main(String[] pArgs) throws IOException { + // params: + int timeout = 0; + boolean followRedirects = true; + boolean debugHeaders = false; + String requestPropertiesFile = null; + String requestHeaders = null; + String postData = null; + File putData = null; + int argIdx = 0; + boolean errArgs = false; + boolean writeToFile = false; + boolean writeToStdOut = false; + String outFileName = null; + + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { + argIdx++; + try { + timeout = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { + debugHeaders = true; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { + followRedirects = false; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { + argIdx++; + requestPropertiesFile = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { + argIdx++; + postData = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { + argIdx++; + putData = new File(pArgs[argIdx++]); + if (!putData.exists()) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { + argIdx++; + requestHeaders = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { + argIdx++; + writeToFile = true; + + // Get optional file name + if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { + outFileName = pArgs[argIdx++]; + } + } + else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { + argIdx++; + writeToStdOut = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (pArgs.length < (argIdx + 1))) { + System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); + System.exit(5); + } + String url = pArgs[argIdx/*++*/]; + + // DONE ARGS + // Get request properties + Properties requestProperties = new Properties(); + + if (requestPropertiesFile != null) { + + // Just read, no exception handling... + requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); + } + if (requestHeaders != null) { + + // Get request headers + String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); + + for (String headerPair : headerPairs) { + String[] pair = StringUtil.toStringArray(headerPair, ":"); + String key = (pair.length > 0) + ? pair[0].trim() + : null; + String value = (pair.length > 1) + ? pair[1].trim() + : ""; + + if (key != null) { + requestProperties.setProperty(key, value); + } + } + } + HttpURLConnection conn; + + // Create connection + URL reqURL = getURLAndSetAuthorization(url, requestProperties); + + conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); + + // POST + if (postData != null) { + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Set entity headers + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); + conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); + + // Write post data to the stream + writer.write(postData); + writer.write("\r\n"); + + //writer.flush(); + writer.close(); // Does this close the underlying stream? + } + // PUT + else if (putData != null) { + // HTTP PUT method + conn.setRequestMethod(HTTP_PUT); + + // Set entity headers + //conn.setRequestProperty("Content-Type", "???"); + // TODO: Set Content-Type to correct type? + // TODO: Set content-encoding? Or can binary data be sent directly? + conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + + // Write put data to the stream + FileUtil.copy(new FileInputStream(putData), os); + + os.close(); + } + + // + InputStream is; + + if (conn.getResponseCode() == 200) { + + // Connect and get stream + is = conn.getInputStream(); + } + else { + is = conn.getErrorStream(); + } + + // + if (debugHeaders) { + System.err.println("Request (debug):"); + System.err.println(conn.getClass()); + System.err.println("Response (debug):"); + + // Headerfield 0 is response code + System.err.println(conn.getHeaderField(0)); + + // Loop from 1, as headerFieldKey(0) == null... + for (int i = 1; ; i++) { + String key = conn.getHeaderFieldKey(i); + + // Seems to be the way to loop through them all... + if (key == null) { + break; + } + System.err.println(key + ": " + conn.getHeaderField(key)); + } + } + + // Create output file if specified + OutputStream os; + + if (writeToFile) { + if (outFileName == null) { + outFileName = reqURL.getFile(); + if (StringUtil.isEmpty(outFileName)) { + outFileName = conn.getHeaderField("Location"); + if (StringUtil.isEmpty(outFileName)) { + outFileName = "index"; + + // Find a suitable extension + // TODO: Replace with MIME-type util with MIME/file ext mapping + String ext = conn.getContentType(); + + if (!StringUtil.isEmpty(ext)) { + int idx = ext.lastIndexOf('/'); + + if (idx >= 0) { + ext = ext.substring(idx + 1); + } + idx = ext.indexOf(';'); + if (idx >= 0) { + ext = ext.substring(0, idx); + } + outFileName += "." + ext; + } + } + } + int idx = outFileName.lastIndexOf('/'); + + if (idx >= 0) { + outFileName = outFileName.substring(idx + 1); + } + idx = outFileName.indexOf('?'); + if (idx >= 0) { + outFileName = outFileName.substring(0, idx); + } + } + File outFile = new File(outFileName); + + if (!outFile.createNewFile()) { + if (outFile.exists()) { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); + } + else { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); + } + System.exit(5); + } + os = new FileOutputStream(outFile); + } + else if (writeToStdOut) { + os = System.out; + } + else { + os = null; + } + + // Get data. + if ((writeToFile || writeToStdOut) && is != null) { + FileUtil.copy(is, os); + } + + /* + Hashtable postData = new Hashtable(); + postData.put("SearchText", "condition"); + + try { + InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, + props, true, 0); + out = new FileOutputStream(file); + FileUtil.copy(in, out); + } + catch (Exception e) { + System.err.println("Error: " + e); + e.printStackTrace(System.err); + continue; + } + */ + } + + /* + public static class Cookie { + String mName = null; + String mValue = null; + + public Cookie(String pName, String pValue) { + mName = pName; + mValue = pValue; + } + + public String toString() { + return mName + "=" + mValue; + } + */ + + /* + // Just a way to set cookies.. + if (pCookies != null) { + String cookieStr = ""; + for (int i = 0; i < pCookies.length; i++) + cookieStr += ((i == pCookies.length) ? pCookies[i].toString() + : pCookies[i].toString() + ";"); + + // System.out.println("Cookie: " + cookieStr); + + conn.setRequestProperty("Cookie", cookieStr); + } + */ + + /* + } + */ + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(String pURL) { + return ((pURL != null) && pURL.startsWith(HTTP)); + } + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(URL pURL) { + return ((pURL != null) && pURL.getProtocol().equals("http")); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws MalformedURLException if the urlName parameter is not a valid + * URL. Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static byte[] getBytesHttp(String pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * + * @param pURL the URL to get. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(String) + */ + public static byte[] getBytesHttp(URL pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws MalformedURLException if the urlName parameter specifies an + * unknown protocol, or does not form a valid URL. + * Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static InputStream getInputStreamHttp(String pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. + * + * @param pURL the URL to get. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(String) + */ + public static InputStream getInputStreamHttp(URL pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) + throws IOException { + + // Make sure we have properties + Properties properties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, properties); + + //unregisterPassword(url); + return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); + } + + /** + * Registers the password from the URL string, and returns the URL object. + * + * @param pURL the string representation of the URL, possibly including authorization part + * @param pProperties the + * @return the URL created from {@code pURL}. + * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} + */ + private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { + String url = pURL; + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + int httpIdx = url.indexOf(HTTPS); + + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + url = url.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = url.indexOf(HTTP); + if (httpIdx >= 0) { + url = url.substring(httpIdx + HTTP.length()); + } + } + + // Get authorization part + int atIdx = url.indexOf("@"); + + if (atIdx >= 0) { + userPass = url.substring(0, atIdx); + url = url.substring(atIdx + 1); + } + + // Set authorization if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); + } + + // Return URL + return new URL(protocolPrefix + url); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP GET method + conn.setRequestMethod(HTTP_GET); + + // This is where the connect happens + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + pProperties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, pProperties); + + //unregisterPassword(url); + return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + // Open the connection, and get the stream + HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Iterate over and create post data string + StringBuilder postStr = new StringBuilder(); + + if (pPostData != null) { + Iterator data = pPostData.entrySet().iterator(); + + while (data.hasNext()) { + Map.Entry entry = (Map.Entry) data.next(); + + // Properties key/values can be safely cast to strings + // Encode the string + postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); + postStr.append('='); + postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + + if (data.hasNext()) { + postStr.append('&'); + } + } + } + + // Set entity headers + String encoding = conn.getRequestProperty("Content-Encoding"); + if (StringUtil.isEmpty(encoding)) { + encoding = "UTF-8"; + } + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); + conn.setRequestProperty("Content-Encoding", encoding); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(os, encoding); + + // Write post data to the stream + writer.write(postStr.toString()); + writer.write("\r\n"); + writer.close(); // Does this close the underlying stream? + + // Get the inputstream + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Creates a HTTP connection to the given URL. + * + * @param pURL the URL to get. + * @param pProperties connection properties. + * @param pFollowRedirects specifies whether we should follow redirects. + * @param pTimeout the specified timeout, in milliseconds. + * @return a HttpURLConnection + * @throws UnknownHostException if the hostname in the URL cannot be found. + * @throws IOException if an I/O exception occurs. + */ + public static HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + HttpURLConnection conn; + + if (pTimeout > 0) { + // Supports timeout + conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); + } + else { + // Faster, more compatible + conn = (HttpURLConnection) pURL.openConnection(); + } + + // Set user agent + if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { + conn.setRequestProperty("User-Agent", + VERSION_ID + + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " + + System.getProperty("os.arch") + "; " + + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); + } + + // Set request properties + if (pProperties != null) { + for (Map.Entry entry : pProperties.entrySet()) { + // Properties key/values can be safely cast to strings + conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); + } + } + + try { + // Breaks with JRE1.2? + conn.setInstanceFollowRedirects(pFollowRedirects); + } + catch (LinkageError le) { + // This is the best we can do... + HttpURLConnection.setFollowRedirects(pFollowRedirects); + System.err.println("You are using an old Java Spec, consider upgrading."); + System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); + + //le.printStackTrace(System.err); + } + + conn.setDoInput(true); + conn.setDoOutput(true); + + //conn.setUseCaches(true); + return conn; + } + + /** + * This is a hack to get around the protected constructors in + * HttpURLConnection, should maybe consider registering and do things + * properly... + */ + + /* + private static class TimedHttpURLConnection + extends com.twelvemonkeys.net.HttpURLConnection { + TimedHttpURLConnection(URL pURL, int pTimeout) { + super(pURL, pTimeout); + } + } + */ + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return a byte array that is read from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Unregisters the password asscociated with this URL + */ + + /* + private static void unregisterPassword(URL pURL) { + Authenticator auth = registerAuthenticator(); + if (auth != null && auth instanceof SimpleAuthenticator) + ((SimpleAuthenticator) auth) + .unregisterPasswordAuthentication(pURL); + } + */ + + /** + * Registers the password from the URL string, and returns the URL object. + */ + + /* + private static URL getURLAndRegisterPassword(String pURL) + throws MalformedURLException + { + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + + int httpIdx = pURL.indexOf(HTTPS); + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + pURL = pURL.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = pURL.indexOf(HTTP); + if (httpIdx >= 0) + pURL = pURL.substring(httpIdx + HTTP.length()); + } + + int atIdx = pURL.indexOf("@"); + if (atIdx >= 0) { + userPass = pURL.substring(0, atIdx); + pURL = pURL.substring(atIdx + 1); + } + + // Set URL + URL url = new URL(protocolPrefix + pURL); + + // Set Authenticator if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + + int colIdx = userPass.indexOf(":"); + if (colIdx < 0) + throw new MalformedURLException("Error in username/password!"); + + String userName = userPass.substring(0, colIdx); + String passWord = userPass.substring(colIdx + 1); + + // Try to register the authenticator + // System.out.println("Trying to register authenticator!"); + Authenticator auth = registerAuthenticator(); + + // System.out.println("Got authenticator " + auth + "."); + + // Register our username/password with it + if (auth != null && auth instanceof SimpleAuthenticator) { + ((SimpleAuthenticator) auth) + .registerPasswordAuthentication(url, + new PasswordAuthentication(userName, + passWord.toCharArray())); + } + else { + // Not supported! + throw new RuntimeException("Could not register PasswordAuthentication"); + } + } + + return url; + } + */ + + /** + * Registers the Authenticator given in the system property + * {@code java.net.Authenticator}, or the default implementation + * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). + *

+ * BUG: What if authenticator has allready been set outside this class? + * + * @return The Authenticator created and set as default, or null, if it + * was not set as the default. However, there is no (clean) way to + * be sure the authenticator was set (the SimpleAuthenticator uses + * a hack to get around this), so it might be possible that the + * returned authenticator was not set as default... + * @see Authenticator#setDefault(Authenticator) + * @see SimpleAuthenticator + */ + public synchronized static Authenticator registerAuthenticator() { + if (sAuthenticator != null) { + return sAuthenticator; + } + + // Get the system property + String authenticatorName = System.getProperty("java.net.Authenticator"); + + // Try to get the Authenticator from the system property + if (authenticatorName != null) { + try { + Class authenticatorClass = Class.forName(authenticatorName); + + sAuthenticator = (Authenticator) authenticatorClass.newInstance(); + } + catch (ClassNotFoundException cnfe) { + // We should maybe rethrow this? + } + catch (InstantiationException ie) { + // Ignore + } + catch (IllegalAccessException iae) { + // Ignore + } + } + + // Get the default authenticator + if (sAuthenticator == null) { + sAuthenticator = SimpleAuthenticator.getInstance(); + } + + // Register authenticator as default + Authenticator.setDefault(sAuthenticator); + return sAuthenticator; + } + + /** + * Creates the InetAddress object from the given URL. + * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} + * except that it returns null, instead of throwing UnknownHostException. + * + * @param pURL the URL to look up. + * @return the createad InetAddress, or null if the host was unknown. + * @see java.net.InetAddress + * @see java.net.URL + */ + public static InetAddress createInetAddressFromURL(URL pURL) { + try { + return InetAddress.getByName(pURL.getHost()); + } + catch (UnknownHostException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using the given + * protocol. + * Equivalent to calling + * {@code new URL(protocol, InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @param pProtocol the protocol to use in the new URL + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { + try { + return new URL(pProtocol, pIP.getHostName(), ""); + } + catch (MalformedURLException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using HTTP protocol. + * Equivalent to calling + * {@code new URL("http", InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP) { + return createURLFromInetAddress(pIP, HTTP); + } + + /* + * TODO: Benchmark! + */ + static byte[] getBytesHttpOld(String pURL) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); + + // Get all the bytes in loop + byte[] bytes = new byte[0]; + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes; + } } \ No newline at end of file diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java similarity index 95% rename from common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java index 3bbebcbb..81c68f66 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java @@ -1,45 +1,45 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 COPYRIGHT OWNER 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. - */ - -package com.twelvemonkeys.net; - -import java.net.*; - -/** - * Interface fro PasswordAuthenticators used by SimpleAuthenticator. - * - * @see SimpleAuthenticator - * @see java.net.Authenticator - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * - * @version 1.0 - */ -public interface PasswordAuthenticator { - public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); -} +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface fro PasswordAuthenticators used by SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr + * + * @version 1.0 + */ +public interface PasswordAuthenticator { + public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java similarity index 97% rename from common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java rename to sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java index d036fb2b..a7830581 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java +++ b/sandbox/sandbox-common/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java @@ -1,270 +1,270 @@ -/* - * Copyright (c) 2008, Harald Kuhr - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * * Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * * 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. - * * Neither the name "TwelveMonkeys" nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "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 COPYRIGHT OWNER 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. - */ - -package com.twelvemonkeys.net; - -import com.twelvemonkeys.lang.Validate; - -import java.net.Authenticator; -import java.net.InetAddress; -import java.net.PasswordAuthentication; -import java.net.URL; -import java.util.HashMap; -import java.util.Map; - -/** - * A simple Authenticator implementation. - * Singleton class, obtain reference through the static - * {@code getInstance} method. - *

- * After swearing, sweating, pulling my hair, banging my head repeatedly - * into the walls and reading the java.net.Authenticator API documentation - * once more, an idea came to my mind. This is the result. I hope you find it - * useful. -- Harald K. - * - * @author Harald Kuhr (haraldk@iconmedialab.no) - * @version 1.0 - * @see java.net.Authenticator - */ -public class SimpleAuthenticator extends Authenticator { - - /** The reference to the single instance of this class. */ - private static SimpleAuthenticator sInstance = null; - /** Keeps track of the state of this class. */ - private static boolean sInitialized = false; - - // These are used for the identification hack. - private final static String MAGIC = "magic"; - private final static int FOURTYTWO = 42; - - /** Basic authentication scheme. */ - public final static String BASIC = "Basic"; - - /** The hastable that keeps track of the PasswordAuthentications. */ - protected Map passwordAuthentications = null; - - /** The hastable that keeps track of the Authenticators. */ - protected Map authenticators = null; - - /** Creates a SimpleAuthenticator. */ - private SimpleAuthenticator() { - passwordAuthentications = new HashMap(); - authenticators = new HashMap(); - } - - /** - * Gets the SimpleAuthenticator instance and registers it through the - * Authenticator.setDefault(). If there is no current instance - * of the SimpleAuthenticator in the VM, one is created. This method will - * try to figure out if the setDefault() succeeded (a hack), and will - * return null if it was not able to register the instance as default. - * - * @return The single instance of this class, or null, if another - * Authenticator is allready registered as default. - */ - public static synchronized SimpleAuthenticator getInstance() { - if (!sInitialized) { - // Create an instance - sInstance = new SimpleAuthenticator(); - - // Try to set default (this may quietly fail...) - Authenticator.setDefault(sInstance); - - // A hack to figure out if we really did set the authenticator - PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); - - // If this test returns false, we didn't succeed, so we set the - // instance back to null. - if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { - sInstance = null; - } - - // Done - sInitialized = true; - } - - return sInstance; - } - - /** - * Gets the PasswordAuthentication for the request. Called when password - * authorization is needed. - * - * @return The PasswordAuthentication collected from the user, or null if - * none is provided. - */ - protected PasswordAuthentication getPasswordAuthentication() { - // Don't worry, this is just a hack to figure out if we were able - // to set this Authenticator through the setDefault method. - if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { - return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); - } - /* - System.err.println("getPasswordAuthentication"); - System.err.println(getRequestingSite()); - System.err.println(getRequestingPort()); - System.err.println(getRequestingProtocol()); - System.err.println(getRequestingPrompt()); - System.err.println(getRequestingScheme()); - */ - - // TODO: - // Look for a more specific PasswordAuthenticatior before using - // Default: - // - // if (...) - // return pa.requestPasswordAuthentication(getRequestingSite(), - // getRequestingPort(), - // getRequestingProtocol(), - // getRequestingPrompt(), - // getRequestingScheme()); - - return passwordAuthentications.get(new AuthKey(getRequestingSite(), - getRequestingPort(), - getRequestingProtocol(), - getRequestingPrompt(), - getRequestingScheme())); - } - - /** Registers a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { - return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), - pURL.getPort(), - pURL.getProtocol(), - null, // Prompt/Realm - BASIC, - pPA); - } - - /** Registers a PasswordAuthentication with a given net address. */ - public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { - /* - System.err.println("registerPasswordAuthentication"); - System.err.println(pAddress); - System.err.println(pPort); - System.err.println(pProtocol); - System.err.println(pPrompt); - System.err.println(pScheme); - */ - - return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); - } - - /** Unregisters a PasswordAuthentication with a given URL address. */ - public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { - return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); - } - - /** Unregisters a PasswordAuthentication with a given net address. */ - public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); - } - - /** - * TODO: Registers a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { - authenticators.put(pPA, pFilter); - } - - /** - * TODO: Unregisters a PasswordAuthenticator that can answer authentication - * requests. - * - * @see PasswordAuthenticator - */ - public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { - authenticators.remove(pPA); - } -} - -/** - * Utility class, used for caching the PasswordAuthentication objects. - * Everything but address may be null - */ -class AuthKey { - - InetAddress address = null; - int port = -1; - String protocol = null; - String prompt = null; - String scheme = null; - - AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { - Validate.notNull(pAddress, "address"); - - address = pAddress; - port = pPort; - protocol = pProtocol; - prompt = pPrompt; - scheme = pScheme; - - // System.out.println("Created: " + this); - } - - /** Creates a string representation of this object. */ - - public String toString() { - return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; - } - - public boolean equals(Object pObj) { - return (pObj instanceof AuthKey && equals((AuthKey) pObj)); - } - - // Ahem.. Breaks the rule from Object.equals(Object): - // It is transitive: for any reference values x, y, and z, if x.equals(y) - // returns true and y.equals(z) returns true, then x.equals(z) - // should return true. - - public boolean equals(AuthKey pKey) { - // Maybe allow nulls, and still be equal? - return (address.equals(pKey.address) - && (port == -1 - || pKey.port == -1 - || port == pKey.port) - && (protocol == null - || pKey.protocol == null - || protocol.equals(pKey.protocol)) - && (prompt == null - || pKey.prompt == null - || prompt.equals(pKey.prompt)) - && (scheme == null - || pKey.scheme == null - || scheme.equalsIgnoreCase(pKey.scheme))); - } - - public int hashCode() { - // There won't be too many pr address, will it? ;-) - return address.hashCode(); - } -} - +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * 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. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "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 COPYRIGHT OWNER 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. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.Validate; + +import java.net.Authenticator; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * A simple Authenticator implementation. + * Singleton class, obtain reference through the static + * {@code getInstance} method. + *

+ * After swearing, sweating, pulling my hair, banging my head repeatedly + * into the walls and reading the java.net.Authenticator API documentation + * once more, an idea came to my mind. This is the result. I hope you find it + * useful. -- Harald K. + * + * @author Harald Kuhr + * @version 1.0 + * @see java.net.Authenticator + */ +public class SimpleAuthenticator extends Authenticator { + /** The reference to the single instance of this class. */ + private static SimpleAuthenticator sInstance = null; + /** Keeps track of the state of this class. */ + private static boolean sInitialized = false; + + // These are used for the identification hack. + private final static String MAGIC = "magic"; + private final static int FOURTYTWO = 42; + + /** Basic authentication scheme. */ + public final static String BASIC = "Basic"; + + /** The hastable that keeps track of the PasswordAuthentications. */ + protected Map passwordAuthentications = null; + + /** The hastable that keeps track of the Authenticators. */ + protected Map authenticators = null; + + /** Creates a SimpleAuthenticator. */ + private SimpleAuthenticator() { + passwordAuthentications = new HashMap(); + authenticators = new HashMap(); + } + + /** + * Gets the SimpleAuthenticator instance and registers it through the + * Authenticator.setDefault(). If there is no current instance + * of the SimpleAuthenticator in the VM, one is created. This method will + * try to figure out if the setDefault() succeeded (a hack), and will + * return null if it was not able to register the instance as default. + * + * @return The single instance of this class, or null, if another + * Authenticator is allready registered as default. + */ + public static synchronized SimpleAuthenticator getInstance() { + if (!sInitialized) { + // Create an instance + sInstance = new SimpleAuthenticator(); + + // Try to set default (this may quietly fail...) + Authenticator.setDefault(sInstance); + + // A hack to figure out if we really did set the authenticator + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(null, FOURTYTWO, null, null, MAGIC); + + // If this test returns false, we didn't succeed, so we set the + // instance back to null. + if (pa == null || !MAGIC.equals(pa.getUserName()) || !("" + FOURTYTWO).equals(new String(pa.getPassword()))) { + sInstance = null; + } + + // Done + sInitialized = true; + } + + return sInstance; + } + + /** + * Gets the PasswordAuthentication for the request. Called when password + * authorization is needed. + * + * @return The PasswordAuthentication collected from the user, or null if + * none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + // Don't worry, this is just a hack to figure out if we were able + // to set this Authenticator through the setDefault method. + if (!sInitialized && MAGIC.equals(getRequestingScheme()) && getRequestingPort() == FOURTYTWO) { + return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO).toCharArray()); + } + /* + System.err.println("getPasswordAuthentication"); + System.err.println(getRequestingSite()); + System.err.println(getRequestingPort()); + System.err.println(getRequestingProtocol()); + System.err.println(getRequestingPrompt()); + System.err.println(getRequestingScheme()); + */ + + // TODO: + // Look for a more specific PasswordAuthenticatior before using + // Default: + // + // if (...) + // return pa.requestPasswordAuthentication(getRequestingSite(), + // getRequestingPort(), + // getRequestingProtocol(), + // getRequestingPrompt(), + // getRequestingScheme()); + + return passwordAuthentications.get(new AuthKey(getRequestingSite(), + getRequestingPort(), + getRequestingProtocol(), + getRequestingPrompt(), + getRequestingScheme())); + } + + /** Registers a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication registerPasswordAuthentication(URL pURL, PasswordAuthentication pPA) { + return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, // Prompt/Realm + BASIC, + pPA); + } + + /** Registers a PasswordAuthentication with a given net address. */ + public PasswordAuthentication registerPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme, PasswordAuthentication pPA) { + /* + System.err.println("registerPasswordAuthentication"); + System.err.println(pAddress); + System.err.println(pPort); + System.err.println(pProtocol); + System.err.println(pPrompt); + System.err.println(pScheme); + */ + + return passwordAuthentications.put(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme), pPA); + } + + /** Unregisters a PasswordAuthentication with a given URL address. */ + public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { + return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), pURL.getPort(), pURL.getProtocol(), null, BASIC); + } + + /** Unregisters a PasswordAuthentication with a given net address. */ + public PasswordAuthentication unregisterPasswordAuthentication(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + return passwordAuthentications.remove(new AuthKey(pAddress, pPort, pProtocol, pPrompt, pScheme)); + } + + /** + * TODO: Registers a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void registerPasswordAuthenticator(PasswordAuthenticator pPA, AuthenticatorFilter pFilter) { + authenticators.put(pPA, pFilter); + } + + /** + * TODO: Unregisters a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { + authenticators.remove(pPA); + } +} + +/** + * Utility class, used for caching the PasswordAuthentication objects. + * Everything but address may be null + */ +class AuthKey { + // TODO: Move this class to sandbox? + + InetAddress address = null; + int port = -1; + String protocol = null; + String prompt = null; + String scheme = null; + + AuthKey(InetAddress pAddress, int pPort, String pProtocol, String pPrompt, String pScheme) { + Validate.notNull(pAddress, "address"); + + address = pAddress; + port = pPort; + protocol = pProtocol; + prompt = pPrompt; + scheme = pScheme; + + // System.out.println("Created: " + this); + } + + /** Creates a string representation of this object. */ + + public String toString() { + return "AuthKey[" + address + ":" + port + "/" + protocol + " \"" + prompt + "\" (" + scheme + ")]"; + } + + public boolean equals(Object pObj) { + return (pObj instanceof AuthKey && equals((AuthKey) pObj)); + } + + // Ahem.. Breaks the rule from Object.equals(Object): + // It is transitive: for any reference values x, y, and z, if x.equals(y) + // returns true and y.equals(z) returns true, then x.equals(z) + // should return true. + + public boolean equals(AuthKey pKey) { + // Maybe allow nulls, and still be equal? + return (address.equals(pKey.address) + && (port == -1 + || pKey.port == -1 + || port == pKey.port) + && (protocol == null + || pKey.protocol == null + || protocol.equals(pKey.protocol)) + && (prompt == null + || pKey.prompt == null + || prompt.equals(pKey.prompt)) + && (scheme == null + || pKey.scheme == null + || scheme.equalsIgnoreCase(pKey.scheme))); + } + + public int hashCode() { + // There won't be too many pr address, will it? ;-) + return address.hashCode(); + } +} + diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java index ee567d8e..719bb378 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; import javax.servlet.ServletOutputStream; @@ -212,7 +212,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.setDateHeader(pName, pValue); } - cachedResponse.setHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void addDateHeader(String pName, long pValue) { @@ -220,7 +220,7 @@ class CacheResponseWrapper extends HttpServletResponseWrapper { if (Boolean.FALSE.equals(cacheable)) { super.addDateHeader(pName, pValue); } - cachedResponse.addHeader(pName, NetUtil.formatHTTPDate(pValue)); + cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); } public void setHeader(String pName, String pValue) { diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java index 1bae14e4..9d73e8b2 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -32,7 +32,7 @@ import com.twelvemonkeys.io.FileUtil; import com.twelvemonkeys.lang.StringUtil; import com.twelvemonkeys.lang.Validate; import com.twelvemonkeys.net.MIMEUtil; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import com.twelvemonkeys.util.LRUHashMap; import com.twelvemonkeys.util.NullMap; @@ -972,7 +972,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -981,11 +981,11 @@ public class HTTPCache { int maxAge = getIntHeader(response, HEADER_CACHE_CONTROL, "max-age"); if (maxAge == -1) { expires = lastModified + defaultExpiryTime; - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); } else { expires = lastModified + (maxAge * 1000L); // max-age is seconds - //// System.out.println(" ## HTTPCache ## Expires is " + NetUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); } } /* @@ -997,7 +997,7 @@ public class HTTPCache { // Expired? if (expires < now) { // System.out.println(" ## HTTPCache ## Content is stale (content expired: " - // + NetUtil.formatHTTPDate(expires) + " before " + NetUtil.formatHTTPDate(now) + ")."); + // + HTTPUtil.formatHTTPDate(expires) + " before " + HTTPUtil.formatHTTPDate(now) + ")."); return true; } @@ -1008,7 +1008,7 @@ public class HTTPCache { File cached = getCachedFile(pCacheURI, pRequest); if (cached != null && cached.exists()) { lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + NetUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); } } */ @@ -1018,7 +1018,7 @@ public class HTTPCache { //noinspection RedundantIfStatement if (real != null && real.exists() && real.lastModified() > lastModified) { // System.out.println(" ## HTTPCache ## Content is stale (new content" - // + NetUtil.formatHTTPDate(lastModified) + " before " + NetUtil.formatHTTPDate(real.lastModified()) + ")."); + // + HTTPUtil.formatHTTPDate(lastModified) + " before " + HTTPUtil.formatHTTPDate(real.lastModified()) + ")."); return true; } @@ -1082,7 +1082,7 @@ public class HTTPCache { static long getDateHeader(final String pHeaderValue) { long date = -1L; if (pHeaderValue != null) { - date = NetUtil.parseHTTPDate(pHeaderValue); + date = HTTPUtil.parseHTTPDate(pHeaderValue); } return date; } diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java index fd39747c..c73ebf99 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/WritableCachedResponseImpl.java @@ -29,7 +29,7 @@ package com.twelvemonkeys.servlet.cache; import com.twelvemonkeys.io.FastByteArrayOutputStream; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -53,7 +53,7 @@ class WritableCachedResponseImpl implements WritableCachedResponse { protected WritableCachedResponseImpl() { cachedResponse = new CachedResponseImpl(); // Hmmm.. - setHeader(HTTPCache.HEADER_CACHED_TIME, NetUtil.formatHTTPDate(System.currentTimeMillis())); + setHeader(HTTPCache.HEADER_CACHED_TIME, HTTPUtil.formatHTTPDate(System.currentTimeMillis())); } public CachedResponse getCachedResponse() { diff --git a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java index b5b69f45..dbe759aa 100755 --- a/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java +++ b/servlet/src/test/java/com/twelvemonkeys/servlet/cache/HTTPCacheTestCase.java @@ -1,6 +1,6 @@ package com.twelvemonkeys.servlet.cache; -import com.twelvemonkeys.net.NetUtil; +import com.twelvemonkeys.net.HTTPUtil; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; @@ -644,7 +644,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(HttpServletResponse.SC_OK); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); res.setHeader("Cache-Control", "public"); res.addHeader("X-Custom", "FOO"); res.addHeader("X-Custom", "BAR"); @@ -1126,7 +1126,7 @@ public class HTTPCacheTestCase { CacheResponse res = (CacheResponse) invocation.getArguments()[1]; res.setStatus(status); - res.setHeader("Date", NetUtil.formatHTTPDate(System.currentTimeMillis())); + res.setHeader("Date", HTTPUtil.formatHTTPDate(System.currentTimeMillis())); for (Map.Entry> header : headers.entrySet()) { for (String value : header.getValue()) {