diff --git a/pom.xml b/pom.xml index e944531..fc994cb 100644 --- a/pom.xml +++ b/pom.xml @@ -1,17 +1,15 @@ - + 4.0.0 org.jboss.com.sun.httpserver httpserver - 1.0.1.Final + 1.0.9.Final-SNAPSHOT org.jboss jboss-parent - 5 + 16 Lightweight HTTP Server @@ -47,4 +45,11 @@ + + + scm:git:https://github.com/jbossas/httpserver.git + scm:git:git@github.com:jbossas/httpserver.git + https://github.com/jbossas/httpserver + HEAD + diff --git a/src/main/java/org/jboss/com/sun/net/httpserver/BasicAuthenticator.java b/src/main/java/org/jboss/com/sun/net/httpserver/BasicAuthenticator.java index f2f301e..348e926 100644 --- a/src/main/java/org/jboss/com/sun/net/httpserver/BasicAuthenticator.java +++ b/src/main/java/org/jboss/com/sun/net/httpserver/BasicAuthenticator.java @@ -25,6 +25,12 @@ package org.jboss.com.sun.net.httpserver; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + /** * BasicAuthenticator provides an implementation of HTTP Basic * authentication. It is an abstract class and must be extended @@ -33,15 +39,35 @@ */ public abstract class BasicAuthenticator extends Authenticator { + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); protected String realm; + private Map browserCharsetMap; + private Charset defaultCharset; + /** * Creates a BasicAuthenticator for the given HTTP realm * @param realm The HTTP Basic authentication realm * @throws NullPointerException if the realm is an empty string */ public BasicAuthenticator (String realm) { + this(realm, DEFAULT_CHARSET, Collections.emptyMap()); + } + + /** + * Creates a BasicAuthenticator for the given HTTP realm. + * + * browserCharsetMap is used to specify a character encoding used to decode BASIC authentication response depending + * on the browser that issued it. + * + * @param realm The HTTP Basic authentication realm + * @param defaultCharset charset that should be used to decode credentials if the user agent is not listed in browserCharsetMap + * @param browserCharsetMap map indexed by Patterns representing User-Agent strings with the charset as values + */ + public BasicAuthenticator (String realm, Charset defaultCharset, Map browserCharsetMap) { this.realm = realm; + this.defaultCharset = defaultCharset; + this.browserCharsetMap = Collections.unmodifiableMap(new HashMap(browserCharsetMap)); } /** @@ -69,7 +95,20 @@ public Result authenticate (HttpExchange t) return new Authenticator.Failure (401); } byte[] b = Base64.base64ToByteArray (auth.substring(sp+1)); - String userpass = new String (b); + + Charset charset = DEFAULT_CHARSET; + if (!browserCharsetMap.isEmpty()) { + String userAgent = rmap.getFirst("User-Agent"); + if (userAgent != null) { + for (Map.Entry entry : browserCharsetMap.entrySet()) { + if (entry.getKey().matcher(userAgent).matches()) { + charset = entry.getValue(); + } + } + } + } + + String userpass = new String (b, charset); int colon = userpass.indexOf (':'); String uname = userpass.substring (0, colon); String pass = userpass.substring (colon+1); diff --git a/src/main/java/org/jboss/sun/net/httpserver/ExchangeImpl.java b/src/main/java/org/jboss/sun/net/httpserver/ExchangeImpl.java index 9096c76..2109a0e 100644 --- a/src/main/java/org/jboss/sun/net/httpserver/ExchangeImpl.java +++ b/src/main/java/org/jboss/sun/net/httpserver/ExchangeImpl.java @@ -115,7 +115,7 @@ class ExchangeImpl { } public Headers getRequestHeaders () { - return new UnmodifiableHeaders (reqHdrs); + return reqHdrs; } public Headers getResponseHeaders () { @@ -137,7 +137,7 @@ public HttpContextImpl getHttpContext (){ private boolean isHeadRequest() { return HEAD.equals(getRequestMethod()); } - + public ServerConfig getServerConfig() { return server.getServerConfig(); } diff --git a/src/main/java/org/jboss/sun/net/httpserver/Request.java b/src/main/java/org/jboss/sun/net/httpserver/Request.java index cc409b3..52707fd 100644 --- a/src/main/java/org/jboss/sun/net/httpserver/Request.java +++ b/src/main/java/org/jboss/sun/net/httpserver/Request.java @@ -178,6 +178,10 @@ Headers headers () throws IOException { c = ' '; break; } + if (s.length >= ServerConfig.getMaxReqHeaderSize()) { + throw new IOException("Maximum size of request header (" + + "sun.net.httpserver.maxReqHeaderSize) exceeded, " + ServerConfig.getMaxReqHeaderSize() + "."); + } if (len >= s.length) { char ns[] = new char[s.length * 2]; System.arraycopy(s, 0, ns, 0, len); @@ -205,7 +209,13 @@ Headers headers () throws IOException { v = new String(); else v = String.copyValueOf(s, keyend, len - keyend); - hdrs.add (k,v); + + if (hdrs.size() >= ServerConfig.getMaxReqHeaders()) { + throw new IOException("Maximum number of request headers (" + "sun.net.httpserver.maxReqHeaders) exceeded, " + + ServerConfig.getMaxReqHeaders() + "."); + } + + hdrs.add(k, v); len = 0; } return hdrs; diff --git a/src/main/java/org/jboss/sun/net/httpserver/SSLStreams.java b/src/main/java/org/jboss/sun/net/httpserver/SSLStreams.java index 94cb90c..6e9f069 100644 --- a/src/main/java/org/jboss/sun/net/httpserver/SSLStreams.java +++ b/src/main/java/org/jboss/sun/net/httpserver/SSLStreams.java @@ -70,7 +70,8 @@ class SSLStreams { this.chan= chan; InetSocketAddress addr = (InetSocketAddress)chan.socket().getRemoteSocketAddress(); - engine = sslctx.createSSLEngine (addr.getHostName(), addr.getPort()); + // This is the server side of the connection so we do not need to hint as to the clients address. + engine = sslctx.createSSLEngine (); engine.setUseClientMode (false); HttpsConfigurator cfg = server.getHttpsConfigurator(); configureEngine (cfg, addr); @@ -96,8 +97,12 @@ private void configureEngine(HttpsConfigurator cfg, InetSocketAddress addr){ ); } catch (IllegalArgumentException e) { /* LOG */} } - engine.setNeedClientAuth (params.getNeedClientAuth()); - engine.setWantClientAuth (params.getWantClientAuth()); + if (params.getNeedClientAuth()) { + engine.setNeedClientAuth (true); + } + if (params.getWantClientAuth()) { + engine.setWantClientAuth (true); + } if (params.getProtocols() != null) { try { engine.setEnabledProtocols ( @@ -518,7 +523,7 @@ public int read (byte[] buf, int off, int len) throws IOException { throw new IOException ("SSL stream is closed"); } if (eof) { - return 0; + return -1; } int available=0; if (!needData) { @@ -531,7 +536,7 @@ public int read (byte[] buf, int off, int len) throws IOException { bbuf = r.buf== bbuf? bbuf: r.buf; if ((available=bbuf.remaining()) == 0) { eof = true; - return 0; + return -1; } else { needData = false; } @@ -562,7 +567,7 @@ public long skip (long s) throws IOException { throw new IOException ("SSL stream is closed"); } if (eof) { - return 0; + return -1; } int ret = n; while (n > 0) { @@ -597,7 +602,7 @@ public int read (byte[] buf) throws IOException { public int read () throws IOException { int n = read (single, 0, 1); - if (n == 0) { + if (n == 0 || n == -1) { return -1; } else { return single[0] & 0xFF; diff --git a/src/main/java/org/jboss/sun/net/httpserver/ServerConfig.java b/src/main/java/org/jboss/sun/net/httpserver/ServerConfig.java index a3ef60d..56e48fb 100644 --- a/src/main/java/org/jboss/sun/net/httpserver/ServerConfig.java +++ b/src/main/java/org/jboss/sun/net/httpserver/ServerConfig.java @@ -40,7 +40,7 @@ class ServerConfig { static final int DEFAULT_CLOCK_TICK = 10000; // 10 sec. /* These values must be a reasonable multiple of clockTick */ - static final long DEFAULT_IDLE_INTERVAL = 30; // 5 min + static final long DEFAULT_IDLE_INTERVAL = 300; // 5 min i.e. 300 seconds. static final int DEFAULT_MAX_IDLE_CONNECTIONS = 200; static final long DEFAULT_MAX_REQ_TIME = -1; // default: forever @@ -48,11 +48,18 @@ class ServerConfig { static final long DEFAULT_TIMER_MILLIS = 1000; static final long DEFAULT_DRAIN_AMOUNT = 64 * 1024; + static final long DEFAULT_MAX_REQ_HEADER_SIZE = 1024 * 1024; + static final int DEFAULT_MAX_REQ_HEADERS = 200; final long idleInterval; final long drainAmount; // max # of bytes to drain from an inputstream final int maxIdleConnections; + // The maximum size of request header allowable + private static long maxReqHeaderSize; + // The maximum number of request headers allowable + private static long maxReqHeaders; + // max time a request or response is allowed to take final long maxReqTime; final long maxRspTime; @@ -68,6 +75,8 @@ public ServerConfig(Map configuration) { clockTick = getIntegerProperty(configuration, "sun.net.httpserver.clockTick", DEFAULT_CLOCK_TICK); maxIdleConnections = getIntegerProperty(configuration, "sun.net.httpserver.maxIdleConnections", DEFAULT_MAX_IDLE_CONNECTIONS); drainAmount = getLongProperty(configuration, "sun.net.httpserver.drainAmount", DEFAULT_DRAIN_AMOUNT); + maxReqHeaderSize = getLongProperty(configuration, "sun.net.httpserver.maxReqHeaderSize", DEFAULT_MAX_REQ_HEADER_SIZE); + maxReqHeaders = getLongProperty(configuration, "sun.net.httpserver.maxReqHeaders", DEFAULT_MAX_REQ_HEADERS); maxReqTime = getLongProperty(configuration, "sun.net.httpserver.maxReqTime", DEFAULT_MAX_REQ_TIME); maxRspTime = getLongProperty(configuration, "sun.net.httpserver.maxRspTime", DEFAULT_MAX_RSP_TIME); timerMillis = getLongProperty(configuration, "sun.net.httpserver.timerMillis", DEFAULT_TIMER_MILLIS); @@ -117,6 +126,14 @@ long getDrainAmount() { return drainAmount; } + static long getMaxReqHeaderSize() { + return maxReqHeaderSize; + } + + static long getMaxReqHeaders() { + return maxReqHeaders; + } + long getMaxReqTime() { return maxReqTime; } diff --git a/src/test/java/org/jboss/com/sun/net/httpserver/BZ1312064.java b/src/test/java/org/jboss/com/sun/net/httpserver/BZ1312064.java new file mode 100644 index 0000000..f9c1b2b --- /dev/null +++ b/src/test/java/org/jboss/com/sun/net/httpserver/BZ1312064.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2005, 2006, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package org.jboss.com.sun.net.httpserver; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Pattern; + +public class BZ1312064 { + + private static HttpServer server; + private static ExecutorService executor; + private static SimpleAuthenticator authenticator; + private static Map browserCharsetMap = new HashMap(); + + static { + browserCharsetMap.put(Pattern.compile(".*Firefox.*"), Charset.forName("8859_1")); + } + + // set up one server instance for all tests to speed things up + @BeforeClass + public static void setUpServer() throws Exception { + Handler handler = new Handler(); + InetSocketAddress addr = new InetSocketAddress (0); + server = HttpServer.create (addr, 0); + HttpContext ctx = server.createContext ("/test", handler); + + authenticator = new SimpleAuthenticator(); + ctx.setAuthenticator (authenticator); + executor = Executors.newCachedThreadPool(); + server.setExecutor (executor); + server.start (); + } + + @AfterClass + public static void shutDownServer() { + server.stop(2); + executor.shutdown(); + } + + @After + public void cleanUpAllowedCredentials() { + authenticator.purge(); + } + + @Test + public void testASCIIPassword() throws Exception { + authenticator.accept("fred", "xyz"); + + final int responseCode = makeCall("fred", "xyz", null, "UTF-8"); + + Assert.assertEquals(HttpURLConnection.HTTP_OK, responseCode); + } + + @Test + public void testNonAsciiPasswordOnUtf8Browser() throws Exception { + authenticator.accept("fred", "test123!ü"); + + final int responseCode = makeCall("fred", "test123!ü", "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36", "UTF-8"); + + Assert.assertEquals(HttpURLConnection.HTTP_OK, responseCode); + } + + @Test + public void testNonAsciiPasswordOnIso8859Browser() throws Exception { + authenticator.accept("fred", "test123!ü"); + + final int responseCode = makeCall("fred", "test123!ü", "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", "8859_1"); + + Assert.assertEquals(HttpURLConnection.HTTP_OK, responseCode); + } + + private int makeCall(String username, String password, String userAgent, String encoding) throws IOException { + URL url = new URL ("http://localhost:"+server.getAddress().getPort()+"/test/foo.html"); + HttpURLConnection urlc = (HttpURLConnection)url.openConnection (); + + final String encodedCredentials = Base64.byteArrayToBase64((username + ":" + password).getBytes(encoding)); + urlc.addRequestProperty("Authorization", "Basic " + encodedCredentials); + if (userAgent != null) { + urlc.addRequestProperty("User-Agent", userAgent); + } + urlc.setRequestMethod("GET"); + + return urlc.getResponseCode(); + } + + public static boolean error = false; + + + static class SimpleAuthenticator extends BasicAuthenticator { + private Map acceptedCredentials = new HashMap(); + + SimpleAuthenticator() { + super ("foobar@test.realm", Charset.forName("UTF-8"), BZ1312064.browserCharsetMap); + } + + public boolean checkCredentials (String username, String pw) { + return acceptedCredentials.containsKey(username) && acceptedCredentials.get(username).equals(pw); + } + + public void accept(String username, String password) { + acceptedCredentials.put(username, password); + } + + public void purge() { + acceptedCredentials.clear(); + } + } + + static class Handler implements HttpHandler { + public void handle (HttpExchange t) + throws IOException + { + t.sendResponseHeaders (200, -1); + t.close(); + } + } +}