8245095: Implementation of JEP 408: Simple Web Server

Co-authored-by: Julia Boes <jboes@openjdk.org>
Co-authored-by: Chris Hegarty <chegar@openjdk.org>
Co-authored-by: Michael McMahon <michaelm@openjdk.org>
Co-authored-by: Daniel Fuchs <dfuchs@openjdk.org>
Co-authored-by: Jan Lahoda <jlahoda@openjdk.org>
Co-authored-by: Ivan Šipka <isipka@openjdk.org>
Reviewed-by: ihse, jlaskey, michaelm, chegar, dfuchs
This commit is contained in:
Julia Boes 2021-10-19 10:19:15 +00:00
parent 947d52c4c3
commit 9d191fce55
42 changed files with 7166 additions and 35 deletions

View File

@ -1,5 +1,5 @@
# #
# Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved. # Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved.
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
# #
# This code is free software; you can redistribute it and/or modify it # This code is free software; you can redistribute it and/or modify it
@ -196,6 +196,11 @@ else # not java.base
endif endif
endif endif
# Set main class of jdk.httpserver module
ifeq ($(MODULE), jdk.httpserver)
JMOD_FLAGS += --main-class sun.net.httpserver.simpleserver.Main
endif
# Changes to the jmod tool itself should also trigger a rebuild of all jmods. # Changes to the jmod tool itself should also trigger a rebuild of all jmods.
# The variable JMOD_CMD could contain an environment variable assignment before # The variable JMOD_CMD could contain an environment variable assignment before
# the actual command. Filter that out using wildcard before adding to DEPS. # the actual command. Filter that out using wildcard before adding to DEPS.

View File

@ -123,6 +123,7 @@ import com.sun.tools.classfile.InnerClasses_attribute;
import com.sun.tools.classfile.InnerClasses_attribute.Info; import com.sun.tools.classfile.InnerClasses_attribute.Info;
import com.sun.tools.classfile.Method; import com.sun.tools.classfile.Method;
import com.sun.tools.classfile.MethodParameters_attribute; import com.sun.tools.classfile.MethodParameters_attribute;
import com.sun.tools.classfile.ModuleMainClass_attribute;
import com.sun.tools.classfile.ModuleResolution_attribute; import com.sun.tools.classfile.ModuleResolution_attribute;
import com.sun.tools.classfile.ModuleTarget_attribute; import com.sun.tools.classfile.ModuleTarget_attribute;
import com.sun.tools.classfile.Module_attribute; import com.sun.tools.classfile.Module_attribute;
@ -928,6 +929,12 @@ public class CreateSymbols {
attributes.put(Attribute.ModuleTarget, attributes.put(Attribute.ModuleTarget,
new ModuleTarget_attribute(attrIdx, targetIdx)); new ModuleTarget_attribute(attrIdx, targetIdx));
} }
if (header.moduleMainClass != null) {
int attrIdx = addString(cp, Attribute.ModuleMainClass);
int targetIdx = addString(cp, header.moduleMainClass);
attributes.put(Attribute.ModuleMainClass,
new ModuleMainClass_attribute(attrIdx, targetIdx));
}
int attrIdx = addString(cp, Attribute.Module); int attrIdx = addString(cp, Attribute.Module);
attributes.put(Attribute.Module, attributes.put(Attribute.Module,
new Module_attribute(attrIdx, new Module_attribute(attrIdx,
@ -2294,6 +2301,13 @@ public class CreateSymbols {
chd.isSealed = true; chd.isSealed = true;
break; break;
} }
case Attribute.ModuleMainClass: {
ModuleMainClass_attribute moduleMainClass = (ModuleMainClass_attribute) attr;
assert feature instanceof ModuleHeaderDescription;
ModuleHeaderDescription mhd = (ModuleHeaderDescription) feature;
mhd.moduleMainClass = moduleMainClass.getMainClassName(cf.constant_pool);
break;
}
default: default:
throw new IllegalStateException("Unhandled attribute: " + throw new IllegalStateException("Unhandled attribute: " +
attrName); attrName);
@ -2731,6 +2745,7 @@ public class CreateSymbols {
List<ProvidesDescription> provides = new ArrayList<>(); List<ProvidesDescription> provides = new ArrayList<>();
Integer moduleResolution; Integer moduleResolution;
String moduleTarget; String moduleTarget;
String moduleMainClass;
@Override @Override
public int hashCode() { public int hashCode() {
@ -2743,6 +2758,7 @@ public class CreateSymbols {
hash = 83 * hash + Objects.hashCode(this.provides); hash = 83 * hash + Objects.hashCode(this.provides);
hash = 83 * hash + Objects.hashCode(this.moduleResolution); hash = 83 * hash + Objects.hashCode(this.moduleResolution);
hash = 83 * hash + Objects.hashCode(this.moduleTarget); hash = 83 * hash + Objects.hashCode(this.moduleTarget);
hash = 83 * hash + Objects.hashCode(this.moduleMainClass);
return hash; return hash;
} }
@ -2781,6 +2797,10 @@ public class CreateSymbols {
other.moduleResolution)) { other.moduleResolution)) {
return false; return false;
} }
if (!Objects.equals(this.moduleMainClass,
other.moduleMainClass)) {
return false;
}
return true; return true;
} }
@ -2818,6 +2838,8 @@ public class CreateSymbols {
output.append(" resolution " + output.append(" resolution " +
quote(Integer.toHexString(moduleResolution), quote(Integer.toHexString(moduleResolution),
true)); true));
if (moduleMainClass != null)
output.append(" moduleMainClass " + quote(moduleMainClass, true));
writeAttributes(output); writeAttributes(output);
output.append("\n"); output.append("\n");
writeInnerClasses(output, baselineVersion, version); writeInnerClasses(output, baselineVersion, version);
@ -2862,6 +2884,8 @@ public class CreateSymbols {
moduleResolution = Integer.parseInt(resolutionFlags, 16); moduleResolution = Integer.parseInt(resolutionFlags, 16);
} }
moduleMainClass = reader.attributes.get("moduleMainClass");
readAttributes(reader); readAttributes(reader);
reader.moveNext(); reader.moveNext();
readInnerClasses(reader); readInnerClasses(reader);

View File

@ -0,0 +1,41 @@
#
# Copyright (c) 2021, 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. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# 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.
#
include GensrcCommonJdk.gmk
include GensrcProperties.gmk
include Modules.gmk
################################################################################
# Use wildcard so as to avoid getting non-existing directories back
SIMPLESERVER_RESOURCES_DIRS := $(wildcard $(addsuffix /sun/net/httpserver/simpleserver/resources, \
$(call FindModuleSrcDirs, jdk.httpserver)))
$(eval $(call SetupCompileProperties, SIMPLESERVER_PROPERTIES, \
SRC_DIRS := $(SIMPLESERVER_RESOURCES_DIRS), \
CLASS := ListResourceBundle, \
))
TARGETS += $(SIMPLESERVER_PROPERTIES)

View File

@ -28,10 +28,13 @@ package com.sun.net.httpserver;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.URI;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Objects; import java.util.Objects;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.UnaryOperator;
import sun.net.httpserver.DelegatingHttpExchange;
/** /**
* A filter used to pre- and post-process incoming requests. Pre-processing occurs * A filter used to pre- and post-process incoming requests. Pre-processing occurs
@ -134,7 +137,6 @@ public abstract class Filter {
*/ */
public abstract void doFilter (HttpExchange exchange, Chain chain) public abstract void doFilter (HttpExchange exchange, Chain chain)
throws IOException; throws IOException;
/** /**
* Returns a short description of this {@code Filter}. * Returns a short description of this {@code Filter}.
* *
@ -252,4 +254,64 @@ public abstract class Filter {
} }
}; };
} }
/**
* Returns a
* {@linkplain Filter#beforeHandler(String, Consumer) pre-processing Filter}
* that inspects and possibly adapts the request state.
*
* The {@code Request} returned by the {@link UnaryOperator requestOperator}
* will be the effective request state of the exchange. It is executed for
* each {@code HttpExchange} before invoking either the next filter in the
* chain or the exchange handler (if this is the final filter in the chain).
* Exceptions thrown by the {@code requestOperator} are not handled by the
* filter.
*
* @apiNote
* When the returned filter is invoked, it first invokes the
* {@code requestOperator} with the given exchange, {@code ex}, in order to
* retrieve the <i>adapted request state</i>. It then invokes the next
* filter in the chain or the exchange handler, passing an exchange
* equivalent to {@code ex} with the <i>adapted request state</i> set as the
* effective request state.
*
* <p> Example of adding the {@code "Foo"} request header to all requests:
* <pre>{@code
* var filter = Filter.adaptRequest("Add Foo header", r -> r.with("Foo", List.of("Bar")));
* httpContext.getFilters().add(filter);
* }</pre>
*
* @param description the string to be returned from {@link #description()}
* @param requestOperator the request operator
* @return a filter that adapts the request state before the exchange is handled
* @throws NullPointerException if any argument is null
* @since 18
*/
public static Filter adaptRequest(String description,
UnaryOperator<Request> requestOperator) {
Objects.requireNonNull(description);
Objects.requireNonNull(requestOperator);
return new Filter() {
@Override
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
var request = requestOperator.apply(exchange);
var newExchange = new DelegatingHttpExchange(exchange) {
@Override
public URI getRequestURI() { return request.getRequestURI(); }
@Override
public String getRequestMethod() { return request.getRequestMethod(); }
@Override
public Headers getRequestHeaders() { return request.getRequestHeaders(); }
};
chain.doFilter(newExchange);
}
@Override
public String description() {
return description;
}
};
}
} }

View File

@ -25,6 +25,7 @@
package com.sun.net.httpserver; package com.sun.net.httpserver;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedList; import java.util.LinkedList;
@ -33,6 +34,8 @@ import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.stream.Collectors;
import sun.net.httpserver.UnmodifiableHeaders;
/** /**
* HTTP request and response headers are represented by this class which * HTTP request and response headers are represented by this class which
@ -65,6 +68,14 @@ import java.util.function.BiFunction;
* value given overwriting any existing values in the value list. * value given overwriting any existing values in the value list.
* </ul> * </ul>
* *
* <p> An instance of {@code Headers} is either <i>mutable</i> or <i>immutable</i>.
* A <i>mutable headers</i> allows to add, remove, or modify header names and
* values, e.g. the instance returned by {@link HttpExchange#getResponseHeaders()}.
* An <i>immutable headers</i> disallows any modification to header names or
* values, e.g. the instance returned by {@link HttpExchange#getRequestHeaders()}.
* The mutator methods for an immutable headers instance unconditionally throw
* {@code UnsupportedOperationException}.
*
* <p> All methods in this class reject {@code null} values for keys and values. * <p> All methods in this class reject {@code null} values for keys and values.
* {@code null} keys will never be present in HTTP request or response headers. * {@code null} keys will never be present in HTTP request or response headers.
* @since 1.6 * @since 1.6
@ -78,6 +89,25 @@ public class Headers implements Map<String,List<String>> {
*/ */
public Headers() {map = new HashMap<>(32);} public Headers() {map = new HashMap<>(32);}
/**
* Creates a mutable {@code Headers} from the given {@code headers} with
* the same header names and values.
*
* @param headers a map of header names and values
* @throws NullPointerException if {@code headers} or any of its names or
* values are null, or if any value contains
* null.
* @since 18
*/
public Headers(Map<String,List<String>> headers) {
Objects.requireNonNull(headers);
var h = headers.entrySet().stream()
.collect(Collectors.toUnmodifiableMap(
Entry::getKey, e -> new LinkedList<>(e.getValue())));
map = new HashMap<>(32);
this.putAll(h);
}
/** /**
* Normalize the key by converting to following form. * Normalize the key by converting to following form.
* First {@code char} upper case, rest lower case. * First {@code char} upper case, rest lower case.
@ -254,4 +284,55 @@ public class Headers implements Map<String,List<String>> {
sb.append(" }"); sb.append(" }");
return sb.toString(); return sb.toString();
} }
/**
* Returns an immutable {@code Headers} with the given name value pairs as
* its set of headers.
*
* <p> The supplied {@code String} instances must alternate as header names
* and header values. To add several values to the same name, the same name
* must be supplied with each new value. If the supplied {@code headers} is
* empty, then an empty {@code Headers} is returned.
*
* @param headers the list of name value pairs
* @return an immutable headers with the given name value pairs
* @throws NullPointerException if {@code headers} or any of its
* elements are null.
* @throws IllegalArgumentException if the number of supplied strings is odd.
* @since 18
*/
public static Headers of(String... headers) {
Objects.requireNonNull(headers);
if (headers.length == 0) {
return new UnmodifiableHeaders(new Headers());
}
if (headers.length % 2 != 0) {
throw new IllegalArgumentException("wrong number, %d, of elements"
.formatted(headers.length));
}
Arrays.stream(headers).forEach(Objects::requireNonNull);
var h = new Headers();
for (int i = 0; i < headers.length; i += 2) {
String name = headers[i];
String value = headers[i + 1];
h.add(name, value);
}
return new UnmodifiableHeaders(h);
}
/**
* Returns an immutable {@code Headers} from the given {@code headers} with
* the same header names and values.
*
* @param headers a map of header names and values
* @return an immutable headers
* @throws NullPointerException if {@code headers} or any of its names or
* values are null, or if any value contains
* null.
* @since 18
*/
public static Headers of(Map<String,List<String>> headers) {
return new UnmodifiableHeaders(new Headers(headers));
}
} }

View File

@ -69,7 +69,7 @@ import java.net.URI;
* @since 1.6 * @since 1.6
*/ */
public abstract class HttpExchange implements AutoCloseable { public abstract class HttpExchange implements AutoCloseable, Request {
/** /**
* Constructor for subclasses to call. * Constructor for subclasses to call.
@ -78,19 +78,8 @@ public abstract class HttpExchange implements AutoCloseable {
} }
/** /**
* Returns an immutable {@link Headers} containing the HTTP headers that * {@inheritDoc}
* were included with this request. * @return {@inheritDoc}
*
* <p> The keys in this {@code Headers} are the header names, while the
* values are a {@link java.util.List} of
* {@linkplain java.lang.String Strings} containing each value that was
* included in the request, in the order they were included. Header fields
* appearing multiple times are represented as multiple string values.
*
* <p> The keys in {@code Headers} are case-insensitive.
*
* @return a read-only {@code Headers} which can be used to access request
* headers.
*/ */
public abstract Headers getRequestHeaders(); public abstract Headers getRequestHeaders();
@ -111,16 +100,14 @@ public abstract class HttpExchange implements AutoCloseable {
public abstract Headers getResponseHeaders(); public abstract Headers getResponseHeaders();
/** /**
* Returns the request {@link URI}. * {@inheritDoc}
* * @return {@inheritDoc}
* @return the request {@code URI}
*/ */
public abstract URI getRequestURI(); public abstract URI getRequestURI();
/** /**
* Returns the request method. * {@inheritDoc}
* * @return {@inheritDoc}
* @return the request method
*/ */
public abstract String getRequestMethod(); public abstract String getRequestMethod();

View File

@ -0,0 +1,169 @@
/*
* Copyright (c) 2021, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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 com.sun.net.httpserver;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Implementations of {@link com.sun.net.httpserver.HttpHandler HttpHandler}
* that implement various useful handlers, such as a static response handler,
* or a conditional handler that complements one handler with another.
*
* <p> The factory method {@link #of(int, Headers, String)} provides a
* means to create handlers with pre-set static response state. For example, a
* {@code jsonHandler} that always returns <i>200</i> with the same json:
* <pre>{@code
* HttpHandlers.of(200,
* Headers.of("Content-Type", "application/json"),
* Files.readString(Path.of("some.json")));
* }</pre>
* or a {@code notAllowedHandler} that always replies with <i>405</i> -
* Method Not Allowed, and indicates the set of methods that are allowed:
* <pre>{@code
* HttpHandlers.of(405, Headers.of("Allow", "GET"), "");
* }</pre>
*
* <p> The functionality of a handler can be extended or enhanced through the
* use of {@link #handleOrElse(Predicate, HttpHandler, HttpHandler) handleOrElse},
* which allows to complement a given handler. For example, complementing a
* {@code jsonHandler} with <i>notAllowedHandler</i>:
*
* <pre>{@code
* Predicate<Request> IS_GET = r -> r.getRequestMethod().equals("GET");
* var handler = HttpHandlers.handleOrElse(IS_GET, jsonHandler, notAllowedHandler);
* }</pre>
*
* The above <i>handleOrElse</i> {@code handler} offers an if-else like construct;
* if the request method is "GET" then handling of the exchange is delegated to
* the {@code jsonHandler}, otherwise handling of the exchange is delegated to
* the {@code notAllowedHandler}.
*
* @since 18
*/
public final class HttpHandlers {
private HttpHandlers() { }
/**
* Complements a conditional {@code HttpHandler} with another handler.
*
* <p> This method creates a <i>handleOrElse</i> handler; an if-else like
* construct. Exchanges who's request matches the {@code handlerTest}
* predicate are handled by the {@code handler}. All remaining exchanges
* are handled by the {@code fallbackHandler}.
*
* <p> Example of a nested handleOrElse handler:
* <pre>{@code
* Predicate<Request> IS_GET = r -> r.getRequestMethod().equals("GET");
* Predicate<Request> WANTS_DIGEST = r -> r.getRequestHeaders().containsKey("Want-Digest");
*
* var h1 = new SomeHandler();
* var h2 = HttpHandlers.handleOrElse(IS_GET, new SomeGetHandler(), h1);
* var h3 = HttpHandlers.handleOrElse(WANTS_DIGEST.and(IS_GET), new SomeDigestHandler(), h2);
* }</pre>
* The {@code h3} handleOrElse handler delegates handling of the exchange to
* {@code SomeDigestHandler} if the "Want-Digest" request header is present
* and the request method is {@code GET}, otherwise it delegates handling of
* the exchange to the {@code h2} handler. The {@code h2} handleOrElse
* handler, in turn, delegates handling of the exchange to {@code
* SomeGetHandler} if the request method is {@code GET}, otherwise it
* delegates handling of the exchange to the {@code h1} handler. The {@code
* h1} handler handles all exchanges that are not previously delegated to
* either {@code SomeGetHandler} or {@code SomeDigestHandler}.
*
* @param handlerTest a request predicate
* @param handler a conditional handler
* @param fallbackHandler a fallback handler
* @return a handler
* @throws NullPointerException if any argument is null
*/
public static HttpHandler handleOrElse(Predicate<Request> handlerTest,
HttpHandler handler,
HttpHandler fallbackHandler) {
Objects.requireNonNull(handlerTest);
Objects.requireNonNull(handler);
Objects.requireNonNull(fallbackHandler);
return exchange -> {
if (handlerTest.test(exchange))
handler.handle(exchange);
else
fallbackHandler.handle(exchange);
};
}
/**
* Returns an {@code HttpHandler} that sends a response comprising the given
* {@code statusCode}, {@code headers}, and {@code body}.
*
* <p> This method creates a handler that reads and discards the request
* body before it sets the response state and sends the response.
*
* <p> {@code headers} are the effective headers of the response. The
* response <i>body bytes</i> are a {@code UTF-8} encoded byte sequence of
* {@code body}. The response headers
* {@linkplain HttpExchange#sendResponseHeaders(int, long) are sent} with
* the given {@code statusCode} and the body bytes' length (or {@code -1}
* if the body is empty). The body bytes are then sent as response body,
* unless the body is empty, in which case no response body is sent.
*
* @param statusCode a response status code
* @param headers a headers
* @param body a response body string
* @return a handler
* @throws IllegalArgumentException if statusCode is not a positive 3-digit
* integer, as per rfc2616, section 6.1.1
* @throws NullPointerException if headers or body are null
*/
public static HttpHandler of(int statusCode, Headers headers, String body) {
if (statusCode < 100 || statusCode > 999)
throw new IllegalArgumentException("statusCode must be 3-digit: "
+ statusCode);
Objects.requireNonNull(headers);
Objects.requireNonNull(body);
final var headersCopy = Headers.of(headers);
final var bytes = body.getBytes(StandardCharsets.UTF_8);
return exchange -> {
try (exchange) {
exchange.getRequestBody().readAllBytes();
exchange.getResponseHeaders().putAll(headersCopy);
if (exchange.getRequestMethod().equals("HEAD")) {
exchange.getResponseHeaders().set("Content-Length", Integer.toString(bytes.length));
exchange.sendResponseHeaders(statusCode, -1);
}
else if (bytes.length == 0) {
exchange.sendResponseHeaders(statusCode, -1);
} else {
exchange.sendResponseHeaders(statusCode, bytes.length);
exchange.getResponseBody().write(bytes);
}
}
};
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2005, 2020, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -30,6 +30,8 @@ import com.sun.net.httpserver.spi.HttpServerProvider;
import java.io.IOException; import java.io.IOException;
import java.net.BindException; import java.net.BindException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
/** /**
@ -150,6 +152,58 @@ public abstract class HttpServer {
return provider.createHttpServer (addr, backlog); return provider.createHttpServer (addr, backlog);
} }
/**
* Creates an {@code HttpServer} instance with an initial context.
*
* <p> The server is created with an <i>initial context</i> that maps the
* URI {@code path} to the exchange {@code handler}. The initial context is
* created as if by an invocation of
* {@link HttpServer#createContext(String) createContext(path)}. The
* {@code filters}, if any, are added to the initial context, in the order
* they are given. The returned server is not started so can be configured
* further if required.
*
* <p> The server instance will bind to the given
* {@link java.net.InetSocketAddress}.
*
* <p> A maximum backlog can also be specified. This is the maximum number
* of queued incoming connections to allow on the listening socket.
* Queued TCP connections exceeding this limit may be rejected by
* the TCP implementation. The HttpServer is acquired from the currently
* installed {@link HttpServerProvider}.
*
* @param addr the address to listen on, if {@code null} then
* {@link #bind bind} must be called to set the address
* @param backlog the socket backlog. If this value is less than or
* equal to zero, then a system default value is used
* @param path the root URI path of the context, must be absolute
* @param handler the HttpHandler for the context
* @param filters the Filters for the context, optional
* @return the HttpServer
* @throws BindException if the server cannot bind to the address
* @throws IOException if an I/O error occurs
* @throws IllegalArgumentException if path is invalid
* @throws NullPointerException if any of: {@code path}, {@code handler},
* {@code filters}, or any element of {@code filters}, are {@code null}
* @since 18
*/
public static HttpServer create(InetSocketAddress addr,
int backlog,
String path,
HttpHandler handler,
Filter... filters) throws IOException {
Objects.requireNonNull(path);
Objects.requireNonNull(handler);
Objects.requireNonNull(filters);
Arrays.stream(filters).forEach(Objects::requireNonNull);
HttpServer server = HttpServer.create(addr, backlog);
HttpContext context = server.createContext(path);
context.setHandler(handler);
Arrays.stream(filters).forEach(f -> context.getFilters().add(f));
return server;
}
/** /**
* Binds a currently unbound {@code HttpServer} to the given address and * Binds a currently unbound {@code HttpServer} to the given address and
* port number. A maximum backlog can also be specified. This is the maximum * port number. A maximum backlog can also be specified. This is the maximum

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -28,6 +28,9 @@ package com.sun.net.httpserver;
import java.io.IOException; import java.io.IOException;
import java.net.BindException; import java.net.BindException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Objects;
import com.sun.net.httpserver.spi.HttpServerProvider; import com.sun.net.httpserver.spi.HttpServerProvider;
/** /**
@ -92,6 +95,61 @@ public abstract class HttpsServer extends HttpServer {
return provider.createHttpsServer(addr, backlog); return provider.createHttpsServer(addr, backlog);
} }
/**
* Creates an {@code HttpsServer} instance with an initial context.
*
* <p> The server is created with an <i>initial context</i> that maps the
* URI {@code path} to the exchange {@code handler}. The initial context is
* created as if by an invocation of
* {@link HttpsServer#createContext(String) createContext(path)}. The
* {@code filters}, if any, are added to the initial context, in the order
* they are given. The returned server is not started so can be configured
* further if required.
*
* <p> The server instance will bind to the given
* {@link java.net.InetSocketAddress}.
*
* <p> A maximum backlog can also be specified. This is the maximum number
* of queued incoming connections to allow on the listening socket.
* Queued TCP connections exceeding this limit may be rejected by
* the TCP implementation. The HttpsServer is acquired from the currently
* installed {@link HttpServerProvider}.
*
* <p> The server must have an HttpsConfigurator established with
* {@link #setHttpsConfigurator(HttpsConfigurator)}.
*
* @param addr the address to listen on, if {@code null} then
* {@link #bind bind} must be called to set the address
* @param backlog the socket backlog. If this value is less than or
* equal to zero, then a system default value is used
* @param path the root URI path of the context, must be absolute
* @param handler the HttpHandler for the context
* @param filters the Filters for the context, optional
* @return the HttpsServer
* @throws BindException if the server cannot bind to the address
* @throws IOException if an I/O error occurs
* @throws IllegalArgumentException if path is invalid
* @throws NullPointerException if any of: {@code path}, {@code handler},
* {@code filters}, or any element of {@code filters}, are {@code null}
* @since 18
*/
public static HttpsServer create(InetSocketAddress addr,
int backlog,
String path,
HttpHandler handler,
Filter... filters) throws IOException {
Objects.requireNonNull(path);
Objects.requireNonNull(handler);
Objects.requireNonNull(filters);
Arrays.stream(filters).forEach(Objects::requireNonNull);
HttpsServer server = HttpsServer.create(addr, backlog);
HttpContext context = server.createContext(path);
context.setHandler(handler);
Arrays.stream(filters).forEach(f -> context.getFilters().add(f));
return server;
}
/** /**
* Sets this server's {@link HttpsConfigurator} object. * Sets this server's {@link HttpsConfigurator} object.
* *

View File

@ -0,0 +1,120 @@
/*
* Copyright (c) 2021, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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 com.sun.net.httpserver;
import java.net.URI;
import java.util.List;
import java.util.Objects;
/**
* A view of the immutable request state of an HTTP exchange.
*
* @since 18
*/
public interface Request {
/**
* Returns the request {@link URI}.
*
* @return the request {@code URI}
*/
URI getRequestURI();
/**
* Returns the request method.
*
* @return the request method string
*/
String getRequestMethod();
/**
* Returns an immutable {@link Headers} containing the HTTP headers that
* were included with this request.
*
* <p> The keys in this {@code Headers} are the header names, while the
* values are a {@link java.util.List} of
* {@linkplain java.lang.String Strings} containing each value that was
* included in the request, in the order they were included. Header fields
* appearing multiple times are represented as multiple string values.
*
* <p> The keys in {@code Headers} are case-insensitive.
*
* @return a read-only {@code Headers} which can be used to access request
* headers.
*/
Headers getRequestHeaders();
/**
* Returns an identical {@code Request} with an additional header.
*
* <p> The returned {@code Request} has the same set of
* {@link #getRequestHeaders() headers} as {@code this} request, but with
* the addition of the given header. All other request state remains
* unchanged.
*
* <p> If {@code this} request already contains a header with the same name
* as the given {@code headerName}, then its value is not replaced.
*
* @implSpec
* The default implementation first creates a new {@code Headers}, {@code h},
* then adds all the request headers from {@code this} request to {@code h},
* then adds the given name-values mapping if {@code headerName} is
* not present in {@code h}. Then an unmodifiable view, {@code h'}, of
* {@code h} and a new {@code Request}, {@code r}, are created.
* The {@code getRequestMethod} and {@code getRequestURI} methods of
* {@code r} simply invoke the equivalently named method of {@code this}
* request. The {@code getRequestHeaders} method returns {@code h'}. Lastly,
* {@code r} is returned.
*
* @param headerName the header name
* @param headerValues the list of header values
* @return a request
* @throws NullPointerException if any argument is null, or if any element
* of headerValues is null.
*/
default Request with(String headerName, List<String> headerValues) {
Objects.requireNonNull(headerName);
Objects.requireNonNull(headerValues);
final Request r = this;
var h = new Headers();
h.putAll(r.getRequestHeaders());
if (!h.containsKey(headerName)) {
h.put(headerName, headerValues);
}
var unmodifiableHeaders = Headers.of(h);
return new Request() {
@Override
public URI getRequestURI() { return r.getRequestURI(); }
@Override
public String getRequestMethod() { return r.getRequestMethod(); }
@Override
public Headers getRequestHeaders() { return unmodifiableHeaders; }
};
}
}

View File

@ -0,0 +1,265 @@
/*
* Copyright (c) 2021, 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 com.sun.net.httpserver;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.net.URLConnection;
import java.nio.file.Path;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;
import sun.net.httpserver.simpleserver.FileServerHandler;
import sun.net.httpserver.simpleserver.OutputFilter;
/**
* A simple HTTP file server and its components (intended for testing,
* development and debugging purposes only).
*
* <p> A <a href="#server-impl">simple file server</a> is composed of three
* components:
* <ul>
* <li> an {@link HttpServer HttpServer} that is bound to a given address, </li>
* <li> an {@link HttpHandler HttpHandler} that serves files from a given
* directory path, and </li>
* <li> an optional {@link Filter Filter} that prints log messages relating to
* the exchanges handled by the server. </li>
* </ul>
* The individual server components can be retrieved for reuse and extension via
* the static methods provided.
*
* <h2>Simple file server</h2>
*
* <p> The {@link #createFileServer(InetSocketAddress,Path,OutputLevel) createFileServer}
* static factory method returns an {@link HttpServer HttpServer} that is a
* simple out-of-the-box file server. The server comes with an initial handler
* that serves files from a given directory path (and its subdirectories).
* The output level determines what log messages are printed to
* {@code System.out}, if any.
*
* <p> Example of a simple file server:
* <pre>{@code
* var addr = new InetSocketAddress(8080);
* var server = SimpleFileServer.createFileServer(addr, Path.of("/some/path"), OutputLevel.INFO);
* server.start();
* }</pre>
*
* <h2>File handler</h2>
*
* <p> The {@link #createFileHandler(Path) createFileHandler} static factory
* method returns an {@code HttpHandler} that serves files and directory
* listings. The handler supports only the <i>HEAD</i> and <i>GET</i> request
* methods; to handle other request methods, one can either add additional
* handlers to the server, or complement the file handler by composing a single
* handler via
* {@link HttpHandlers#handleOrElse(Predicate, HttpHandler, HttpHandler)}.
*
* <p>Example of composing a single handler:
* <pre>{@code
* var handler = HttpHandlers.handleOrElse(
* (req) -> req.getRequestMethod().equals("PUT"),
* (exchange) -> {
* // validate and handle PUT request
* },
* SimpleFileServer.createFileHandler(Path.of("/some/path")))
* );
* }</pre>
*
* <h2>Output filter</h2>
*
* <p> The {@link #createOutputFilter(OutputStream, OutputLevel) createOutputFilter}
* static factory method returns a
* {@link Filter#afterHandler(String, Consumer) post-processing filter} that
* prints log messages relating to the exchanges handled by the server. The
* output format is specified by the {@link OutputLevel outputLevel}.
*
* <p> Example of an output filter:
* <pre>{@code
* var filter = SimpleFileServer.createOutputFilter(System.out, OutputLevel.VERBOSE);
* var server = HttpServer.create(new InetSocketAddress(8080), 10, "/some/path/", new SomeHandler(), filter);
* server.start();
* }</pre>
*
* <h2>Main entry point</h2>
*
* <p>A <a id="server-impl">simple HTTP file server implementation</a> is
* provided via the
* <a href="{@docRoot}/jdk.httpserver/module-summary.html#entry-point">main entry point</a>
* of the {@code jdk.httpserver} module.
*
* @since 18
*/
public final class SimpleFileServer {
private static final UnaryOperator<String> MIME_TABLE =
URLConnection.getFileNameMap()::getContentTypeFor;
private SimpleFileServer() { }
/**
* Describes the log message output level produced by the server when
* processing exchanges.
*
* @since 18
*/
public enum OutputLevel {
/**
* Used to specify no log message output level.
*/
NONE,
/**
* Used to specify the informative log message output level.
*
* <p> The log message format is based on the
* <a href='https://www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format'>Common Logfile Format</a>,
* that includes the following information about an {@code HttpExchange}:
*
* <p> {@code remotehost rfc931 authuser [date] "request" status bytes}
*
* <p> Example:
* <pre>{@code
* 127.0.0.1 - - [22/Jun/2000:13:55:36 -0700] "GET /example.txt HTTP/1.1" 200 -
* }</pre>
*
* @implNote The fields {@code rfc931}, {@code authuser} and {@code bytes}
* are not captured in the implementation, so are always represented as
* {@code '-'}.
*/
INFO,
/**
* Used to specify the verbose log message output level.
*
* <p> Additional to the information provided by the
* {@linkplain OutputLevel#INFO info} level, the verbose level
* includes the request and response headers of the {@code HttpExchange}
* and the absolute path of the resource served up.
*/
VERBOSE
}
/**
* Creates a <i>file server</i> the serves files from a given path.
*
* <p> The server is configured with an initial context that maps the
* URI {@code path} to a <i>file handler</i>. The <i>file handler</i> is
* created as if by an invocation of
* {@link #createFileHandler(Path) createFileHandler(rootDirectory)}, and is
* associated to a context created as if by an invocation of
* {@link HttpServer#createContext(String) createContext("/")}.
*
* <p> An output level can be given to print log messages relating to the
* exchanges handled by the server. The log messages, if any, are printed to
* {@code System.out}. If {@link OutputLevel#NONE OutputLevel.NONE} is
* given, no log messages are printed.
*
* @param addr the address to listen on
* @param rootDirectory the root directory to be served, must be an absolute path
* @param outputLevel the log message output level
* @return an HttpServer
* @throws IllegalArgumentException if root does not exist, is not absolute,
* is not a directory, or is not readable
* @throws UncheckedIOException if an I/O error occurs
* @throws NullPointerException if any argument is null
* @throws SecurityException if a security manager is installed and a
* recursive {@link java.io.FilePermission} "{@code read}" of the
* rootDirectory is denied
*/
public static HttpServer createFileServer(InetSocketAddress addr,
Path rootDirectory,
OutputLevel outputLevel) {
Objects.requireNonNull(addr);
Objects.requireNonNull(rootDirectory);
Objects.requireNonNull(outputLevel);
try {
var handler = FileServerHandler.create(rootDirectory, MIME_TABLE);
if (outputLevel.equals(OutputLevel.NONE))
return HttpServer.create(addr, 0, "/", handler);
else
return HttpServer.create(addr, 0, "/", handler, OutputFilter.create(System.out, outputLevel));
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}
/**
* Creates a <i>file handler</i> that serves files from a given directory
* path (and its subdirectories).
*
* <p> The file handler resolves the request URI against the given
* {@code rootDirectory} path to determine the path {@code p} on the
* associated file system to serve the response. If the path {@code p} is
* a directory, then the response contains a directory listing, formatted in
* HTML, as the response body. If the path {@code p} is a file, then the
* response contains a "Content-Type" header based on the best-guess
* content type, as determined by an invocation of
* {@linkplain java.net.FileNameMap#getContentTypeFor(String) getContentTypeFor},
* on the system-wide {@link URLConnection#getFileNameMap() mimeTable}, as
* well as the contents of the file as the response body.
*
* <p> The handler supports only requests with the <i>HEAD</i> or <i>GET</i>
* method, and will reply with a {@code 405} response code for requests with
* any other method.
*
* @param rootDirectory the root directory to be served, must be an absolute path
* @return a file handler
* @throws IllegalArgumentException if rootDirectory does not exist,
* is not absolute, is not a directory, or is not readable
* @throws NullPointerException if the argument is null
* @throws SecurityException if a security manager is installed and a
* recursive {@link java.io.FilePermission} "{@code read}" of the
* rootDirectory is denied
*/
public static HttpHandler createFileHandler(Path rootDirectory) {
Objects.requireNonNull(rootDirectory);
return FileServerHandler.create(rootDirectory, MIME_TABLE);
}
/**
* Creates a {@linkplain Filter#afterHandler(String, Consumer)
* post-processing Filter} that prints log messages about
* {@linkplain HttpExchange exchanges}. The log messages are printed to
* the given {@code OutputStream} in {@code UTF-8} encoding.
*
* @apiNote
* To not output any log messages it is recommended to not use a filter.
*
* @param out the stream to print to
* @param outputLevel the output level
* @return a post-processing filter
* @throws IllegalArgumentException if {@link OutputLevel#NONE OutputLevel.NONE}
* is given
* @throws NullPointerException if any argument is null
*/
public static Filter createOutputFilter(OutputStream out,
OutputLevel outputLevel) {
Objects.requireNonNull(out);
Objects.requireNonNull(outputLevel);
return OutputFilter.create(out, outputLevel);
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2005, 2013, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -120,6 +120,12 @@
} }
}); });
</pre></blockquote> </pre></blockquote>
<p>
The {@link com.sun.net.httpserver.SimpleFileServer} class offers a simple
HTTP file server (intended for testing, development and debugging purposes
only). A default implementation is provided via the
<a href="{@docRoot}/jdk.httpserver/module-summary.html#entry-point">main entry point</a>
of the {@code jdk.httpserver} module.
@since 1.6 @since 1.6
*/ */

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2014, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,6 +25,40 @@
/** /**
* Defines the JDK-specific HTTP server API. * Defines the JDK-specific HTTP server API.
* <p>
* A basic high-level API for building embedded servers. Both HTTP and
* HTTPS are supported.
* <p>
* The main components are:
* <ul>
* <li>the {@link com.sun.net.httpserver.HttpExchange} class that describes a
* request and response pair,</li>
* <li>the {@link com.sun.net.httpserver.HttpHandler} interface to handle
* incoming requests, plus the {@link com.sun.net.httpserver.HttpHandlers} class
* that provides useful handler implementations,</li>
* <li>the {@link com.sun.net.httpserver.HttpContext} class that maps a URI path
* to a {@code HttpHandler},</li>
* <li>the {@link com.sun.net.httpserver.HttpServer} class to listen for
* connections and dispatch requests to handlers,</li>
* <li>the {@link com.sun.net.httpserver.Filter} class that allows pre- and post-
* processing of requests.</li></ul>
* <p>
* The {@link com.sun.net.httpserver.SimpleFileServer} class offers a simple
* HTTP file server (intended for testing, development and debugging purposes
* only). A default implementation is provided via the <a id="entry-point"></a>
* main entry point of the {@code jdk.httpserver} module, which can be used on
* the command line as such:
* <pre>{@code
* Usage: java -m jdk.httpserver [-b bind address] [-p port] [-d directory]
* [-o none|info|verbose] [-h to show options]
* Options:
* -b, --bind-address - Address to bind to. Default: 127.0.0.1 or ::1 (loopback).
* For all interfaces use "-b 0.0.0.0" or "-b ::".
* -d, --directory - Directory to serve. Default: current directory.
* -o, --output - Output format. none|info|verbose. Default: info.
* -p, --port - Port to listen on. Default: 8000.
* -h, -?, --help - Print this help message.
* }</pre>
* *
* @uses com.sun.net.httpserver.spi.HttpServerProvider * @uses com.sun.net.httpserver.spi.HttpServerProvider
* *

View File

@ -0,0 +1,108 @@
/*
* Copyright (c) 2021, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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 sun.net.httpserver;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpPrincipal;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
public abstract class DelegatingHttpExchange extends HttpExchange {
private final HttpExchange exchange;
public DelegatingHttpExchange(HttpExchange ex) {
this.exchange = ex;
}
public abstract Headers getRequestHeaders();
public abstract String getRequestMethod();
public abstract URI getRequestURI();
public Headers getResponseHeaders() {
return exchange.getResponseHeaders();
}
public HttpContext getHttpContext() {
return exchange.getHttpContext();
}
public void close() {
exchange.close();
}
public InputStream getRequestBody() {
return exchange.getRequestBody();
}
public int getResponseCode() {
return exchange.getResponseCode();
}
public OutputStream getResponseBody() {
return exchange.getResponseBody();
}
public void sendResponseHeaders(int rCode, long contentLen) throws IOException {
exchange.sendResponseHeaders(rCode, contentLen);
}
public InetSocketAddress getRemoteAddress() {
return exchange.getRemoteAddress();
}
public InetSocketAddress getLocalAddress() {
return exchange.getLocalAddress();
}
public String getProtocol() {
return exchange.getProtocol();
}
public Object getAttribute(String name) {
return exchange.getAttribute(name);
}
public void setAttribute(String name, Object value) {
exchange.setAttribute(name, value);
}
public void setStreams(InputStream i, OutputStream o) {
exchange.setStreams(i, o);
}
public HttpPrincipal getPrincipal() {
return exchange.getPrincipal();
}
}

View File

@ -85,7 +85,7 @@ class ExchangeImpl {
String m, URI u, Request req, long len, HttpConnection connection String m, URI u, Request req, long len, HttpConnection connection
) throws IOException { ) throws IOException {
this.req = req; this.req = req;
this.reqHdrs = new UnmodifiableHeaders(req.headers()); this.reqHdrs = Headers.of(req.headers());
this.rspHdrs = new Headers(); this.rspHdrs = new Headers();
this.method = m; this.method = m;
this.uri = u; this.uri = u;

View File

@ -0,0 +1,385 @@
/*
* Copyright (c) 2005, 2021, 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 sun.net.httpserver.simpleserver;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.lang.System.Logger;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.function.UnaryOperator;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpHandlers;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* A basic HTTP file server handler for static content.
*
* <p> Must be given an absolute pathname to the directory to be served.
* Supports only HEAD and GET requests. Directory listings and files can be
* served, content types are supported on a best-guess basis.
*/
public final class FileServerHandler implements HttpHandler {
private static final List<String> SUPPORTED_METHODS = List.of("HEAD", "GET");
private static final List<String> UNSUPPORTED_METHODS =
List.of("CONNECT", "DELETE", "OPTIONS", "PATCH", "POST", "PUT", "TRACE");
private final Path root;
private final UnaryOperator<String> mimeTable;
private final Logger logger;
private FileServerHandler(Path root, UnaryOperator<String> mimeTable) {
root = root.normalize();
@SuppressWarnings("removal")
var securityManager = System.getSecurityManager();
if (securityManager != null)
securityManager.checkRead(pathForSecurityCheck(root.toString()));
if (!Files.exists(root))
throw new IllegalArgumentException("Path does not exist: " + root);
if (!root.isAbsolute())
throw new IllegalArgumentException("Path is not absolute: " + root);
if (!Files.isDirectory(root))
throw new IllegalArgumentException("Path is not a directory: " + root);
if (!Files.isReadable(root))
throw new IllegalArgumentException("Path is not readable: " + root);
this.root = root;
this.mimeTable = mimeTable;
this.logger = System.getLogger("com.sun.net.httpserver");
}
private static String pathForSecurityCheck(String path) {
var separator = String.valueOf(File.separatorChar);
return path.endsWith(separator) ? (path + "-") : (path + separator + "-");
}
private static final HttpHandler NOT_IMPLEMENTED_HANDLER =
HttpHandlers.of(501, Headers.of(), "");
private static final HttpHandler METHOD_NOT_ALLOWED_HANDLER =
HttpHandlers.of(405, Headers.of("Allow", "HEAD, GET"), "");
public static HttpHandler create(Path root, UnaryOperator<String> mimeTable) {
var fallbackHandler = HttpHandlers.handleOrElse(
r -> UNSUPPORTED_METHODS.contains(r.getRequestMethod()),
METHOD_NOT_ALLOWED_HANDLER,
NOT_IMPLEMENTED_HANDLER);
return HttpHandlers.handleOrElse(
r -> SUPPORTED_METHODS.contains(r.getRequestMethod()),
new FileServerHandler(root, mimeTable), fallbackHandler);
}
private void handleHEAD(HttpExchange exchange, Path path) throws IOException {
handleSupportedMethod(exchange, path, false);
}
private void handleGET(HttpExchange exchange, Path path) throws IOException {
handleSupportedMethod(exchange, path, true);
}
private void handleSupportedMethod(HttpExchange exchange, Path path, boolean writeBody)
throws IOException {
if (Files.isDirectory(path)) {
if (missingSlash(exchange)) {
handleMovedPermanently(exchange);
return;
}
if (indexFile(path) != null) {
serveFile(exchange, indexFile(path), writeBody);
} else {
listFiles(exchange, path, writeBody);
}
} else {
serveFile(exchange, path, writeBody);
}
}
private void handleMovedPermanently(HttpExchange exchange) throws IOException {
exchange.getResponseHeaders().set("Location", getRedirectURI(exchange.getRequestURI()));
exchange.sendResponseHeaders(301, -1);
}
private void handleForbidden(HttpExchange exchange) throws IOException {
exchange.sendResponseHeaders(403, -1);
}
private void handleNotFound(HttpExchange exchange) throws IOException {
String fileNotFound = ResourceBundleHelper.getMessage("html.not.found");
var bytes = (openHTML
+ "<h1>" + fileNotFound + "</h1>\n"
+ "<p>" + sanitize.apply(exchange.getRequestURI().getPath()) + "</p>\n"
+ closeHTML).getBytes(UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8");
if (exchange.getRequestMethod().equals("HEAD")) {
exchange.getResponseHeaders().set("Content-Length", Integer.toString(bytes.length));
exchange.sendResponseHeaders(404, -1);
} else {
exchange.sendResponseHeaders(404, bytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
}
}
private static void discardRequestBody(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody()) {
is.readAllBytes();
}
}
private String getRedirectURI(URI uri) {
String query = uri.getRawQuery();
String redirectPath = uri.getRawPath() + "/";
return query == null ? redirectPath : redirectPath + "?" + query;
}
private static boolean missingSlash(HttpExchange exchange) {
return !exchange.getRequestURI().getPath().endsWith("/");
}
private static String contextPath(HttpExchange exchange) {
String context = exchange.getHttpContext().getPath();
if (!context.startsWith("/")) {
throw new IllegalArgumentException("Context path invalid: " + context);
}
return context;
}
private static String requestPath(HttpExchange exchange) {
String request = exchange.getRequestURI().getPath();
if (!request.startsWith("/")) {
throw new IllegalArgumentException("Request path invalid: " + request);
}
return request;
}
// Checks that the request does not escape context.
private static void checkRequestWithinContext(String requestPath,
String contextPath) {
if (requestPath.equals(contextPath)) {
return; // context path requested, e.g. context /foo, request /foo
}
String contextPathWithTrailingSlash = contextPath.endsWith("/")
? contextPath : contextPath + "/";
if (!requestPath.startsWith(contextPathWithTrailingSlash)) {
throw new IllegalArgumentException("Request not in context: " + contextPath);
}
}
// Checks that path is, or is within, the root.
private static Path checkPathWithinRoot(Path path, Path root) {
if (!path.startsWith(root)) {
throw new IllegalArgumentException("Request not in root");
}
return path;
}
// Returns the request URI path relative to the context.
private static String relativeRequestPath(HttpExchange exchange) {
String context = contextPath(exchange);
String request = requestPath(exchange);
checkRequestWithinContext(request, context);
return request.substring(context.length());
}
private Path mapToPath(HttpExchange exchange, Path root) {
try {
assert root.isAbsolute() && Files.isDirectory(root); // checked during creation
String uriPath = relativeRequestPath(exchange);
String[] pathSegment = uriPath.split("/");
// resolve each path segment against the root
Path path = root;
for (var segment : pathSegment) {
path = path.resolve(segment);
if (!Files.isReadable(path) || isHiddenOrSymLink(path)) {
return null; // stop resolution, null results in 404 response
}
}
path = path.normalize();
return checkPathWithinRoot(path, root);
} catch (Exception e) {
logger.log(System.Logger.Level.TRACE,
"FileServerHandler: request URI path resolution failed", e);
return null; // could not resolve request URI path
}
}
private static Path indexFile(Path path) {
Path html = path.resolve("index.html");
Path htm = path.resolve("index.htm");
return Files.exists(html) ? html : Files.exists(htm) ? htm : null;
}
private void serveFile(HttpExchange exchange, Path path, boolean writeBody)
throws IOException
{
var respHdrs = exchange.getResponseHeaders();
respHdrs.set("Content-Type", mediaType(path.toString()));
respHdrs.set("Last-Modified", getLastModified(path));
if (writeBody) {
exchange.sendResponseHeaders(200, Files.size(path));
try (InputStream fis = Files.newInputStream(path);
OutputStream os = exchange.getResponseBody()) {
fis.transferTo(os);
}
} else {
respHdrs.set("Content-Length", Long.toString(Files.size(path)));
exchange.sendResponseHeaders(200, -1);
}
}
private void listFiles(HttpExchange exchange, Path path, boolean writeBody)
throws IOException
{
var respHdrs = exchange.getResponseHeaders();
respHdrs.set("Content-Type", "text/html; charset=UTF-8");
respHdrs.set("Last-Modified", getLastModified(path));
var bodyBytes = dirListing(exchange, path).getBytes(UTF_8);
if (writeBody) {
exchange.sendResponseHeaders(200, bodyBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(bodyBytes);
}
} else {
respHdrs.set("Content-Length", Integer.toString(bodyBytes.length));
exchange.sendResponseHeaders(200, -1);
}
}
private static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
private static final String closeHTML = """
</body>
</html>
""";
private static final String hrefListItemTemplate = """
<li><a href="%s">%s</a></li>
""";
private static String hrefListItemFor(URI uri) {
return hrefListItemTemplate.formatted(uri.toASCIIString(), sanitize.apply(uri.getPath()));
}
private static String dirListing(HttpExchange exchange, Path path) throws IOException {
String dirListing = ResourceBundleHelper.getMessage("html.dir.list");
var sb = new StringBuilder(openHTML
+ "<h1>" + dirListing + " "
+ sanitize.apply(exchange.getRequestURI().getPath())
+ "</h1>\n"
+ "<ul>\n");
try (var paths = Files.list(path)) {
paths.filter(p -> Files.isReadable(p) && !isHiddenOrSymLink(p))
.map(p -> path.toUri().relativize(p.toUri()))
.forEach(uri -> sb.append(hrefListItemFor(uri)));
}
sb.append("</ul>\n");
sb.append(closeHTML);
return sb.toString();
}
private static String getLastModified(Path path) throws IOException {
var fileTime = Files.getLastModifiedTime(path);
return fileTime.toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
private static boolean isHiddenOrSymLink(Path path) {
try {
return Files.isHidden(path) || Files.isSymbolicLink(path);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
// Default for unknown content types, as per RFC 2046
private static final String DEFAULT_CONTENT_TYPE = "application/octet-stream";
private String mediaType(String file) {
String type = mimeTable.apply(file);
return type != null ? type : DEFAULT_CONTENT_TYPE;
}
// A non-exhaustive map of reserved-HTML and special characters to their
// equivalent entity.
private static final Map<Integer,String> RESERVED_CHARS = Map.of(
(int) '&' , "&amp;" ,
(int) '<' , "&lt;" ,
(int) '>' , "&gt;" ,
(int) '"' , "&quot;" ,
(int) '\'' , "&#x27;" ,
(int) '/' , "&#x2F;" );
// A function that takes a string and returns a sanitized version of that
// string with the reserved-HTML and special characters replaced with their
// equivalent entity.
private static final UnaryOperator<String> sanitize =
file -> file.chars().collect(StringBuilder::new,
(sb, c) -> sb.append(RESERVED_CHARS.getOrDefault(c, Character.toString(c))),
StringBuilder::append).toString();
@Override
public void handle(HttpExchange exchange) throws IOException {
assert List.of("GET", "HEAD").contains(exchange.getRequestMethod());
try (exchange) {
discardRequestBody(exchange);
Path path = mapToPath(exchange, root);
if (path != null) {
exchange.setAttribute("request-path", path.toString()); // store for OutputFilter
if (!Files.exists(path) || !Files.isReadable(path) || isHiddenOrSymLink(path)) {
handleNotFound(exchange);
} else if (exchange.getRequestMethod().equals("HEAD")) {
handleHEAD(exchange, path);
} else {
handleGET(exchange, path);
}
} else {
exchange.setAttribute("request-path", "could not resolve request URI path");
handleNotFound(exchange);
}
}
}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2021, 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 sun.net.httpserver.simpleserver;
import java.io.PrintWriter;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Programmatic entry point to start the simpleserver tool.
*
* <p><b> This is NOT part of any supported API.
* If you write code that depends on this, you do so at your own risk.
* This code and its internal interface are subject to change or deletion
* without notice.</b>
*/
public class Main {
/**
* This constructor should never be called.
*/
private Main() { throw new AssertionError(); }
/**
* The main entry point.
*
* <p> The command line arguments are parsed and the server is started. If
* started successfully, the server will run on a new non-daemon thread,
* and this method will return. Otherwise, if the server is not started
* successfully, e.g. an error is encountered while parsing the arguments
* or an I/O error occurs, the server is not started and this method invokes
* System::exit with an appropriate exit code.
*
* @param args the command-line options
* @throws NullPointerException if {@code args} is {@code null}, or if there
* are any {@code null} values in the {@code args} array
*/
public static void main(String... args) {
int ec = SimpleFileServerImpl.start(new PrintWriter(System.out, true, UTF_8), args);
if (ec != 0)
System.exit(ec);
// otherwise the server has been started successfully and runs in
// another non-daemon thread.
}
}

View File

@ -0,0 +1,133 @@
/*
* Copyright (c) 2005, 2021, 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 sun.net.httpserver.simpleserver;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.function.Consumer;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* A Filter that outputs log messages about an HttpExchange. The implementation
* uses a {@link Filter#afterHandler(String, Consumer) post-processing filter}.
*
* <p> If the outputLevel is INFO, the format is based on the
* <a href='https://www.w3.org/Daemon/User/Config/Logging.html#common-logfile-format'>Common Logfile Format</a>.
* In this case the output includes the following information about an exchange:
*
* <p> remotehost rfc931 authuser [date] "request line" status bytes
*
* <p> Example:
* 127.0.0.1 - - [22/Jun/2000:13:55:36 -0700] "GET /example.txt HTTP/1.1" 200 -
*
* <p> The fields rfc931, authuser and bytes are not captured in the implementation
* and are always represented as '-'.
*
* <p> If the outputLevel is VERBOSE, the output additionally includes the
* absolute path of the resource requested, if it has been
* {@linkplain HttpExchange#setAttribute(String, Object) provided} via the
* attribute {@code "request-path"}, as well as the request and response headers
* of the exchange.
*/
public final class OutputFilter extends Filter {
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z");
private final PrintStream printStream;
private final OutputLevel outputLevel;
private final Filter filter;
private OutputFilter(OutputStream os, OutputLevel outputLevel) {
printStream = new PrintStream(os, true, UTF_8);
this.outputLevel = outputLevel;
var description = "HttpExchange OutputFilter (outputLevel: " + outputLevel + ")";
this.filter = Filter.afterHandler(description, operation());
}
public static OutputFilter create(OutputStream os, OutputLevel outputLevel) {
if (outputLevel.equals(OutputLevel.NONE)) {
throw new IllegalArgumentException("Not a valid outputLevel: " + outputLevel);
}
return new OutputFilter(os, outputLevel);
}
private Consumer<HttpExchange> operation() {
return e -> {
String s = e.getRemoteAddress().getHostString() + " "
+ "- - " // rfc931 and authuser
+ "[" + OffsetDateTime.now().format(FORMATTER) + "] "
+ "\"" + e.getRequestMethod() + " " + e.getRequestURI() + " " + e.getProtocol() + "\" "
+ e.getResponseCode() + " -"; // bytes
printStream.println(s);
if (outputLevel.equals(OutputLevel.VERBOSE)) {
if (e.getAttribute("request-path") instanceof String requestPath) {
printStream.println("Resource requested: " + requestPath);
}
logHeaders(">", e.getRequestHeaders());
logHeaders("<", e.getResponseHeaders());
}
};
}
private void logHeaders(String sign, Headers headers) {
headers.forEach((name, values) -> {
var sb = new StringBuilder();
var it = values.iterator();
while (it.hasNext()) {
sb.append(it.next());
if (it.hasNext()) {
sb.append(", ");
}
}
printStream.println(sign + " " + name + ": " + sb);
});
printStream.println(sign);
}
@Override
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
try {
filter.doFilter(exchange, chain);
} catch (Throwable t) {
if (!outputLevel.equals(OutputLevel.NONE)) {
reportError(ResourceBundleHelper.getMessage("err.server.handle.failed",
t.getMessage()));
}
throw t;
}
}
@Override
public String description() { return filter.description(); }
private void reportError(String message) {
printStream.println(ResourceBundleHelper.getMessage("error.prefix") + " " + message);
}
}

View File

@ -0,0 +1,49 @@
/*
* Copyright (c) 2021, 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 sun.net.httpserver.simpleserver;
import java.text.MessageFormat;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
class ResourceBundleHelper {
static final ResourceBundle bundle;
static {
try {
bundle = ResourceBundle.getBundle("sun.net.httpserver.simpleserver.resources.simpleserver");
} catch (MissingResourceException e) {
throw new InternalError("Cannot find simpleserver resource bundle for locale " + Locale.getDefault());
}
}
static String getMessage(String key, Object... args) {
try {
return MessageFormat.format(bundle.getString(key), args);
} catch (MissingResourceException e) {
throw new InternalError("Missing message: " + key);
}
}
}

View File

@ -0,0 +1,216 @@
/*
* Copyright (c) 2021, 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 sun.net.httpserver.simpleserver;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* A class that provides a simple HTTP file server to serve the content of
* a given directory.
*
* <p> The server is an HttpServer bound to a given address. It comes with an
* HttpHandler that serves files from a given directory path
* (and its subdirectories) on the default file system, and an optional Filter
* that prints log messages related to the exchanges handled by the server to
* a given output stream.
*
* <p> Unless specified as arguments, the default values are:<ul>
* <li>bind address: 127.0.0.1 or ::1 (loopback)</li>
* <li>directory: current working directory</li>
* <li>outputLevel: info</li></ul>
* <li>port: 8000</li>
* <p>
* The implementation is provided via the main entry point of the jdk.httpserver
* module.
*/
final class SimpleFileServerImpl {
private static final InetAddress LOOPBACK_ADDR = InetAddress.getLoopbackAddress();
private static final int DEFAULT_PORT = 8000;
private static final Path DEFAULT_ROOT = Path.of("").toAbsolutePath();
private static final OutputLevel DEFAULT_OUTPUT_LEVEL = OutputLevel.INFO;
private static boolean addrSpecified = false;
private SimpleFileServerImpl() { throw new AssertionError(); }
/**
* Starts a simple HTTP file server created on a directory.
*
* @param writer the writer to which output should be written
* @param args the command line options
* @throws NullPointerException if any of the arguments are {@code null},
* or if there are any {@code null} values in the {@code args} array
* @return startup status code
*/
static int start(PrintWriter writer, String[] args) {
Objects.requireNonNull(args);
for (var arg : args) {
Objects.requireNonNull(arg);
}
Out out = new Out(writer);
InetAddress addr = LOOPBACK_ADDR;
int port = DEFAULT_PORT;
Path root = DEFAULT_ROOT;
OutputLevel outputLevel = DEFAULT_OUTPUT_LEVEL;
// parse options
Iterator<String> options = Arrays.asList(args).iterator();
String option = null;
String optionArg = null;
try {
while (options.hasNext()) {
option = options.next();
switch (option) {
case "-h", "-?", "--help" -> {
out.showHelp();
return Startup.OK.statusCode;
}
case "-b", "--bind-address" -> {
addr = InetAddress.getByName(optionArg = options.next());
addrSpecified = true;
}
case "-d", "--directory" ->
root = Path.of(optionArg = options.next());
case "-o", "--output" ->
outputLevel = Enum.valueOf(OutputLevel.class,
(optionArg = options.next()).toUpperCase(Locale.ROOT));
case "-p", "--port" ->
port = Integer.parseInt(optionArg = options.next());
default -> throw new AssertionError();
}
}
} catch (AssertionError ae) {
out.reportError(ResourceBundleHelper.getMessage("err.unknown.option", option));
out.showUsage();
return Startup.CMDERR.statusCode;
} catch (NoSuchElementException nsee) {
out.reportError(ResourceBundleHelper.getMessage("err.missing.arg", option));
out.showOption(option);
return Startup.CMDERR.statusCode;
} catch (Exception e) {
out.reportError(ResourceBundleHelper.getMessage("err.invalid.arg", option, optionArg));
e.printStackTrace(out.writer);
return Startup.CMDERR.statusCode;
} finally {
out.flush();
}
// configure and start server
try {
var socketAddr = new InetSocketAddress(addr, port);
var server = SimpleFileServer.createFileServer(socketAddr, root, outputLevel);
server.start();
out.printStartMessage(root, server);
} catch (Throwable t) {
out.reportError(ResourceBundleHelper.getMessage("err.server.config.failed", t.getMessage()));
return Startup.SYSERR.statusCode;
} finally {
out.flush();
}
return Startup.OK.statusCode;
}
private final static class Out {
private final PrintWriter writer;
private Out() { throw new AssertionError(); }
Out(PrintWriter writer) {
this.writer = Objects.requireNonNull(writer);
}
void printStartMessage(Path root, HttpServer server)
throws UnknownHostException
{
String port = Integer.toString(server.getAddress().getPort());
var inetAddr = server.getAddress().getAddress();
var isAnyLocal = inetAddr.isAnyLocalAddress();
var addr = isAnyLocal ? InetAddress.getLocalHost().getHostAddress() : inetAddr.getHostAddress();
if (!addrSpecified) {
writer.println(ResourceBundleHelper.getMessage("loopback.info"));
}
if (isAnyLocal) {
writer.println(ResourceBundleHelper.getMessage("msg.start.anylocal", root, addr, port));
} else {
writer.println(ResourceBundleHelper.getMessage("msg.start.other", root, addr, port));
}
}
void showUsage() {
writer.println(ResourceBundleHelper.getMessage("usage"));
}
void showHelp() {
writer.println(ResourceBundleHelper.getMessage("usage"));
writer.println(ResourceBundleHelper.getMessage("options", LOOPBACK_ADDR.getHostAddress()));
}
void showOption(String option) {
switch (option) {
case "-b", "--bind-address" ->
writer.println(ResourceBundleHelper.getMessage("opt.bindaddress", LOOPBACK_ADDR.getHostAddress()));
case "-d", "--directory" ->
writer.println(ResourceBundleHelper.getMessage("opt.directory"));
case "-o", "--output" ->
writer.println(ResourceBundleHelper.getMessage("opt.output"));
case "-p", "--port" ->
writer.println(ResourceBundleHelper.getMessage("opt.port"));
}
}
void reportError(String message) {
writer.println(ResourceBundleHelper.getMessage("error.prefix") + " " + message);
}
void flush() {
writer.flush();
}
}
private enum Startup {
/** Started with no errors */
OK(0),
/** Not started, bad command-line arguments */
CMDERR(1),
/** Not started, system error or resource exhaustion */
SYSERR(2);
Startup(int statusCode) {
this.statusCode = statusCode;
}
public final int statusCode;
}
}

View File

@ -0,0 +1,70 @@
#
# Copyright (c) 2021, 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. Oracle designates this
# particular file as subject to the "Classpath" exception as provided
# by Oracle in the LICENSE file that accompanied this code.
#
# 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.
#
usage=\
Usage: java -m jdk.httpserver [-b bind address] [-p port] [-d directory]\n\
\ [-o none|info|verbose] [-h to show options]
options=\
Options:\n\
-b, --bind-address - Address to bind to. Default: {0} (loopback).\n\
\ For all interfaces use "-b 0.0.0.0" or "-b ::".\n\
-d, --directory - Directory to serve. Default: current directory.\n\
-o, --output - Output format. none|info|verbose. Default: info.\n\
-p, --port - Port to listen on. Default: 8000.\n\
-h, -?, --help - Print this help message.\n\
To stop the server, press Ctrl + C.
opt.bindaddress=\
-b, --bind-address - Address to bind to. Default: {0} (loopback).\n\
\ For all interfaces use "-b 0.0.0.0" or "-b ::".
opt.directory=\
-d, --directory - Directory to serve. Default: current directory.
opt.output=\
-o, --output - Output format. none|info|verbose. Default: info.
opt.port=\
-p, --port - Port to listen on. Default: 8000.
loopback.info=\
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
msg.start.anylocal=\
Serving {0} and subdirectories on 0.0.0.0 (all interfaces) port {2}\n\
URL http://{1}:{2}/
msg.start.other=\
Serving {0} and subdirectories on {1} port {2}\n\
URL http://{1}:{2}/
error.prefix=Error:
err.unknown.option=unknown option: {0}
err.missing.arg=no value given for {0}
err.invalid.arg=invalid value given for {0}: {1}
err.server.config.failed=server config failed: {0}
err.server.handle.failed=server exchange handling failed: {0}
html.dir.list=Directory listing for
html.not.found=File not found

View File

@ -27,8 +27,9 @@ exclusiveAccess.dirs=java/math/BigInteger/largeMemory \
java/rmi/Naming java/util/prefs sun/management/jmxremote \ java/rmi/Naming java/util/prefs sun/management/jmxremote \
sun/tools/jstatd sun/tools/jcmd \ sun/tools/jstatd sun/tools/jcmd \
sun/tools/jinfo sun/tools/jmap sun/tools/jps sun/tools/jstack sun/tools/jstat \ sun/tools/jinfo sun/tools/jmap sun/tools/jps sun/tools/jstack sun/tools/jstat \
com/sun/tools/attach sun/security/mscapi java/util/Arrays/largeMemory \ com/sun/tools/attach sun/security/mscapi java/util/stream java/util/Arrays/largeMemory \
java/util/BitSet/stream javax/rmi java/net/httpclient/websocket java/util/BitSet/stream javax/rmi java/net/httpclient/websocket \
com/sun/net/httpserver/simpleserver
# Group definitions # Group definitions
groups=TEST.groups groups=TEST.groups

View File

@ -40,6 +40,8 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.ConsoleHandler; import java.util.logging.ConsoleHandler;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -47,10 +49,10 @@ import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import org.testng.annotations.DataProvider; import org.testng.annotations.DataProvider;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import org.testng.annotations.BeforeTest; import org.testng.annotations.BeforeTest;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.*; import static org.testng.Assert.*;
public class FilterTest { public class FilterTest {
@ -79,6 +81,9 @@ public class FilterTest {
expectThrows(NPE, () -> Filter.afterHandler("Some description", null)); expectThrows(NPE, () -> Filter.afterHandler("Some description", null));
expectThrows(NPE, () -> Filter.afterHandler(null, HttpExchange::getResponseCode)); expectThrows(NPE, () -> Filter.afterHandler(null, HttpExchange::getResponseCode));
expectThrows(NPE, () -> Filter.adaptRequest("Some description", null));
expectThrows(NPE, () -> Filter.adaptRequest(null, r -> r.with("Foo", List.of("Bar"))));
} }
@Test @Test
@ -90,6 +95,9 @@ public class FilterTest {
var afterFilter = Filter.afterHandler(desc, HttpExchange::getResponseCode); var afterFilter = Filter.afterHandler(desc, HttpExchange::getResponseCode);
assertEquals(desc, afterFilter.description()); assertEquals(desc, afterFilter.description());
var adaptFilter = Filter.adaptRequest(desc, r -> r.with("Foo", List.of("Bar")));
assertEquals(desc, adaptFilter.description());
} }
@DataProvider @DataProvider
@ -305,6 +313,64 @@ public class FilterTest {
} }
} }
@Test
public void testInspectRequest() throws Exception {
var handler = new EchoHandler();
var inspectedURI = new AtomicReference<URI>();
var filter = Filter.adaptRequest("Inspect request URI",
r -> {inspectedURI.set(r.getRequestURI()); return r;});
var server = HttpServer.create(new InetSocketAddress(LOOPBACK_ADDR,0), 10);
server.createContext("/", handler).getFilters().add(filter);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "foo/bar")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(inspectedURI.get(), URI.create("/foo/bar"));
} finally {
server.stop(0);
}
}
private static HttpExchange originalExchange;
/**
* Confirms that adaptRequest changes only the expected request state and
* all other exchange state remains unchanged.
*/
@Test
public void testAdaptRequest() throws Exception {
var handler = new CompareStateAndEchoHandler();
var captureFilter = Filter.beforeHandler("capture exchange", e -> {
e.setAttribute("foo", "bar");
originalExchange = e;
});
var adaptFilter = Filter.adaptRequest("Add x-foo request header", r -> {
// Confirm request state is unchanged
assertEquals(r.getRequestHeaders(), originalExchange.getRequestHeaders());
assertEquals(r.getRequestURI(), originalExchange.getRequestURI());
assertEquals(r.getRequestMethod(), originalExchange.getRequestMethod());
return r.with("x-foo", List.of("bar"));
});
var server = HttpServer.create(new InetSocketAddress(LOOPBACK_ADDR,0), 10);
var context = server.createContext("/", handler);
context.getFilters().add(captureFilter);
context.getFilters().add(adaptFilter);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "bar");
} finally {
server.stop(0);
}
}
// --- infra ---
static URI uri(HttpServer server, String path) { static URI uri(HttpServer server, String path) {
return URI.create("http://localhost:%s/%s".formatted(server.getAddress().getPort(), path)); return URI.create("http://localhost:%s/%s".formatted(server.getAddress().getPort(), path));
} }
@ -325,6 +391,42 @@ public class FilterTest {
} }
} }
/**
* A handler that compares the adapted exchange with the original exchange,
* before discarding the request and returning the test request header value.
*/
static class CompareStateAndEchoHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
assertEquals(exchange.getLocalAddress(), originalExchange.getLocalAddress());
assertEquals(exchange.getRemoteAddress(), originalExchange.getRemoteAddress());
assertEquals(exchange.getProtocol(), originalExchange.getProtocol());
assertEquals(exchange.getPrincipal(), originalExchange.getPrincipal());
assertEquals(exchange.getHttpContext(), originalExchange.getHttpContext());
assertEquals(exchange.getRequestMethod(), originalExchange.getRequestMethod());
assertEquals(exchange.getRequestURI(), originalExchange.getRequestURI());
assertEquals(exchange.getRequestBody(), originalExchange.getRequestBody());
assertEquals(exchange.getResponseHeaders(), originalExchange.getResponseHeaders());
assertEquals(exchange.getResponseCode(), originalExchange.getResponseCode());
assertEquals(exchange.getResponseBody(), originalExchange.getResponseBody());
assertEquals(exchange.getAttribute("foo"), originalExchange.getAttribute("foo"));
assertFalse(exchange.getRequestHeaders().equals(originalExchange.getRequestHeaders()));
exchange.setAttribute("foo", "barbar");
assertEquals(exchange.getAttribute("foo"), originalExchange.getAttribute("foo"));
try (InputStream is = exchange.getRequestBody();
OutputStream os = exchange.getResponseBody()) {
is.readAllBytes();
var resp = exchange.getRequestHeaders().get("x-foo")
.get(0)
.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, resp.length);
os.write(resp);
}
}
}
/** /**
* A test handler that does nothing * A test handler that does nothing
*/ */

View File

@ -26,6 +26,7 @@
* @bug 8251496 8268960 * @bug 8251496 8268960
* @summary Tests for methods in Headers class * @summary Tests for methods in Headers class
* @modules jdk.httpserver/com.sun.net.httpserver:+open * @modules jdk.httpserver/com.sun.net.httpserver:+open
* jdk.httpserver/sun.net.httpserver:+open
* @library /test/lib * @library /test/lib
* @build jdk.test.lib.net.URIBuilder * @build jdk.test.lib.net.URIBuilder
* @run testng/othervm HeadersTest * @run testng/othervm HeadersTest
@ -55,10 +56,12 @@ import com.sun.net.httpserver.HttpServer;
import jdk.test.lib.net.URIBuilder; import jdk.test.lib.net.URIBuilder;
import org.testng.annotations.DataProvider; import org.testng.annotations.DataProvider;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import sun.net.httpserver.UnmodifiableHeaders;
import static java.net.http.HttpClient.Builder.NO_PROXY; import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotEquals;
import static org.testng.Assert.assertThrows; import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertTrue;
@ -314,4 +317,131 @@ public class HeadersTest {
h2.put("b", List.of("22")); h2.put("b", List.of("22"));
assertTrue(h1.equals(h2)); assertTrue(h1.equals(h2));
} }
@Test
public static void test1ArgConstructorNull() {
assertThrows(NPE, () -> new Headers(null));
{
final var m = new HashMap<String, List<String>>();
m.put(null, List.of("Bar"));
assertThrows(NPE, () -> new Headers(m));
}
{
final var m = new HashMap<String, List<String>>();
m.put("Foo", null);
assertThrows(NPE, () -> new Headers(m));
}
{
final var m = new HashMap<String, List<String>>();
final var list = new LinkedList<String>();
list.add(null);
m.put("Foo", list);
assertThrows(NPE, () -> new Headers(m));
}
}
@Test
public static void test1ArgConstructor() {
{
var h = new Headers(new Headers());
assertTrue(h.isEmpty());
}
{
var h = new Headers(Map.of("Foo", List.of("Bar")));
assertEquals(h.get("Foo"), List.of("Bar"));
assertEquals(h.size(), 1);
}
{
var h1 = new Headers(new UnmodifiableHeaders(new Headers()));
assertTrue(h1.isEmpty());
h1.put("Foo", List.of("Bar")); // modifiable
assertEquals(h1.get("Foo"), List.of("Bar"));
assertEquals(h1.size(), 1);
var h2 = new Headers(h1);
assertEquals(h2.get("Foo"), List.of("Bar"));
assertEquals(h2.size(), 1);
assertEquals(h1, h2);
h1.set("Foo", "Barbar");
assertNotEquals(h1, h2);
}
}
@Test
public static void testMutableHeaders() {
{
var h = new Headers();
h.add("Foo", "Bar");
h.add("Foo", "Bazz");
h.get("Foo").remove(0);
h.remove("Foo");
h.clear();
}
{
var h = new Headers(Map.of("Foo", List.of("Bar")));
h.get("Foo").add("Bazz");
h.get("Foo").remove(0);
h.remove("Foo");
h.clear();
}
}
@Test
public static void testOfNull() {
assertThrows(NPE, () -> Headers.of((String[])null));
assertThrows(NPE, () -> Headers.of(null, "Bar"));
assertThrows(NPE, () -> Headers.of("Foo", null));
assertThrows(NPE, () -> Headers.of((Map<String, List<String>>) null));
{
final var m = new HashMap<String, List<String>>();
m.put(null, List.of("Bar"));
assertThrows(NPE, () -> Headers.of(m));
}
{
final var m = new HashMap<String, List<String>>();
m.put("Foo", null);
assertThrows(NPE, () -> Headers.of(m));
}
{
final var m = new HashMap<String, List<String>>();
final var list = new LinkedList<String>();
list.add(null);
m.put("Foo", list);
assertThrows(NPE, () -> Headers.of(m));
}
}
@Test
public static void testOf() {
final var h = Headers.of("a", "1", "b", "2");
assertEquals(h.size(), 2);
List.of("a", "b").forEach(n -> assertTrue(h.containsKey(n)));
List.of("1", "2").forEach(v -> assertTrue(h.containsValue(List.of(v))));
}
@Test
public static void testOfEmpty() {
for (var h : List.of(Headers.of(), Headers.of(new String[] { }))) {
assertEquals(h.size(), 0);
assertTrue(h.isEmpty());
}
}
@Test
public static void testOfNumberOfElements() {
assertThrows(IAE, () -> Headers.of("a"));
assertThrows(IAE, () -> Headers.of("a", "1", "b"));
}
@Test
public static void testOfMultipleValues() {
final var h = Headers.of("a", "1", "b", "1", "b", "2", "b", "3");
assertEquals(h.size(), 2);
List.of("a", "b").forEach(n -> assertTrue(h.containsKey(n)));
List.of(List.of("1"), List.of("1", "2", "3")).forEach(v -> assertTrue(h.containsValue(v)));
}
// Immutability tests in UnmodifiableHeadersTest.java
} }

View File

@ -40,6 +40,7 @@ import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext; import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpPrincipal; import com.sun.net.httpserver.HttpPrincipal;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import sun.net.httpserver.UnmodifiableHeaders; import sun.net.httpserver.UnmodifiableHeaders;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
@ -62,14 +63,24 @@ public class UnmodifiableHeadersTest {
assertEquals(unmodifiableHeaders2.get("Foo"), headers.get("Foo")); assertEquals(unmodifiableHeaders2.get("Foo"), headers.get("Foo"));
} }
@Test @DataProvider
public static void testUnmodifiableHeaders() { public Object[][] headers() {
var headers = new Headers(); var headers = new Headers();
headers.add("Foo", "Bar"); headers.add("Foo", "Bar");
HttpExchange exchange = new TestHttpExchange(headers); var exchange = new TestHttpExchange(headers);
assertUnsupportedOperation(exchange.getRequestHeaders()); return new Object[][] {
assertUnmodifiableCollection(exchange.getRequestHeaders()); { exchange.getRequestHeaders() },
{ Headers.of("Foo", "Bar") },
{ Headers.of(Map.of("Foo", List.of("Bar"))) },
};
}
@Test(dataProvider = "headers")
public static void testUnmodifiableHeaders(Headers headers) {
assertUnsupportedOperation(headers);
assertUnmodifiableCollection(headers);
assertUnmodifiableList(headers);
} }
static final Class<UnsupportedOperationException> UOP = UnsupportedOperationException.class; static final Class<UnsupportedOperationException> UOP = UnsupportedOperationException.class;

View File

@ -0,0 +1,245 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Negative tests for simpleserver command-line tool
* @library /test/lib
* @modules jdk.httpserver
* @run testng/othervm CommandLineNegativeTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import jdk.test.lib.Platform;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.util.FileUtils;
import org.testng.SkipException;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.lang.System.out;
import static org.testng.Assert.assertFalse;
public class CommandLineNegativeTest {
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
static final String JAVA = getJava(JAVA_HOME);
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
static final Path TEST_DIR = CWD.resolve("CommandLineNegativeTest");
static final Path TEST_FILE = TEST_DIR.resolve("file.txt");
static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress();
@BeforeTest
public void setup() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
Files.createDirectories(TEST_DIR);
Files.createFile(TEST_FILE);
}
@DataProvider
public Object[][] unknownOption() {
return new Object[][] {
{"--unknownOption"},
{"null"}
};
}
@Test(dataProvider = "unknownOption")
public void testBadOption(String opt) throws Throwable {
out.println("\n--- testUnknownOption, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt)
.shouldNotHaveExitValue(0)
.shouldContain("Error: unknown option: " + opt);
}
@DataProvider
public Object[][] tooManyOptionArgs() {
return new Object[][] {
{"-b", "localhost"},
{"-d", "/some/path"},
{"-o", "none"},
{"-p", "0"},
{"--bind-address", "localhost"},
{"--directory", "/some/path"},
{"--output", "none"},
{"--port", "0"}
// doesn't fail for -h option
};
}
@Test(dataProvider = "tooManyOptionArgs")
public void testTooManyOptionArgs(String opt, String arg) throws Throwable {
out.println("\n--- testTooManyOptionArgs, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, arg, arg)
.shouldNotHaveExitValue(0)
.shouldContain("Error: unknown option: " + arg);
}
@DataProvider
public Object[][] noArg() {
return new Object[][] {
{"-b", """
-b, --bind-address - Address to bind to. Default: %s (loopback).
For all interfaces use "-b 0.0.0.0" or "-b ::".""".formatted(LOOPBACK_ADDR)},
{"-d", "-d, --directory - Directory to serve. Default: current directory."},
{"-o", "-o, --output - Output format. none|info|verbose. Default: info."},
{"-p", "-p, --port - Port to listen on. Default: 8000."},
{"--bind-address", """
-b, --bind-address - Address to bind to. Default: %s (loopback).
For all interfaces use "-b 0.0.0.0" or "-b ::".""".formatted(LOOPBACK_ADDR)},
{"--directory", "-d, --directory - Directory to serve. Default: current directory."},
{"--output", "-o, --output - Output format. none|info|verbose. Default: info."},
{"--port", "-p, --port - Port to listen on. Default: 8000."}
// doesn't fail for -h option
};
}
@Test(dataProvider = "noArg")
public void testNoArg(String opt, String msg) throws Throwable {
out.println("\n--- testNoArg, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt)
.shouldNotHaveExitValue(0)
.shouldContain("Error: no value given for " + opt)
.shouldContain(msg);
}
@DataProvider
public Object[][] invalidValue() {
return new Object[][] {
{"-b", "[127.0.0.1]"},
{"-b", "badhost"},
{"--bind-address", "192.168.1.220..."},
{"-o", "bad-output-level"},
{"--output", "bad-output-level"},
{"-p", "+-"},
{"--port", "+-"}
};
}
@Test(dataProvider = "invalidValue")
public void testInvalidValue(String opt, String val) throws Throwable {
out.println("\n--- testInvalidValue, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, val)
.shouldNotHaveExitValue(0)
.shouldContain("Error: invalid value given for " + opt + ": " + val);
}
@DataProvider
public Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
@Test(dataProvider = "portOptions")
public void testPortOutOfRange(String opt) throws Throwable {
out.println("\n--- testPortOutOfRange, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, "65536") // range 0 to 65535
.shouldNotHaveExitValue(0)
.shouldContain("Error: server config failed: " + "port out of range:65536");
}
@DataProvider
public Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
@Test(dataProvider = "directoryOptions")
public void testRootNotAbsolute(String opt) throws Throwable {
out.println("\n--- testRootNotAbsolute, opt=\"%s\" ".formatted(opt));
var root = Path.of(".");
assertFalse(root.isAbsolute());
simpleserver(JAVA, "-m", "jdk.httpserver", opt, root.toString())
.shouldNotHaveExitValue(0)
.shouldContain("Error: server config failed: " + "Path is not absolute:");
}
@Test(dataProvider = "directoryOptions")
public void testRootNotADirectory(String opt) throws Throwable {
out.println("\n--- testRootNotADirectory, opt=\"%s\" ".formatted(opt));
var file = TEST_FILE.toString();
assertFalse(Files.isDirectory(TEST_FILE));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, file)
.shouldNotHaveExitValue(0)
.shouldContain("Error: server config failed: " + "Path is not a directory: " + file);
}
@Test(dataProvider = "directoryOptions")
public void testRootDoesNotExist(String opt) throws Throwable {
out.println("\n--- testRootDoesNotExist, opt=\"%s\" ".formatted(opt));
Path root = TEST_DIR.resolve("not/existent/dir");
assertFalse(Files.exists(root));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, root.toString())
.shouldNotHaveExitValue(0)
.shouldContain("Error: server config failed: " + "Path does not exist: " + root.toString());
}
@Test(dataProvider = "directoryOptions")
public void testRootNotReadable(String opt) throws Throwable {
out.println("\n--- testRootNotReadable, opt=\"%s\" ".formatted(opt));
if (Platform.isWindows()) {
// Not applicable to Windows. Reason: cannot revoke an owner's read
// access to a directory that was created by that owner
throw new SkipException("cannot run on Windows");
}
Path root = Files.createDirectories(TEST_DIR.resolve("not/readable/dir"));
try {
root.toFile().setReadable(false, false);
assertFalse(Files.isReadable(root));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, root.toString())
.shouldNotHaveExitValue(0)
.shouldContain("Error: server config failed: " + "Path is not readable: " + root.toString());
} finally {
root.toFile().setReadable(true, false);
}
}
@AfterTest
public void teardown() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
}
// --- infra ---
static String getJava(Path image) {
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
Path java = image.resolve("bin").resolve(isWindows ? "java.exe" : "java");
if (Files.notExists(java))
throw new RuntimeException(java + " not found");
return java.toAbsolutePath().toString();
}
static OutputAnalyzer simpleserver(String... args) throws Throwable {
var pb = new ProcessBuilder(args)
.directory(TEST_DIR.toFile());
var outputAnalyser = ProcessTools.executeCommand(pb)
.outputTo(System.out)
.errorTo(System.out);
return outputAnalyser;
}
}

View File

@ -0,0 +1,240 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Positive tests for simpleserver command-line tool
* @library /test/lib
* @modules jdk.httpserver
* @run testng/othervm CommandLinePositiveTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import jdk.test.lib.Platform;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.test.lib.util.FileUtils;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.lang.System.out;
public class CommandLinePositiveTest {
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
static final String JAVA = getJava(JAVA_HOME);
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
static final Path TEST_DIR = CWD.resolve("CommandLinePositiveTest");
static final Path TEST_FILE = TEST_DIR.resolve("file.txt");
static final String TEST_DIR_STR = TEST_DIR.toString();
static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress();
@BeforeTest
public void setup() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
Files.createDirectories(TEST_DIR);
Files.createFile(TEST_FILE);
}
static final int SIGTERM = 15;
static final int NORMAL_EXIT_CODE = normalExitCode();
static int normalExitCode() {
if (Platform.isWindows()) {
return 1; // expected process destroy exit code
} else {
// signal terminated exit code on Unix is 128 + signal value
return 128 + SIGTERM;
}
}
@DataProvider
public Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
@Test(dataProvider = "directoryOptions")
public void testDirectory(String opt) throws Throwable {
out.println("\n--- testDirectory, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", "-p", "0", opt, TEST_DIR_STR)
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@DataProvider
public Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
@Test(dataProvider = "portOptions")
public void testPort(String opt) throws Throwable {
out.println("\n--- testPort, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, "0")
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@DataProvider
public Object[][] helpOptions() { return new Object[][] {{"-h"}, {"-?"}, {"--help"}}; }
static final String USAGE_TEXT = """
Usage: java -m jdk.httpserver [-b bind address] [-p port] [-d directory]
[-o none|info|verbose] [-h to show options]""";
static final String OPTIONS_TEXT = """
Options:
-b, --bind-address - Address to bind to. Default: %s (loopback).
For all interfaces use "-b 0.0.0.0" or "-b ::".
-d, --directory - Directory to serve. Default: current directory.
-o, --output - Output format. none|info|verbose. Default: info.
-p, --port - Port to listen on. Default: 8000.
-h, -?, --help - Print this help message.
To stop the server, press Ctrl + C.""".formatted(LOOPBACK_ADDR);
@Test(dataProvider = "helpOptions")
public void testHelp(String opt) throws Throwable {
out.println("\n--- testHelp, opt=\"%s\" ".formatted(opt));
simpleserver(WaitForLine.HELP_STARTUP_LINE,
false, // do not explicitly destroy the process
JAVA, "-m", "jdk.httpserver", opt)
.shouldHaveExitValue(0)
.shouldContain(USAGE_TEXT)
.shouldContain(OPTIONS_TEXT);
}
@DataProvider
public Object[][] bindOptions() { return new Object[][] {{"-b"}, {"--bind-address"}}; }
@Test(dataProvider = "bindOptions")
public void testBindAllInterfaces(String opt) throws Throwable {
out.println("\n--- testPort, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, "0.0.0.0")
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
simpleserver(JAVA, "-m", "jdk.httpserver", opt, "::0")
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
}
@Test(dataProvider = "bindOptions")
public void testLastOneWinsBindAddress(String opt) throws Throwable {
out.println("\n--- testLastOneWinsBindAddress, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", "-p", "0", opt, "123.4.5.6", opt, LOOPBACK_ADDR)
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@Test(dataProvider = "directoryOptions")
public void testLastOneWinsDirectory(String opt) throws Throwable {
out.println("\n--- testLastOneWinsDirectory, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", "-p", "0", opt, TEST_DIR_STR, opt, TEST_DIR_STR)
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@DataProvider
public Object[][] outputOptions() { return new Object[][] {{"-o"}, {"--output"}}; }
@Test(dataProvider = "outputOptions")
public void testLastOneWinsOutput(String opt) throws Throwable {
out.println("\n--- testLastOneWinsOutput, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", "-p", "0", opt, "none", opt, "verbose")
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@Test(dataProvider = "portOptions")
public void testLastOneWinsPort(String opt) throws Throwable {
out.println("\n--- testLastOneWinsPort, opt=\"%s\" ".formatted(opt));
simpleserver(JAVA, "-m", "jdk.httpserver", opt, "-999", opt, "0")
.shouldHaveExitValue(NORMAL_EXIT_CODE)
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
.shouldContain("Serving " + TEST_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
.shouldContain("URL http://" + LOOPBACK_ADDR);
}
@AfterTest
public void teardown() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
}
// --- infra ---
static String getJava(Path image) {
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
Path java = image.resolve("bin").resolve(isWindows ? "java.exe" : "java");
if (Files.notExists(java))
throw new RuntimeException(java + " not found");
return java.toAbsolutePath().toString();
}
static final String REGULAR_STARTUP_LINE1_STRING = "Serving";
static final String REGULAR_STARTUP_LINE2_STRING = "URL http://";
// The stdout/stderr output line to wait for when starting the simpleserver
enum WaitForLine {
REGULAR_STARTUP_LINE (REGULAR_STARTUP_LINE2_STRING) ,
HELP_STARTUP_LINE (OPTIONS_TEXT.lines().reduce((first, second) -> second).orElseThrow());
final String value;
WaitForLine(String value) { this.value = value; }
}
static OutputAnalyzer simpleserver(String... args) throws Throwable {
return simpleserver(WaitForLine.REGULAR_STARTUP_LINE, true, args);
}
static OutputAnalyzer simpleserver(WaitForLine waitForLine, boolean destroy, String... args) throws Throwable {
StringBuffer sb = new StringBuffer(); // stdout & stderr
// start the process and await the waitForLine before returning
var p = ProcessTools.startProcess("simpleserver",
new ProcessBuilder(args).directory(TEST_DIR.toFile()),
line -> sb.append(line + "\n"),
line -> line.startsWith(waitForLine.value),
30, // suitably high default timeout, not expected to timeout
TimeUnit.SECONDS);
if (destroy) {
p.destroy(); // SIGTERM on Unix
}
int ec = p.waitFor();
var outputAnalyser = new OutputAnalyzer(sb.toString(), "", ec);
return outputAnalyser;
}
}

View File

@ -0,0 +1,970 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for SimpleFileServer with a root that is not of the default
* file system
* @library /test/lib
* @build jdk.test.lib.Platform jdk.test.lib.net.URIBuilder
* @run testng/othervm CustomFileSystemTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.CopyOption;
import java.nio.file.DirectoryStream;
import java.nio.file.FileStore;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.ProviderMismatchException;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileAttributeView;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.Platform;
import jdk.test.lib.net.URIBuilder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
public class CustomFileSystemTest {
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws Exception {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
}
@Test
public void testFileGET() throws Exception {
var root = createDirectoryInCustomFs("testFileGET");
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "some text");
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
} finally {
server.stop(0);
}
}
@Test
public void testDirectoryGET() throws Exception {
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
<li><a href="aFile.txt">aFile.txt</a></li>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testDirectoryGET");
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testFileHEAD() throws Exception {
var root = createDirectoryInCustomFs("testFileHEAD");
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt"))
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@Test
public void testDirectoryHEAD() throws Exception {
var expectedLength = Integer.toString(
(openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
<li><a href="aFile.txt">aFile.txt</a></li>
</ul>
""" + closeHTML).getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testDirectoryHEAD");
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ""))
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@DataProvider
public Object[][] indexFiles() {
var fileContent = openHTML + """
<h1>This is an index file</h1>
""" + closeHTML;
var dirListing = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
return new Object[][] {
{"1", "index.html", "text/html", "116", fileContent, true},
{"2", "index.htm", "text/html", "116", fileContent, true},
{"3", "index.txt", "text/html; charset=UTF-8", "134", dirListing, false}
};
}
@Test(dataProvider = "indexFiles")
public void testDirectoryWithIndexGET(String id,
String filename,
String contentType,
String contentLength,
String expectedBody,
boolean serveIndexFile) throws Exception {
var root = createDirectoryInCustomFs("testDirectoryWithIndexGET"+id);
var lastModified = getLastModified(root);
if (serveIndexFile) {
var file = Files.writeString(root.resolve(filename), expectedBody, CREATE);
lastModified = getLastModified(file);
}
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), contentType);
assertEquals(response.headers().firstValue("content-length").get(), contentLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
if (serveIndexFile) {
Files.delete(root.resolve(filename));
}
}
}
@Test
public void testNotReadableFileGET() throws Exception {
if (!Platform.isWindows()) { // not applicable on Windows
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testNotReadableFileGET");
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
file.toFile().setReadable(false, false);
assert !Files.isReadable(file);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
file.toFile().setReadable(true, false);
}
}
}
@Test
public void testNotReadableSegmentGET() throws Exception {
if (!Platform.isWindows()) { // not applicable on Windows
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;dir&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testNotReadableSegmentGET");
var dir = Files.createDirectory(root.resolve("dir"));
var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE);
dir.toFile().setReadable(false, false);
assert !Files.isReadable(dir);
assert Files.isReadable(file);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
dir.toFile().setReadable(true, false);
}
}
}
@Test
public void testInvalidRequestURIGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;aFile?#.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testInvalidRequestURIGET");
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile?#.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testNotFoundGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testNotFoundGET");
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testNotFoundHEAD() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testNotFoundHEAD");
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt"))
.method("HEAD", HttpRequest.BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@Test
public void testSymlinkGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;symlink</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testSymlinkGET");
var symlink = root.resolve("symlink");
var target = Files.writeString(root.resolve("target.txt"), "some text", CREATE);
Files.createSymbolicLink(symlink, target);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "symlink")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testSymlinkSegmentGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;symlink&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createDirectoryInCustomFs("testSymlinkSegmentGET");
var symlink = root.resolve("symlink");
var target = Files.createDirectory(root.resolve("target"));
Files.writeString(target.resolve("aFile.txt"), "some text", CREATE);
Files.createSymbolicLink(symlink, target);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "symlink/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testHiddenFileGET() throws Exception {
var root = createDirectoryInCustomFs("testHiddenFileGET");
var file = createHiddenFile(root);
var fileName = file.getFileName().toString();
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;""" + fileName +
"""
</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, fileName)).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testHiddenSegmentGET() throws Exception {
var root = createDirectoryInCustomFs("testHiddenSegmentGET");
var file = createFileInHiddenDirectory(root);
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;.hiddenDirectory&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ".hiddenDirectory/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
private Path createHiddenFile(Path root) throws IOException {
Path file;
if (Platform.isWindows()) {
file = Files.createFile(root.resolve("aFile.txt"));
Files.setAttribute(file, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
} else {
file = Files.writeString(root.resolve(".aFile.txt"), "some text", CREATE);
}
assertTrue(Files.isHidden(file));
return file;
}
private Path createFileInHiddenDirectory(Path root) throws IOException {
Path dir;
Path file;
if (Platform.isWindows()) {
dir = Files.createDirectory(root.resolve("hiddenDirectory"));
Files.setAttribute(dir, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
} else {
dir = Files.createDirectory(root.resolve(".hiddenDirectory"));
}
file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE);
assertTrue(Files.isHidden(dir));
assertFalse(Files.isHidden(file));
return file;
}
@Test
public void testMovedPermanently() throws Exception {
var root = createDirectoryInCustomFs("testMovedPermanently");
Files.createDirectory(root.resolve("aDirectory"));
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;aDirectory&#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
{
var client = HttpClient.newBuilder().proxy(NO_PROXY)
.followRedirects(HttpClient.Redirect.NEVER).build();
var uri = uri(server, "aDirectory");
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 301);
assertEquals(response.headers().firstValue("content-length").get(), "0");
assertEquals(response.headers().firstValue("location").get(), "/aDirectory/");
// tests that query component is preserved during redirect
var uri2 = uri(server, "aDirectory", "query");
var req2 = HttpRequest.newBuilder(uri2).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 301);
assertEquals(res2.headers().firstValue("content-length").get(), "0");
assertEquals(res2.headers().firstValue("location").get(), "/aDirectory/?query");
}
{ // tests that redirect to returned relative URI works
var client = HttpClient.newBuilder().proxy(NO_PROXY)
.followRedirects(HttpClient.Redirect.ALWAYS).build();
var uri = uri(server, "aDirectory");
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), expectedBody);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
}
} finally {
server.stop(0);
}
}
@Test
public void testXss() throws Exception {
var root = createDirectoryInCustomFs("testXss");
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "beginDelim%3C%3EEndDelim")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertTrue(response.body().contains("beginDelim%3C%3EEndDelim"));
assertTrue(response.body().contains("File not found"));
} finally {
server.stop(0);
}
}
static Path createDirectoryInCustomFs(String name) throws Exception {
var defaultFs = FileSystems.getDefault();
var fs = new CustomProvider(defaultFs.provider()).newFileSystem(defaultFs);
var dir = fs.getPath(name);
if (Files.notExists(dir)) {
Files.createDirectory(dir);
}
return dir.toAbsolutePath();
}
static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
static final String closeHTML = """
</body>
</html>
""";
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
static URI uri(HttpServer server, String path, String query) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.query(query)
.buildUnchecked();
}
static String getLastModified(Path path) throws IOException {
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
// --- Custom File System ---
static class CustomProvider extends FileSystemProvider {
private final ConcurrentHashMap<FileSystem, CustomFileSystem> map =
new ConcurrentHashMap<>();
private final FileSystemProvider defaultProvider;
public CustomProvider(FileSystemProvider provider) {
defaultProvider = provider;
}
@Override
public String getScheme() {
return defaultProvider.getScheme();
}
public FileSystem newFileSystem(FileSystem fs) {
return map.computeIfAbsent(fs, (sfs) ->
new CustomFileSystem(this, fs));
}
@Override
public FileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
FileSystem fs = defaultProvider.newFileSystem(uri, env);
return map.computeIfAbsent(fs, (sfs) ->
new CustomFileSystem(this, fs)
);
}
@Override
public FileSystem getFileSystem(URI uri) {
return map.get(defaultProvider.getFileSystem(uri));
}
@Override
public Path getPath(URI uri) {
Path p = defaultProvider.getPath(uri);
return map.get(defaultProvider.getFileSystem(uri)).wrap(p);
}
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
Path p = toCustomPath(path).unwrap();
return defaultProvider.newByteChannel(p, options, attrs);
}
@Override
public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) throws IOException {
throw new RuntimeException("not implemented");
}
@Override
public void createDirectory(Path dir, FileAttribute<?>... attrs) throws IOException {
Path p = toCustomPath(dir).unwrap();
defaultProvider.createDirectory(p, attrs);
}
@Override
public void delete(Path path) throws IOException {
Path p = toCustomPath(path).unwrap();
defaultProvider.delete(p);
}
@Override
public void copy(Path source, Path target, CopyOption... options) throws IOException {
Path sp = toCustomPath(source).unwrap();
Path tp = toCustomPath(target).unwrap();
defaultProvider.copy(sp, tp, options);
}
@Override
public void move(Path source, Path target, CopyOption... options)
throws IOException {
Path sp = toCustomPath(source).unwrap();
Path tp = toCustomPath(target).unwrap();
defaultProvider.move(sp, tp, options);
}
@Override
public boolean isSameFile(Path path, Path path2)
throws IOException {
Path p = toCustomPath(path).unwrap();
Path p2 = toCustomPath(path2).unwrap();
return defaultProvider.isSameFile(p, p2);
}
@Override
public boolean isHidden(Path path) throws IOException {
Path p = toCustomPath(path).unwrap();
return defaultProvider.isHidden(p);
}
@Override
public FileStore getFileStore(Path path) throws IOException {
Path p = toCustomPath(path).unwrap();
return defaultProvider.getFileStore(p);
}
@Override
public void checkAccess(Path path, AccessMode... modes) throws IOException {
Path p = toCustomPath(path).unwrap();
defaultProvider.checkAccess(p, modes);
}
@Override
public <V extends FileAttributeView> V getFileAttributeView(Path path,
Class<V> type,
LinkOption... options) {
Path p = toCustomPath(path).unwrap();
return defaultProvider.getFileAttributeView(p, type, options);
}
@Override
public <A extends BasicFileAttributes> A readAttributes(Path path,
Class<A> type,
LinkOption... options)
throws IOException {
Path p = toCustomPath(path).unwrap();
return defaultProvider.readAttributes(p, type, options);
}
@Override
public Map<String, Object> readAttributes(Path path,
String attributes,
LinkOption... options)
throws IOException {
Path p = toCustomPath(path).unwrap();
return defaultProvider.readAttributes(p, attributes, options);
}
@Override
public void setAttribute(Path path, String attribute,
Object value, LinkOption... options)
throws IOException {
Path p = toCustomPath(path).unwrap();
defaultProvider.setAttribute(p, attribute, options);
}
// Checks that the given file is a CustomPath
static CustomPath toCustomPath(Path obj) {
if (obj == null)
throw new NullPointerException();
if (!(obj instanceof CustomPath cp))
throw new ProviderMismatchException();
return cp;
}
}
static class CustomFileSystem extends FileSystem {
private final CustomProvider provider;
private final FileSystem delegate;
public CustomFileSystem(CustomProvider provider, FileSystem delegate) {
this.provider = provider;
this.delegate = delegate;
}
@Override
public FileSystemProvider provider() {
return provider;
}
@Override
public void close() throws IOException { delegate.close(); }
@Override
public boolean isOpen() {
return true;
}
@Override
public boolean isReadOnly() {
return false;
}
@Override
public String getSeparator() { return delegate.getSeparator(); }
@Override
public Iterable<Path> getRootDirectories() {
return null;
}
@Override
public Iterable<FileStore> getFileStores() {
return null;
}
@Override
public Set<String> supportedFileAttributeViews() {
return null;
}
@Override
public Path getPath(String first, String... more) {
return delegate.getPath(first, more);
}
@Override
public PathMatcher getPathMatcher(String syntaxAndPattern) {
return null;
}
@Override
public UserPrincipalLookupService getUserPrincipalLookupService() {
return null;
}
@Override
public WatchService newWatchService() throws IOException {
return null;
}
@Override
public String toString() {
return delegate.toString();
}
Path wrap(Path path) {
return (path != null) ? new CustomPath(this, path) : null;
}
Path unwrap(Path wrapper) {
if (wrapper == null)
throw new NullPointerException();
if (!(wrapper instanceof CustomPath cp))
throw new ProviderMismatchException();
return cp.unwrap();
}
}
static class CustomPath implements Path {
private final CustomFileSystem fs;
private final Path delegate;
CustomPath(CustomFileSystem fs, Path delegate) {
this.fs = fs;
this.delegate = delegate;
}
@Override
public FileSystem getFileSystem() {
return fs;
}
@Override
public boolean isAbsolute() {
return delegate.isAbsolute();
}
@Override
public Path getRoot() {
return fs.wrap(delegate.getRoot());
}
@Override
public Path getFileName() {
return null;
}
@Override
public Path getParent() {
return null;
}
@Override
public int getNameCount() {
return 0;
}
@Override
public Path getName(int index) {
return null;
}
@Override
public Path subpath(int beginIndex, int endIndex) {
return null;
}
@Override
public boolean startsWith(Path other) {
return delegate.startsWith(other);
}
@Override
public boolean endsWith(Path other) {
return false;
}
@Override
public Path normalize() {
return fs.wrap(delegate.normalize());
}
@Override
public Path resolve(Path other) {
return fs.wrap(delegate.resolve(fs.unwrap(other)));
}
@Override
public Path relativize(Path other) {
return null;
}
@Override
public URI toUri() {
return delegate.toUri();
}
@Override
public Path toAbsolutePath() {
return fs.wrap(delegate.toAbsolutePath());
}
@Override
public Path toRealPath(LinkOption... options) throws IOException {
return null;
}
@Override
public WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) throws IOException {
return null;
}
@Override
public int compareTo(Path other) {
return 0;
}
Path unwrap() {
return delegate;
}
}
}

View File

@ -0,0 +1,231 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for FileServerHandler
* @run testng FileServerHandlerTest
*/
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpPrincipal;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class FileServerHandlerTest {
static final Path CWD = Path.of(".").toAbsolutePath();
static final Class<RuntimeException> RE = RuntimeException.class;
@DataProvider
public Object[][] notAllowedMethods() {
var l = List.of("POST", "PUT", "DELETE", "TRACE", "OPTIONS");
return l.stream().map(s -> new Object[] { s }).toArray(Object[][]::new);
}
@Test(dataProvider = "notAllowedMethods")
public void testNotAllowedRequestMethod(String requestMethod) throws Exception {
var handler = SimpleFileServer.createFileHandler(CWD);
var exchange = new MethodHttpExchange(requestMethod);
handler.handle(exchange);
assertEquals(exchange.rCode, 405);
assertEquals(exchange.getResponseHeaders().getFirst("allow"), "HEAD, GET");
}
@DataProvider
public Object[][] notImplementedMethods() {
var l = List.of("GARBAGE", "RUBBISH", "TRASH", "FOO", "BAR");
return l.stream().map(s -> new Object[] { s }).toArray(Object[][]::new);
}
@Test(dataProvider = "notImplementedMethods")
public void testNotImplementedRequestMethod(String requestMethod) throws Exception {
var handler = SimpleFileServer.createFileHandler(CWD);
var exchange = new MethodHttpExchange(requestMethod);
handler.handle(exchange);
assertEquals(exchange.rCode, 501);
}
// 301 and 404 response codes tested in SimpleFileServerTest
@Test
public void testThrowingExchange() {
var h = SimpleFileServer.createFileHandler(CWD);
{
var exchange = new ThrowingHttpExchange("GET") {
public InputStream getRequestBody() {
throw new RuntimeException("getRequestBody");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getRequestBody");
}
{
var exchange = new ThrowingHttpExchange("GET") {
public Headers getResponseHeaders() {
throw new RuntimeException("getResponseHeaders");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getResponseHeaders");
}
{
var exchange = new ThrowingHttpExchange("GET") {
public void sendResponseHeaders(int rCode, long responseLength) {
throw new RuntimeException("sendResponseHeaders");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "sendResponseHeaders");
}
{
var exchange = new ThrowingHttpExchange("GET") {
public OutputStream getResponseBody() {
throw new RuntimeException("getResponseBody");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getResponseBody");
}
{
var exchange = new ThrowingHttpExchange("GET") {
public void close() {
throw new RuntimeException("close");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "close");
}
}
static class ThrowingHttpExchange extends StubHttpExchange {
private final String method;
volatile int rCode;
volatile long responseLength;
volatile Headers responseHeaders;
volatile Headers requestHeaders;
volatile InputStream requestBody;
ThrowingHttpExchange(String method) {
this.method = method;
responseHeaders = new Headers();
requestHeaders = new Headers();
requestBody = new ByteArrayInputStream(new byte[]{});
}
@Override public String getRequestMethod() { return method; }
@Override public Headers getResponseHeaders() { return responseHeaders; }
@Override public Headers getRequestHeaders() { return requestHeaders; }
@Override public InputStream getRequestBody() { return requestBody; }
@Override public URI getRequestURI() { return URI.create("/"); }
@Override public OutputStream getResponseBody() {
return OutputStream.nullOutputStream();
}
@Override public void sendResponseHeaders(int rCode, long responseLength) {
this.rCode = rCode;
this.responseLength = responseLength;
}
@Override public HttpContext getHttpContext() {
return new HttpContext() {
@Override public HttpHandler getHandler() { return null; }
@Override public void setHandler(HttpHandler handler) { }
@Override public String getPath() {
return "/";
}
@Override public HttpServer getServer() {
return null;
}
@Override public Map<String, Object> getAttributes() {
return null;
}
@Override public List<Filter> getFilters() {
return null;
}
@Override public Authenticator setAuthenticator(Authenticator auth) {
return null;
}
@Override public Authenticator getAuthenticator() {
return null;
}
};
}
}
static class MethodHttpExchange extends StubHttpExchange {
private final String method;
volatile int rCode;
volatile long responseLength;
volatile Headers responseHeaders;
volatile InputStream requestBody;
MethodHttpExchange(String method) {
this.method = method;
responseHeaders = new Headers();
requestBody = InputStream.nullInputStream();
}
@Override public String getRequestMethod() { return method; }
@Override public Headers getResponseHeaders() { return responseHeaders; }
@Override public InputStream getRequestBody() { return requestBody; }
@Override public void sendResponseHeaders(int rCode, long responseLength) {
this.rCode = rCode;
this.responseLength = responseLength;
}
}
static class StubHttpExchange extends HttpExchange {
@Override public Headers getRequestHeaders() { return null; }
@Override public Headers getResponseHeaders() { return null; }
@Override public URI getRequestURI() { return null; }
@Override public String getRequestMethod() { return null; }
@Override public void close() { }
@Override public InputStream getRequestBody() { return null; }
@Override public OutputStream getResponseBody() { return null; }
@Override public HttpContext getHttpContext() { return null; }
@Override public void sendResponseHeaders(int rCode, long responseLength) { }
@Override public InetSocketAddress getRemoteAddress() { return null; }
@Override public int getResponseCode() { return 0; }
@Override public InetSocketAddress getLocalAddress() { return null; }
@Override public String getProtocol() { return null; }
@Override public Object getAttribute(String name) { return null; }
@Override public void setAttribute(String name, Object value) { }
@Override public void setStreams(InputStream i, OutputStream o) { }
@Override public HttpPrincipal getPrincipal() { return null; }
}
}

View File

@ -0,0 +1,362 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for HttpHandlers
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run testng/othervm HttpHandlersTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdk.test.lib.net.URIBuilder;
import com.sun.net.httpserver.*;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.*;
public class HttpHandlersTest {
static final Class<NullPointerException> NPE = NullPointerException.class;
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
static final Class<RuntimeException> RE = RuntimeException.class;
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
}
@Test
public void testNull() {
final var handler = new TestHandler();
assertThrows(NPE, () -> HttpHandlers.handleOrElse(null, handler, new TestHandler()));
assertThrows(NPE, () -> HttpHandlers.handleOrElse(p -> true, null, handler));
assertThrows(NPE, () -> HttpHandlers.handleOrElse(p -> true, handler, null));
final var headers = new Headers();
final var body = "";
assertThrows(NPE, () -> HttpHandlers.of(200, null, body));
assertThrows(NPE, () -> HttpHandlers.of(200, headers, null));
}
@Test
public void testOfStatusCode() {
final var headers = new Headers();
final var body = "";
assertThrows(IAE, () -> HttpHandlers.of(99, headers, body));
assertThrows(IAE, () -> HttpHandlers.of(1000, headers, body));
}
@Test
public void testOfNoBody() throws Exception {
var handler = HttpHandlers.of(200, Headers.of("foo", "bar"), "");
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertTrue(response.headers().map().containsKey("date"));
assertEquals(response.headers().firstValue("foo").get(), "bar");
assertEquals(response.headers().firstValue("content-length").get(), "0");
assertEquals(response.headers().map().size(), 3);
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@Test
public void testOfWithBody() throws Exception {
var handler = HttpHandlers.of(200, Headers.of("foo", "bar"), "hello world");
var expectedLength = Integer.toString("hello world".getBytes(UTF_8).length);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertTrue(response.headers().map().containsKey("date"));
assertEquals(response.headers().firstValue("foo").get(), "bar");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().map().size(), 3);
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "hello world");
} finally {
server.stop(0);
}
}
@Test
public void testOfHeadRequest() throws Exception {
var handler = HttpHandlers.of(200, Headers.of("content-length", "999"), "hello world");
var expectedLength = Integer.toString("hello world".getBytes(UTF_8).length);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ""))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertTrue(response.headers().map().containsKey("date"));
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().map().size(), 2);
assertEquals(response.statusCode(), 200);
} finally {
server.stop(0);
}
}
@Test
public void testOfOverwriteHeaders() throws Exception {
var headers = Headers.of("content-length", "1000", "date", "12345");
var handler = HttpHandlers.of(200, headers, "hello world");
var expectedLength = Integer.toString("hello world".getBytes(UTF_8).length);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertNotEquals(response.headers().firstValue("date").get(), "12345");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().map().size(), 2);
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "hello world");
} finally {
server.stop(0);
}
}
@DataProvider
public Object[][] responseBodies() {
return new Object[][] { {"hello world"}, {""} };
}
@Test(dataProvider = "responseBodies")
public void testOfThrowingExchange(String body) {
var h = HttpHandlers.of(200, Headers.of(), body);
{
var exchange = new ThrowingHttpExchange() {
public InputStream getRequestBody() {
throw new RuntimeException("getRequestBody");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getRequestBody");
}
{
var exchange = new ThrowingHttpExchange() {
public Headers getResponseHeaders() {
throw new RuntimeException("getResponseHeaders");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getResponseHeaders");
}
{
var exchange = new ThrowingHttpExchange() {
public void sendResponseHeaders(int rCode, long responseLength) {
throw new RuntimeException("sendResponseHeaders");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "sendResponseHeaders");
}
{
var exchange = new ThrowingHttpExchange() {
public OutputStream getResponseBody() {
throw new RuntimeException("getResponseBody");
}
};
if (!body.isEmpty()) { // getResponseBody not called if no responseBody
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "getResponseBody");
}
}
{
var exchange = new ThrowingHttpExchange() {
public void close() {
throw new RuntimeException("close");
}
};
var t = expectThrows(RE, () -> h.handle(exchange));
assertEquals(t.getMessage(), "close");
}
}
@Test
public void testHandleOrElseTrue() throws Exception {
var h1 = new TestHandler("TestHandler-1");
var h2 = new TestHandler("TestHandler-2");
var handler = HttpHandlers.handleOrElse(p -> true, h1, h2);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "TestHandler-1");
} finally {
server.stop(0);
}
}
@Test
public void testHandleOrElseFalse() throws Exception {
var h1 = new TestHandler("TestHandler-1");
var h2 = new TestHandler("TestHandler-2");
var handler = HttpHandlers.handleOrElse(p -> false, h1, h2);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "TestHandler-2");
} finally {
server.stop(0);
}
}
@Test
public void testHandleOrElseNested() throws Exception {
var h1 = new TestHandler("TestHandler-1");
var h2 = new TestHandler("TestHandler-2");
var h3 = new TestHandler("TestHandler-3");
var h4 = HttpHandlers.handleOrElse(p -> false, h1, h2);
var handler = HttpHandlers.handleOrElse(p -> false, h3, h4);
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", handler);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "TestHandler-2");
} finally {
server.stop(0);
}
}
/**
* A test handler that discards the request and returns its name
*/
static class TestHandler implements HttpHandler {
final String name;
TestHandler(String name) { this.name = name; }
TestHandler() { this("no name"); }
@Override
public void handle(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody();
OutputStream os = exchange.getResponseBody()) {
is.readAllBytes();
var resp = name.getBytes(UTF_8);
exchange.sendResponseHeaders(200, resp.length);
os.write(resp);
}
}
}
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
static class ThrowingHttpExchange extends HttpExchange {
static final String requestMethod = "GET";
volatile int responseCode;
volatile long responseLength;
volatile Headers responseHeaders;
volatile InputStream requestBody;
ThrowingHttpExchange() {
responseHeaders = new Headers();
this.requestBody = InputStream.nullInputStream();
}
@Override public Headers getResponseHeaders() { return responseHeaders; }
@Override public InputStream getRequestBody() { return requestBody; }
@Override public void sendResponseHeaders(int rCode, long responseLength) {
this.responseCode = rCode;
this.responseLength = responseLength;
}
@Override public OutputStream getResponseBody() {
return OutputStream.nullOutputStream();
}
@Override public String getRequestMethod() { return requestMethod; }
@Override public Headers getRequestHeaders() { return null; }
@Override public URI getRequestURI() { return null; }
@Override public HttpContext getHttpContext() { return null; }
@Override public void close() { }
@Override public InetSocketAddress getRemoteAddress() { return null; }
@Override public int getResponseCode() { return 0; }
@Override public InetSocketAddress getLocalAddress() { return null; }
@Override public String getProtocol() { return null; }
@Override public Object getAttribute(String name) { return null; }
@Override public void setAttribute(String name, Object value) { }
@Override public void setStreams(InputStream i, OutputStream o) { }
@Override public HttpPrincipal getPrincipal() { return null; }
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Test for HttpsServer::create
* @library /test/lib
* @build jdk.test.lib.Platform jdk.test.lib.net.URIBuilder
* @run testng/othervm HttpsServerTest
*/
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import javax.net.ssl.SSLContext;
import jdk.test.lib.net.SimpleSSLContext;
import jdk.test.lib.net.URIBuilder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
public class HttpsServerTest {
static final Class<NullPointerException> NPE = NullPointerException.class;
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
SSLContext sslContext;
@BeforeTest
public void setup() throws IOException {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
sslContext = new SimpleSSLContext().get();
SSLContext.setDefault(sslContext);
}
@Test
public void testNull() {
assertThrows(NPE, () -> HttpsServer.create(null, 0, null, new Handler()));
assertThrows(NPE, () -> HttpsServer.create(null, 0, "/", null));
assertThrows(NPE, () -> HttpsServer.create(null, 0, "/", new Handler(), (Filter)null));
assertThrows(NPE, () -> HttpsServer.create(null, 0, "/", new Handler(), new Filter[]{null}));
}
@Test
public void testCreate() throws IOException {
assertNull(HttpsServer.create().getAddress());
final var s1 = HttpsServer.create(null, 0);
assertNull(s1.getAddress());
s1.bind((LOOPBACK_ADDR), 0);
assertEquals(s1.getAddress().getAddress(), LOOPBACK_ADDR.getAddress());
final var s2 = HttpsServer.create(null, 0, "/foo/", new Handler());
assertNull(s2.getAddress());
s2.bind(LOOPBACK_ADDR, 0);
assertEquals(s2.getAddress().getAddress(), LOOPBACK_ADDR.getAddress());
s2.removeContext("/foo/"); // throws if context doesn't exist
}
@Test
public void testExchange() throws Exception {
var filter = new Filter();
var server = HttpsServer.create(LOOPBACK_ADDR, 0, "/test", new Handler(), filter);
server.setHttpsConfigurator(new HttpsConfigurator(sslContext));
server.start();
try {
var client = HttpClient.newBuilder()
.proxy(NO_PROXY)
.sslContext(sslContext)
.build();
var request = HttpRequest.newBuilder(uri(server, "/test")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "hello world");
assertEquals(response.headers().firstValue("content-length").get(),
Integer.toString("hello world".length()));
assertEquals(response.statusCode(), filter.responseCode.get().intValue());
} finally {
server.stop(0);
}
}
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.scheme("https")
.host("localhost")
.port(server.getAddress().getPort())
.path(path)
.buildUnchecked();
}
/**
* A test handler that discards the request and sends a response
*/
static class Handler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody();
OutputStream os = exchange.getResponseBody()) {
is.readAllBytes();
var resp = "hello world".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, resp.length);
os.write(resp);
}
}
}
/**
* A test post-processing filter that captures the response code
*/
static class Filter extends com.sun.net.httpserver.Filter {
final CompletableFuture<Integer> responseCode = new CompletableFuture<>();
@Override
public void doFilter(HttpExchange exchange, Chain chain) throws IOException {
chain.doFilter(exchange);
responseCode.complete(exchange.getResponseCode());
}
@Override
public String description() {
return "HttpsServerTest Filter";
}
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright (c) 2021, 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.
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import jdk.test.lib.net.URIBuilder;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.file.StandardOpenOption.CREATE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
/*
* @test
* @summary Test idempotency and commutativity of responses with an exhaustive
* set of binary request sequences
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run testng/othervm IdempotencyAndCommutativityTest
*/
public class IdempotencyAndCommutativityTest {
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final String FILE_NAME = "file.txt";
static final String DIR_NAME = "";
static final String MISSING_FILE_NAME = "doesNotExist";
static HttpServer server;
static HttpClient client;
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws IOException {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
Path root = Files.createDirectories(CWD.resolve("testDirectory"));
Files.writeString(root.resolve(FILE_NAME), "some text", CREATE);
client = HttpClient.newBuilder().proxy(NO_PROXY).build();
server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, SimpleFileServer.OutputLevel.VERBOSE);
server.start();
}
// Container of expected response state for a given request
record ExchangeValues(String method, String resource, int respCode, String contentType) {}
// Creates an exhaustive set of binary exchange sequences
@DataProvider
public Object[][] allBinarySequences() {
final List<ExchangeValues> sequences = List.of(
new ExchangeValues("GET", FILE_NAME, 200, "text/plain"),
new ExchangeValues("GET", DIR_NAME, 200, "text/html; charset=UTF-8"),
new ExchangeValues("GET", MISSING_FILE_NAME, 404, "text/html; charset=UTF-8"),
new ExchangeValues("HEAD", FILE_NAME, 200, "text/plain"),
new ExchangeValues("HEAD", DIR_NAME, 200, "text/html; charset=UTF-8"),
new ExchangeValues("HEAD", MISSING_FILE_NAME, 404, "text/html; charset=UTF-8"),
new ExchangeValues("UNKNOWN", FILE_NAME, 501, null),
new ExchangeValues("UNKNOWN", DIR_NAME, 501, null),
new ExchangeValues("UNKNOWN", MISSING_FILE_NAME, 501, null)
);
return sequences.stream() // cartesian product
.flatMap(s1 -> sequences.stream().map(s2 -> new ExchangeValues[] { s1, s2 }))
.toArray(Object[][]::new);
}
@Test(dataProvider = "allBinarySequences")
public void testBinarySequences(ExchangeValues e1, ExchangeValues e2) throws Exception {
System.out.println("---");
System.out.println(e1);
executeExchange(e1);
System.out.println(e2);
executeExchange(e2);
}
private static void executeExchange(ExchangeValues e) throws Exception {
var request = HttpRequest.newBuilder(uri(server, e.resource()))
.method(e.method(), HttpRequest.BodyPublishers.noBody())
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), e.respCode());
if (e.contentType != null) {
assertEquals(response.headers().firstValue("content-type").get(), e.contentType());
} else {
assertTrue(response.headers().firstValue("content-type").isEmpty());
}
}
@AfterTest
public static void teardown() {
server.stop(0);
}
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
}

View File

@ -0,0 +1,359 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests the FileServerHandler's mapping of request URI path to file
* system path
* @library /test/lib
* @build jdk.test.lib.Platform jdk.test.lib.net.URIBuilder
* @run testng/othervm MapToPathTest
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpHandlers;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.net.URIBuilder;
import jdk.test.lib.util.FileUtils;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static java.lang.System.out;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.file.StandardOpenOption.CREATE;
import static org.testng.Assert.assertEquals;
public class MapToPathTest {
static final Path CWD = Path.of(".").toAbsolutePath();
static final Path TEST_DIR = CWD.resolve("MapToPathTest").normalize();
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final Filter OUTPUT_FILTER = SimpleFileServer.createOutputFilter(out, OutputLevel.VERBOSE);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws IOException {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
createDirectories(TEST_DIR);
}
private void createDirectories(Path testDir) throws IOException {
// Create directory tree:
//
// |-- TEST_DIR
// |-- foo
// |-- bar
// |-- baz
// |-- file.txt
// |-- file.txt
// |-- foobar
// |-- file.txt
// |-- file.txt
Files.createDirectories(TEST_DIR);
Stream.of("foo", "foobar", "foo/bar/baz").forEach(s -> {
try {
Path p = testDir.resolve(s);
Files.createDirectories(p);
Files.writeString(p.resolve("file.txt"), s, CREATE);
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
});
Files.writeString(testDir.resolve("file.txt"), "testdir", CREATE);
}
@Test
public void test() throws Exception {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
{
var handler = SimpleFileServer.createFileHandler(TEST_DIR);
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, OUTPUT_FILTER);
server.start();
try {
var req1 = HttpRequest.newBuilder(uri(server, "/")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(res1.headers().firstValue("content-length").get(), Long.toString(257L));
assertEquals(res1.headers().firstValue("last-modified").get(), getLastModified(TEST_DIR));
var req2 = HttpRequest.newBuilder(uri(server, "/../")).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 404); // cannot escape root
var req3 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/c://")).build();
var res3 = client.send(req3, BodyHandlers.ofString());
assertEquals(res3.statusCode(), 404); // not found
var req4 = HttpRequest.newBuilder(uri(server, "/foo/file:" + TEST_DIR.getParent())).build();
var res4 = client.send(req4, BodyHandlers.ofString());
assertEquals(res4.statusCode(), 404); // not found
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar/\\..\\../")).build();
var res5 = client.send(req5, BodyHandlers.ofString());
assertEquals(res5.statusCode(), 404); // not found
var req6 = HttpRequest.newBuilder(uri(server, "/foo")).build();
var res6 = client.send(req6, BodyHandlers.ofString());
assertEquals(res6.statusCode(), 301); // redirect
assertEquals(res6.headers().firstValue("content-length").get(), "0");
assertEquals(res6.headers().firstValue("location").get(), "/foo/");
} finally {
server.stop(0);
}
}
{
var handler = SimpleFileServer.createFileHandler(TEST_DIR);
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/browse/", handler, OUTPUT_FILTER);
server.start();
try {
var req1 = HttpRequest.newBuilder(uri(server, "/browse/file.txt")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "testdir");
assertEquals(res1.headers().firstValue("content-type").get(), "text/plain");
assertEquals(res1.headers().firstValue("content-length").get(), Long.toString(7L));
assertEquals(res1.headers().firstValue("last-modified").get(), getLastModified(TEST_DIR.resolve("file.txt")));
var req2 = HttpRequest.newBuilder(uri(server, "/store/file.txt")).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 404); // no context found
} finally {
server.stop(0);
}
}
{
// Test "/foo/" context (with trailing slash)
var handler = SimpleFileServer.createFileHandler(TEST_DIR.resolve("foo"));
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/foo/", handler, OUTPUT_FILTER);
server.start();
try {
var req1 = HttpRequest.newBuilder(uri(server, "/foo/file.txt")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "foo");
assertEquals(res1.headers().firstValue("content-type").get(), "text/plain");
assertEquals(res1.headers().firstValue("content-length").get(), Long.toString(3L));
assertEquals(res1.headers().firstValue("last-modified").get(), getLastModified(TEST_DIR.resolve("foo").resolve("file.txt")));
var req2 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 404); // no context found
var req3 = HttpRequest.newBuilder(uri(server, "/foo/../foobar/file.txt")).build();
var res3 = client.send(req3, BodyHandlers.ofString());
assertEquals(res3.statusCode(), 404); // cannot escape context
var req4 = HttpRequest.newBuilder(uri(server, "/foo/../..")).build();
var res4 = client.send(req4, BodyHandlers.ofString());
assertEquals(res4.statusCode(), 404); // cannot escape root
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar")).build();
var res5 = client.send(req5, BodyHandlers.ofString());
assertEquals(res5.statusCode(), 301); // redirect
assertEquals(res5.headers().firstValue("content-length").get(), "0");
assertEquals(res5.headers().firstValue("location").get(), "/foo/bar/");
} finally {
server.stop(0);
}
}
{
// Test "/foo" context (without trailing slash)
var handler = SimpleFileServer.createFileHandler(TEST_DIR.resolve("foo"));
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/foo", handler, OUTPUT_FILTER);
server.start();
try {
var req1 = HttpRequest.newBuilder(uri(server, "/foo/file.txt")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "foo");
assertEquals(res1.headers().firstValue("content-type").get(), "text/plain");
assertEquals(res1.headers().firstValue("content-length").get(), Long.toString(3L));
assertEquals(res1.headers().firstValue("last-modified").get(), getLastModified(TEST_DIR.resolve("foo").resolve("file.txt")));
var req2 = HttpRequest.newBuilder(uri(server, "/foobar/")).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 404); // handler prevents mapping to /foo/bar
var req3 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
var res3 = client.send(req3, BodyHandlers.ofString());
assertEquals(res3.statusCode(), 404); // handler prevents mapping to /foo/bar/file.txt
var req4 = HttpRequest.newBuilder(uri(server, "/file.txt")).build();
var res4 = client.send(req4, BodyHandlers.ofString());
assertEquals(res4.statusCode(), 404);
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar")).build();
var res5 = client.send(req5, BodyHandlers.ofString());
assertEquals(res5.statusCode(), 301); // redirect
assertEquals(res5.headers().firstValue("content-length").get(), "0");
assertEquals(res5.headers().firstValue("location").get(), "/foo/bar/");
var req6 = HttpRequest.newBuilder(uri(server, "/foo")).build();
var res6 = client.send(req6, BodyHandlers.ofString());
assertEquals(res6.statusCode(), 301); // redirect
assertEquals(res6.headers().firstValue("content-length").get(), "0");
assertEquals(res6.headers().firstValue("location").get(), "/foo/");
} finally {
server.stop(0);
}
}
}
// Tests with a mixture of in-memory and file handlers.
@Test
public void multipleContexts() throws Exception {
var rootHandler = HttpHandlers.of(200, Headers.of(), "root response body");
var fooHandler = SimpleFileServer.createFileHandler(TEST_DIR.resolve("foo"));
var foobarHandler = SimpleFileServer.createFileHandler(TEST_DIR.resolve("foobar"));
var barHandler = HttpHandlers.of(200, Headers.of(), "bar response body");
var server = HttpServer.create(LOOPBACK_ADDR, 0);
server.createContext("/", rootHandler);
server.createContext("/foo/", fooHandler);
server.createContext("/bar/", barHandler);
server.createContext("/foobar/", foobarHandler);
server.start();
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
try {
for (String uriPath : List.of("/", "/blah", "/xyz/t/z", "/txt") ) {
out.println("uri.Path=" + uriPath);
var req1 = HttpRequest.newBuilder(uri(server, uriPath)).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "root response body");
}
{
var req1 = HttpRequest.newBuilder(uri(server, "/foo/file.txt")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "foo");
var req2 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/file.txt")).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 200);
assertEquals(res2.body(), "foo/bar/baz");
}
{
var req1 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "foobar");
}
for (String uriPath : List.of("/bar/", "/bar/t", "/bar/t/z", "/bar/index.html") ) {
out.println("uri.Path=" + uriPath);
var req1 = HttpRequest.newBuilder(uri(server, uriPath)).build();
var res1 = client.send(req1, BodyHandlers.ofString());
assertEquals(res1.statusCode(), 200);
assertEquals(res1.body(), "bar response body");
}
} finally {
server.stop(0);
}
}
// Tests requests with queries, which are simply ignored by the handler
@Test
public void requestWithQuery() throws Exception {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var handler = SimpleFileServer.createFileHandler(TEST_DIR);
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, OUTPUT_FILTER);
server.start();
try {
for (String query : List.of("x=y", "x=", "xxx", "#:?") ) {
out.println("uri.Query=" + query);
var req = HttpRequest.newBuilder(uri(server, "", query)).build();
var res = client.send(req, BodyHandlers.ofString());
assertEquals(res.statusCode(), 200);
assertEquals(res.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(res.headers().firstValue("content-length").get(), Long.toString(257L));
assertEquals(res.headers().firstValue("last-modified").get(), getLastModified(TEST_DIR));
}
} finally {
server.stop(0);
}
}
@AfterTest
public void teardown() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
}
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path(path)
.buildUnchecked();
}
static URI uri(HttpServer server, String path, String query) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path(path)
.query(query)
.buildUnchecked();
}
static String getLastModified(Path path) throws IOException {
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}

View File

@ -0,0 +1,342 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for OutputFilter
* @modules java.base/sun.net.www:+open
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run testng/othervm -Djdk.httpclient.redirects.retrylimit=1 OutputFilterTest
*/
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.List;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.net.URIBuilder;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.*;
import static com.sun.net.httpserver.SimpleFileServer.OutputLevel.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
public class OutputFilterTest {
static final Class<NullPointerException> NPE = NullPointerException.class;
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
static final Class<IOException> IOE = IOException.class;
static final OutputStream OUT = new ByteArrayOutputStream();
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger logger = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
logger.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
logger.addHandler(ch);
}
}
@Test
public void testNull() {
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(null, null));
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(null, VERBOSE));
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(OUT, null));
}
@Test
public void testDescription() {
var filter = SimpleFileServer.createOutputFilter(OUT, VERBOSE);
assertEquals(filter.description(), "HttpExchange OutputFilter (outputLevel: VERBOSE)");
filter = SimpleFileServer.createOutputFilter(OUT, INFO);
assertEquals(filter.description(), "HttpExchange OutputFilter (outputLevel: INFO)");
}
@Test
public void testNONE() {
assertThrows(IAE, () -> SimpleFileServer.createOutputFilter(OUT, NONE));
}
/**
* Confirms that the output filter produces the expected output for a
* successful exchange (with the request-path attribute set.)
*/
@Test
public void testExchange() throws Exception {
var baos = new ByteArrayOutputStream();
var handler = new RequestPathHandler();
var filter = SimpleFileServer.createOutputFilter(baos, VERBOSE);
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, filter);
server.start();
try (baos) {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().map().size(), 3);
assertEquals(response.body(), "hello world");
} finally {
server.stop(0);
baos.flush();
var filterOutput = baos.toString(UTF_8);
var pattern = Pattern.compile("""
127\\.0\\.0\\.1 - - \\[[\\s\\S]+] "GET / HTTP/1\\.1" 200 -
Resource requested: /foo/bar
(>[\\s\\S]+:[\\s\\S]+)+
>
(<[\\s\\S]+:[\\s\\S]+)+
<
""".replaceAll("\n", System.lineSeparator()));
assertTrue(pattern.matcher(filterOutput).matches());
/*
* Expected output format:
* """
* 127.0.0.1 - - [06/Jul/2021:12:56:47 +0100] "GET / HTTP/1.1" 200 -
* Resource requested: /foo/bar
* > Connection: Upgrade, HTTP2-Settings
* > Http2-settings: AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA
* > Host: localhost:59146
* > Upgrade: h2c
* > User-agent: Java-http-client/18-internal
* > Content-length: 0
* >
* < Date: Tue, 06 Jul 2021 11:56:47 GMT
* < Content-length: 11
* <
* """;
*/
}
}
/**
* Confirms that the output filter produces the expected output for
* a successful exchange (without the request-path attribute set.)
*/
@Test
public void testExchangeWithoutRequestPath() throws Exception {
var baos = new ByteArrayOutputStream();
var handler = new NoRequestPathHandler();
var filter = SimpleFileServer.createOutputFilter(baos, VERBOSE);
var server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, filter);
server.start();
try (baos) {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().map().size(), 2);
assertEquals(response.body(), "hello world");
} finally {
server.stop(0);
baos.flush();
var filterOutput = baos.toString(UTF_8);
var pattern = Pattern.compile("""
127\\.0\\.0\\.1 - - \\[[\\s\\S]+] "GET / HTTP/1\\.1" 200 -
(>[\\s\\S]+:[\\s\\S]+)+
>
(<[\\s\\S]+:[\\s\\S]+)+
<
""".replaceAll("\n", System.lineSeparator()));
assertTrue(pattern.matcher(filterOutput).matches());
/*
* Expected output:
* """
* 127.0.0.1 - - [12/Jul/2021:10:05:10 +0000] "GET / HTTP/1.1" 200 -
* > Connection: Upgrade, HTTP2-Settings
* > Http2-settings: AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA
* > Host: localhost:57931
* > Upgrade: h2c
* > User-agent: Java-http-client/18-internal
* > Content-length: 0
* >
* < Date: Mon, 12 Jul 2021 10:05:10 GMT
* < Content-length: 11
* <
* """;
*/
}
}
@DataProvider
public Object[][] throwingHandler() {
return new Object[][] {
{VERBOSE, "Error: server exchange handling failed: IOE ThrowingHandler" + System.lineSeparator()},
{INFO, "Error: server exchange handling failed: IOE ThrowingHandler" + System.lineSeparator()},
{NONE, ""}
};
}
/**
* Confirms that the output filter captures a throwable that is thrown
* during the exchange handling and prints the expected error message.
* The "httpclient.redirects.retrylimit" system property is set to 1 to
* prevent retries on the client side, which would result in more than one
* error message.
*/
@Test(dataProvider = "throwingHandler")
public void testExchangeThrowingHandler(OutputLevel level,
String expectedOutput) throws Exception {
var baos = new ByteArrayOutputStream();
var handler = new ThrowingHandler();
HttpServer server;
if (level.equals(NONE)) {
server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler);
} else {
var filter = SimpleFileServer.createOutputFilter(baos, level);
server = HttpServer.create(LOOPBACK_ADDR, 10, "/", handler, filter);
}
server.start();
try (baos) {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
assertThrows(IOE, () -> client.send(request, HttpResponse.BodyHandlers.ofString()));
} finally {
server.stop(0);
baos.flush();
assertEquals(baos.toString(UTF_8), expectedOutput);
}
}
/**
* Confirms that the output filter prints the expected message if the request
* URI cannot be resolved. This only applies if the filter is used in
* combination with the SimpleFileServer file-handler, which sets the
* necessary request-path attribute.
*/
@Test
public void testCannotResolveRequestURI() throws Exception {
var baos = new ByteArrayOutputStream();
var handler = SimpleFileServer.createFileHandler(Path.of(".").toAbsolutePath());
var filter = SimpleFileServer.createOutputFilter(baos, VERBOSE);
var server = HttpServer.create(LOOPBACK_ADDR, 0, "/", handler, filter);
server.start();
try (baos) {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile\u0000.txt")).build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().map().size(), 3);
} finally {
server.stop(0);
baos.flush();
var filterOutput = baos.toString(UTF_8);
var pattern = Pattern.compile("""
127\\.0\\.0\\.1 - - \\[[\\s\\S]+] "GET /aFile%00\\.txt HTTP/1\\.1" 404 -
Resource requested: could not resolve request URI path
(>[\\s\\S]+:[\\s\\S]+)+
>
(<[\\s\\S]+:[\\s\\S]+)+
<
""".replaceAll("\n", System.lineSeparator()));
assertTrue(pattern.matcher(filterOutput).matches());
}
}
// --- infra ---
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
/**
* A handler that sets the request-path attribute and a custom header
* and sends a response.
*/
static class RequestPathHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody();
OutputStream os = exchange.getResponseBody()) {
is.readAllBytes();
exchange.setAttribute("request-path", "/foo/bar");
exchange.getResponseHeaders().put("Foo", List.of("bar", "bar"));
var resp = "hello world".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, resp.length);
os.write(resp);
}
}
}
/**
* A handler that sets no request-path attribute and sends a response.
*/
static class NoRequestPathHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (InputStream is = exchange.getRequestBody();
OutputStream os = exchange.getResponseBody()) {
is.readAllBytes();
var resp = "hello world".getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, resp.length);
os.write(resp);
}
}
}
/**
* A handler that throws an IOException.
*/
static class ThrowingHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
try (exchange) {
throw new IOException("IOE ThrowingHandler");
}
}
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for Request
* @run testng RequestTest
*/
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import com.sun.net.httpserver.*;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertThrows;
public class RequestTest {
@Test
public void testAddToEmpty() {
var headers = new Headers();
Request request = new TestHttpExchange(headers);
request = request.with("Foo", List.of("Bar"));
assertEquals(request.getRequestHeaders().size(), 1);
assertEquals(request.getRequestHeaders().get("Foo"), List.of("Bar"));
assertReadOnly(request.getRequestHeaders());
}
@Test
public void testAddition() {
var headers = new Headers();
headers.add("Foo", "Bar");
Request request = new TestHttpExchange(headers);
request = request.with("X-Foo", List.of("Bar"));
assertEquals(request.getRequestHeaders().size(), 2);
assertEquals(request.getRequestHeaders().get("Foo"), List.of("Bar"));
assertEquals(request.getRequestHeaders().get("X-Foo"), List.of("Bar"));
assertReadOnly(request.getRequestHeaders());
}
@Test
public void testAddWithExisting() {
final String headerName = "Foo";
var headers = new Headers();
headers.add(headerName, "Bar");
Request request = new TestHttpExchange(headers);
request = request.with(headerName, List.of("blahblahblah"));
assertEquals(request.getRequestHeaders().size(), 1);
assertEquals(request.getRequestHeaders().get(headerName), List.of("Bar"));
assertReadOnly(request.getRequestHeaders());
}
@Test
public void testAddSeveral() {
var headers = new Headers();
headers.add("Foo", "Bar");
Request request = new TestHttpExchange(headers);
request = request.with("Larry", List.of("a"))
.with("Curly", List.of("b"))
.with("Moe", List.of("c"));
assertEquals(request.getRequestHeaders().size(), 4);
assertEquals(request.getRequestHeaders().getFirst("Foo"), "Bar");
assertEquals(request.getRequestHeaders().getFirst("Larry"), "a");
assertEquals(request.getRequestHeaders().getFirst("Curly"), "b");
assertEquals(request.getRequestHeaders().getFirst("Moe" ), "c");
assertReadOnly(request.getRequestHeaders());
}
static final Class<UnsupportedOperationException> UOP = UnsupportedOperationException.class;
static void assertReadOnly(Headers headers) {
assertUnsupportedOperation(headers);
assertUnmodifiableCollection(headers);
assertUnmodifiableList(headers);
}
static void assertUnsupportedOperation(Headers headers) {
assertThrows(UOP, () -> headers.add("a", "b"));
assertThrows(UOP, () -> headers.compute("c", (k, v) -> List.of("c")));
assertThrows(UOP, () -> headers.computeIfAbsent("d", k -> List.of("d")));
assertThrows(UOP, () -> headers.computeIfPresent("Foo", (k, v) -> null));
assertThrows(UOP, () -> headers.merge("e", List.of("e"), (k, v) -> List.of("e")));
assertThrows(UOP, () -> headers.put("f", List.of("f")));
assertThrows(UOP, () -> headers.putAll(Map.of()));
assertThrows(UOP, () -> headers.putIfAbsent("g", List.of("g")));
assertThrows(UOP, () -> headers.remove("h"));
assertThrows(UOP, () -> headers.replace("i", List.of("i")));
assertThrows(UOP, () -> headers.replace("j", List.of("j"), List.of("j")));
assertThrows(UOP, () -> headers.replaceAll((k, v) -> List.of("k")));
assertThrows(UOP, () -> headers.set("l", "m"));
assertThrows(UOP, () -> headers.clear());
}
static void assertUnmodifiableCollection(Headers headers) {
var entry = new AbstractMap.SimpleEntry<>("n", List.of("n"));
assertThrows(UOP, () -> headers.values().remove(List.of("Bar")));
assertThrows(UOP, () -> headers.values().removeAll(List.of("Bar")));
assertThrows(UOP, () -> headers.keySet().remove("Foo"));
assertThrows(UOP, () -> headers.keySet().removeAll(List.of("Foo")));
assertThrows(UOP, () -> headers.entrySet().remove(entry));
assertThrows(UOP, () -> headers.entrySet().removeAll(List.of(entry)));
}
static void assertUnmodifiableList(Headers headers) {
assertThrows(UOP, () -> headers.get("Foo").remove(0));
assertThrows(UOP, () -> headers.get("foo").remove(0));
assertThrows(UOP, () -> headers.values().stream().findFirst().orElseThrow().remove(0));
assertThrows(UOP, () -> headers.entrySet().stream().findFirst().orElseThrow().getValue().remove(0));
}
static class TestHttpExchange extends StubHttpExchange {
final Headers headers;
TestHttpExchange(Headers headers) {
this.headers = headers;
}
@Override
public Headers getRequestHeaders() {
return headers;
}
}
static class StubHttpExchange extends HttpExchange {
@Override public Headers getRequestHeaders() { return null; }
@Override public Headers getResponseHeaders() { return null; }
@Override public URI getRequestURI() { return null; }
@Override public String getRequestMethod() { return null; }
@Override public HttpContext getHttpContext() { return null; }
@Override public void close() { }
@Override public InputStream getRequestBody() { return null; }
@Override public OutputStream getResponseBody() { return null; }
@Override public void sendResponseHeaders(int rCode, long responseLength) { }
@Override public InetSocketAddress getRemoteAddress() { return null; }
@Override public int getResponseCode() { return 0; }
@Override public InetSocketAddress getLocalAddress() { return null; }
@Override public String getProtocol() { return null; }
@Override public Object getAttribute(String name) { return null; }
@Override public void setAttribute(String name, Object value) { }
@Override public void setStreams(InputStream i, OutputStream o) { }
@Override public HttpPrincipal getPrincipal() { return null; }
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for FileServerHandler with SecurityManager
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run main/othervm/java.security.policy=SecurityManagerTestRead.policy -ea SecurityManagerTest true
* @run main/othervm/java.security.policy=SecurityManagerTestNoRead.policy -ea SecurityManagerTest false
* @run main/othervm -ea SecurityManagerTest true
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.AccessControlException;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.net.URIBuilder;
import jdk.test.lib.util.FileUtils;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
/**
* Tests the permission checks during the creation of a FileServerHandler.
*
* A FileServerHandler can only be created if a "read" FilePermission is
* granted for the root directory passed. The test consists of 3 runs:
* 1) security manager enabled and "read" FilePermission granted
* 2) security manager enabled and "read" FilePermission NOT granted
* 3) security manager NOT enabled
* 2) misses the required permissions to call many of the java.nio.file methods,
* the test works around this by reusing the test directory created in the
* previous run.
* */
public class SecurityManagerTest {
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
static final Path TEST_DIR = CWD.resolve("SecurityManagerTest");
static final InetSocketAddress LOOPBACK_ADDR =
new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
static boolean readPermitted;
static String lastModifiedDir;
static String lastModifiedFile;
public static void main(String[] args) throws Exception {
setupLogging();
readPermitted = Boolean.parseBoolean(args[0]);
if (readPermitted) {
createTestDir();
testDirectoryGET();
testFileGET();
} else { // no FilePermission "read" for TEST_DIR granted,
// assert handler cannot be created
testCreateHandler();
}
}
private static void setupLogging() {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
}
private static void createTestDir() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
Files.createDirectories(TEST_DIR);
var file = Files.writeString(TEST_DIR.resolve("aFile.txt"), "some text", CREATE);
lastModifiedDir = getLastModified(TEST_DIR);
lastModifiedFile = getLastModified(file);
}
private static void testDirectoryGET() throws Exception {
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
<li><a href="aFile.txt">aFile.txt</a></li>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assert response.statusCode() == 200;
assert response.body().equals(expectedBody);
assert response.headers().firstValue("content-type").get().equals("text/html; charset=UTF-8");
assert response.headers().firstValue("content-length").get().equals(expectedLength);
assert response.headers().firstValue("last-modified").get().equals(lastModifiedDir);
} finally {
server.stop(0);
}
}
private static void testFileGET() throws Exception {
var expectedBody = "some text";
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assert response.statusCode() == 200;
assert response.body().equals("some text");
assert response.headers().firstValue("content-type").get().equals("text/plain");
assert response.headers().firstValue("content-length").get().equals(expectedLength);
assert response.headers().firstValue("last-modified").get().equals(lastModifiedFile);
} finally {
server.stop(0);
}
}
@SuppressWarnings("removal")
private static void testCreateHandler(){
try {
SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.NONE);
throw new RuntimeException("Handler creation expected to fail");
} catch (AccessControlException expected) { }
try {
SimpleFileServer.createFileHandler(TEST_DIR);
throw new RuntimeException("Handler creation expected to fail");
} catch (AccessControlException expected) { }
}
static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
static final String closeHTML = """
</body>
</html>
""";
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
static String getLastModified(Path path) throws IOException {
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}

View File

@ -0,0 +1,39 @@
//
// Copyright (c) 2021, 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.
//
// for JTwork/classes/test/lib/jdk/test/lib/util/FileUtils.class
grant codeBase "file:${test.classes}/../../../../../../test/lib/-" {
permission java.util.PropertyPermission "*", "read";
};
grant codeBase "file:${test.classes}/*" {
permission java.net.URLPermission "http://localhost:*/*", "GET";
// for HTTP server
permission java.net.SocketPermission "localhost:*", "accept,resolve";
// for HTTP/1.1 server logging
permission java.util.logging.LoggingPermission "control";
permission java.util.PropertyPermission "*", "read";
};

View File

@ -0,0 +1,43 @@
//
// Copyright (c) 2021, 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.
//
// for JTwork/classes/test/lib/jdk/test/lib/util/FileUtils.class
grant codeBase "file:${test.classes}/../../../../../../test/lib/-" {
permission java.util.PropertyPermission "*", "read";
};
grant codeBase "file:${test.classes}/*" {
permission java.net.URLPermission "http://localhost:*/*", "GET";
// for test directory tree
permission java.io.FilePermission "${user.dir}${/}SecurityManagerTest", "read,write,delete";
permission java.io.FilePermission "${user.dir}${/}SecurityManagerTest/-", "read,write,delete";
// for HTTP server
permission java.net.SocketPermission "localhost:*", "accept,resolve";
// for HTTP/1.1 server logging
permission java.util.logging.LoggingPermission "control";
permission java.util.PropertyPermission "*", "read";
};

View File

@ -0,0 +1,211 @@
/*
* Copyright (c) 2021, 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.
*/
import java.io.IOException;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import jdk.test.lib.net.URIBuilder;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import sun.net.www.MimeTable;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
/*
* @test
* @summary Tests for MIME types in response headers
* @modules java.base/sun.net.www:+open
* @library /test/lib
* @build jdk.test.lib.net.URIBuilder
* @run testng/othervm ServerMimeTypesResolutionTest
*/
public class ServerMimeTypesResolutionTest {
static final Path CWD = Path.of(".").toAbsolutePath();
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final String FILE_NAME = "empty-file-of-type";
static final String UNKNOWN_FILE_EXTENSION = ".unknown-file-extension";
static final Properties SUPPORTED_MIME_TYPES = new Properties();
static final Set<String> UNSUPPORTED_FILE_EXTENSIONS = new HashSet<>();
static List<String> supportedFileExtensions;
static Path root;
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws Exception {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
getSupportedMimeTypes(SUPPORTED_MIME_TYPES);
supportedFileExtensions = getFileExtensions(SUPPORTED_MIME_TYPES);
root = createFileTreeFromMimeTypes(SUPPORTED_MIME_TYPES);
}
public static Properties getSupportedMimeTypes(Properties properties) throws IOException {
properties.load(MimeTable.class.getResourceAsStream("content-types.properties"));
return properties;
}
private static List<String> getFileExtensions(Properties input) {
return new ArrayList<>(getMimeTypesPerFileExtension(input).keySet());
}
private static Map<String,String> getMimeTypesPerFileExtension(Properties input) {
return input
.entrySet()
.stream()
.filter(entry -> ((String)entry.getValue()).contains("file_extensions"))
.flatMap(entry ->
Arrays.stream(
((String)deserialize((String) entry.getValue(), ";")
.get("file_extensions")).split(","))
.map(extension ->
Map.entry(extension, entry.getKey().toString())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
private static Path createFileTreeFromMimeTypes(Properties properties)
throws IOException {
final Path root = Files.createDirectory(CWD.resolve(ServerMimeTypesResolutionTest.class.getSimpleName()));
for (String extension : supportedFileExtensions) {
Files.createFile(root.resolve(toFileName(extension)));
}
Files.createFile(root.resolve(toFileName(UNKNOWN_FILE_EXTENSION)));
return root;
}
private static String toFileName(String extension) {
return "%s%s".formatted(FILE_NAME, extension);
}
protected static Properties deserialize(String serialized, String delimiter) {
try {
Properties properties = new Properties();
properties.load(
new StringReader(
Optional.ofNullable(delimiter)
.map(d -> serialized.replaceAll(delimiter, System.lineSeparator()))
.orElse(serialized)
)
);
return properties;
} catch (IOException exception) {
exception.printStackTrace();
throw new RuntimeException(("error while deserializing string %s " +
"to properties").formatted(serialized), exception);
}
}
@Test
public static void testMimeTypeHeaders() throws Exception {
final var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, SimpleFileServer.OutputLevel.VERBOSE);
server.start();
try {
final var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
final Map<String, String> mimeTypesPerFileExtension = getMimeTypesPerFileExtension(SUPPORTED_MIME_TYPES);
for (String extension : supportedFileExtensions) {
final String expectedMimeType = mimeTypesPerFileExtension.get(extension);
execute(server, client, extension, expectedMimeType);
}
execute(server, client, UNKNOWN_FILE_EXTENSION,"application/octet-stream");
} finally {
server.stop(0);
}
}
private static void execute(HttpServer server,
HttpClient client,
String extension,
String expectedMimeType)
throws IOException, InterruptedException {
final var uri = uri(server, toFileName(extension));
final var request = HttpRequest.newBuilder(uri).build();
final var response = client.send(request, HttpResponse.BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(),expectedMimeType);
}
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
@DataProvider
public static Object[][] commonExtensions() {
Set<String> extensions = Set.of(".aac", ".abw", ".arc", ".avi", ".azw", ".bin", ".bmp", ".bz",
".bz2", ".csh", ".css", ".csv", ".doc", ".docx",".eot", ".epub", ".gz", ".gif", ".htm", ".html", ".ico",
".ics", ".jar", ".jpeg", ".jpg", ".js", ".json", ".jsonld", ".mid", ".midi", ".mjs", ".mp3", ".cda",
".mp4", ".mpeg", ".mpkg", ".odp", ".ods", ".odt", ".oga", ".ogv", ".ogx", ".opus", ".otf", ".png",
".pdf", ".php", ".ppt", ".pptx", ".rar", ".rtf", ".sh", ".svg", ".swf", ".tar", ".tif", ".tiff", ".ts",
".ttf", ".txt", ".vsd", ".wav", ".weba", ".webm", ".webp", ".woff", ".woff2", ".xhtml", ".xls", ".xlsx",
".xml", ".xul", ".zip", ".3gp", ".3g2", ".7z");
return extensions.stream().map(e -> new Object[]{e}).toArray(Object[][]::new);
}
/**
* This is a one-off test to check which common file extensions are
* currently supported by the system-wide mime table returned by
* {@linkplain java.net.FileNameMap#getContentTypeFor(String) getContentTypeFor}.
*
* Source common mime types:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
*/
// @Test(dataProvider = "commonExtensions")
public static void testCommonExtensions(String extension) {
var contains = supportedFileExtensions.contains(extension);
if (!contains) UNSUPPORTED_FILE_EXTENSIONS.add(extension);
assertTrue(contains, "expecting %s to be present".formatted(extension));
}
// @AfterTest
public static void printUnsupportedFileExtensions() {
System.out.println("Unsupported file extensions: " + UNSUPPORTED_FILE_EXTENSIONS.size());
UNSUPPORTED_FILE_EXTENSIONS.forEach(System.out::println);
}
}

View File

@ -0,0 +1,703 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for SimpleFileServer
* @library /test/lib
* @build jdk.test.lib.Platform jdk.test.lib.net.URIBuilder
* @run testng/othervm SimpleFileServerTest
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.Platform;
import jdk.test.lib.net.URIBuilder;
import jdk.test.lib.util.FileUtils;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static org.testng.Assert.*;
public class SimpleFileServerTest {
static final Class<NullPointerException> NPE = NullPointerException.class;
static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class;
static final Class<UncheckedIOException> UIOE = UncheckedIOException.class;
static final Path CWD = Path.of(".").toAbsolutePath();
static final Path TEST_DIR = CWD.resolve("SimpleFileServerTest");
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws IOException {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
Files.createDirectories(TEST_DIR);
}
@Test
public void testFileGET() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testFileGET"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "some text");
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
} finally {
server.stop(0);
}
}
@Test
public void testDirectoryGET() throws Exception {
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
<li><a href="aFile.txt">aFile.txt</a></li>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testDirectoryGET"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testFileHEAD() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testFileHEAD"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt"))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@Test
public void testDirectoryHEAD() throws Exception {
var expectedLength = Integer.toString(
(openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
<li><a href="aFile.txt">aFile.txt</a></li>
</ul>
""" + closeHTML).getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testDirectoryHEAD"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ""))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@DataProvider
public Object[][] indexFiles() {
var fileContent = openHTML + """
<h1>This is an index file</h1>
""" + closeHTML;
var dirListing = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
return new Object[][] {
{"1", "index.html", "text/html", "116", fileContent, true},
{"2", "index.htm", "text/html", "116", fileContent, true},
{"3", "index.txt", "text/html; charset=UTF-8", "134", dirListing, false}
};
}
@Test(dataProvider = "indexFiles")
public void testDirectoryWithIndexGET(String id,
String filename,
String contentType,
String contentLength,
String expectedBody,
boolean serveIndexFile) throws Exception {
var root = Files.createDirectories(TEST_DIR.resolve("testDirectoryWithIndexGET"+id));
var lastModified = getLastModified(root);
if (serveIndexFile) {
var file = Files.writeString(root.resolve(filename), expectedBody, CREATE);
lastModified = getLastModified(file);
}
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), contentType);
assertEquals(response.headers().firstValue("content-length").get(), contentLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
if (serveIndexFile) {
Files.delete(root.resolve(filename));
}
}
}
@Test
public void testNotReadableFileGET() throws Exception {
if (!Platform.isWindows()) { // not applicable on Windows
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableFileGET"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
file.toFile().setReadable(false, false);
assert !Files.isReadable(file);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
file.toFile().setReadable(true, false);
}
}
}
@Test
public void testNotReadableSegmentGET() throws Exception {
if (!Platform.isWindows()) { // not applicable on Windows
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;dir&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testNotReadableSegmentGET"));
var dir = Files.createDirectory(root.resolve("dir"));
var file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE);
dir.toFile().setReadable(false, false);
assert !Files.isReadable(dir);
assert Files.isReadable(file);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "dir/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
dir.toFile().setReadable(true, false);
}
}
}
@Test
public void testInvalidRequestURIGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;aFile?#.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testInvalidRequestURIGET"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile?#.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testNotFoundGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testNotFoundGET"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testNotFoundHEAD() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testNotFoundHEAD"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt"))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), "");
} finally {
server.stop(0);
}
}
@Test
public void testSymlinkGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;symlink</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testSymlinkGET"));
var symlink = root.resolve("symlink");
var target = Files.writeString(root.resolve("target.txt"), "some text", CREATE);
Files.createSymbolicLink(symlink, target);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "symlink")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testSymlinkSegmentGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;symlink&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = Files.createDirectory(TEST_DIR.resolve("testSymlinkSegmentGET"));
var symlink = root.resolve("symlink");
var target = Files.createDirectory(root.resolve("target"));
Files.writeString(target.resolve("aFile.txt"), "some text", CREATE);
Files.createSymbolicLink(symlink, target);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "symlink/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testHiddenFileGET() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testHiddenFileGET"));
var file = createHiddenFile(root);
var fileName = file.getFileName().toString();
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;""" + fileName +
"""
</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, fileName)).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
@Test
public void testHiddenSegmentGET() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testHiddenSegmentGET"));
var file = createFileInHiddenDirectory(root);
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;.hiddenDirectory&#x2F;aFile.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ".hiddenDirectory/aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
}
}
private Path createHiddenFile(Path root) throws IOException {
Path file;
if (Platform.isWindows()) {
file = Files.createFile(root.resolve("aFile.txt"));
Files.setAttribute(file, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
} else {
file = Files.writeString(root.resolve(".aFile.txt"), "some text", CREATE);
}
assertTrue(Files.isHidden(file));
return file;
}
private Path createFileInHiddenDirectory(Path root) throws IOException {
Path dir;
Path file;
if (Platform.isWindows()) {
dir = Files.createDirectory(root.resolve("hiddenDirectory"));
Files.setAttribute(dir, "dos:hidden", true, LinkOption.NOFOLLOW_LINKS);
} else {
dir = Files.createDirectory(root.resolve(".hiddenDirectory"));
}
file = Files.writeString(dir.resolve("aFile.txt"), "some text", CREATE);
assertTrue(Files.isHidden(dir));
assertFalse(Files.isHidden(file));
return file;
}
@Test
public void testMovedPermanently() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testMovedPermanently"));
Files.createDirectory(root.resolve("aDirectory"));
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;aDirectory&#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
{
var client = HttpClient.newBuilder().proxy(NO_PROXY)
.followRedirects(HttpClient.Redirect.NEVER).build();
var uri = uri(server, "aDirectory");
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 301);
assertEquals(response.headers().firstValue("content-length").get(), "0");
assertEquals(response.headers().firstValue("location").get(), "/aDirectory/");
// tests that query component is preserved during redirect
var uri2 = uri(server, "aDirectory", "query");
var req2 = HttpRequest.newBuilder(uri2).build();
var res2 = client.send(req2, BodyHandlers.ofString());
assertEquals(res2.statusCode(), 301);
assertEquals(res2.headers().firstValue("content-length").get(), "0");
assertEquals(res2.headers().firstValue("location").get(), "/aDirectory/?query");
}
{ // tests that redirect to returned relative URI works
var client = HttpClient.newBuilder().proxy(NO_PROXY)
.followRedirects(HttpClient.Redirect.ALWAYS).build();
var uri = uri(server, "aDirectory");
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), expectedBody);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
}
} finally {
server.stop(0);
}
}
@Test
public void testNull() {
final var addr = InetSocketAddress.createUnresolved("foo", 8080);
final var path = Path.of("/tmp");
final var levl = OutputLevel.INFO;
assertThrows(NPE, () -> SimpleFileServer.createFileServer(null, null, null));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(null, null, levl));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(null, path, null));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(null, path, levl));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(addr, null, null));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(addr, null, levl));
assertThrows(NPE, () -> SimpleFileServer.createFileServer(addr, path, null));
assertThrows(NPE, () -> SimpleFileServer.createFileHandler(null));
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(null, null));
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(null, levl));
assertThrows(NPE, () -> SimpleFileServer.createOutputFilter(System.out, null));
}
@Test
public void testInitialSlashContext() {
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.INFO);
server.removeContext("/"); // throws if no context
server.stop(0);
}
@Test
public void testBound() {
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.INFO);
var boundAddr = server.getAddress();
server.stop(0);
assertTrue(boundAddr.getAddress() != null);
assertTrue(boundAddr.getPort() > 0);
}
@Test
public void testIllegalPath() throws Exception {
var addr = LOOPBACK_ADDR;
{ // not absolute
Path p = Path.of(".");
assert Files.isDirectory(p);
assert Files.exists(p);
assert !p.isAbsolute();
var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO));
assertTrue(iae.getMessage().contains("is not absolute"));
}
{ // not a directory
Path p = Files.createFile(TEST_DIR.resolve("aFile"));
assert !Files.isDirectory(p);
var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO));
assertTrue(iae.getMessage().contains("not a directory"));
}
{ // does not exist
Path p = TEST_DIR.resolve("doesNotExist");
assert !Files.exists(p);
var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO));
assertTrue(iae.getMessage().contains("does not exist"));
}
{ // not readable
if (!Platform.isWindows()) { // not applicable on Windows
Path p = Files.createDirectory(TEST_DIR.resolve("aDir"));
p.toFile().setReadable(false, false);
assert !Files.isReadable(p);
try {
var iae = expectThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO));
assertTrue(iae.getMessage().contains("not readable"));
} finally {
p.toFile().setReadable(true, false);
}
}
}
}
@Test
public void testUncheckedIOException() {
var addr = InetSocketAddress.createUnresolved("foo", 8080);
assertThrows(UIOE, () -> SimpleFileServer.createFileServer(addr, TEST_DIR, OutputLevel.INFO));
assertThrows(UIOE, () -> SimpleFileServer.createFileServer(addr, TEST_DIR, OutputLevel.VERBOSE));
}
@Test
public void testXss() throws Exception {
var root = Files.createDirectory(TEST_DIR.resolve("testXss"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "beginDelim%3C%3EEndDelim")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertTrue(response.body().contains("beginDelim%3C%3EEndDelim"));
assertTrue(response.body().contains("File not found"));
} finally {
server.stop(0);
}
}
@AfterTest
public void teardown() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
}
static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
static final String closeHTML = """
</body>
</html>
""";
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
static URI uri(HttpServer server, String path, String query) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.query(query)
.buildUnchecked();
}
static String getLastModified(Path path) throws IOException {
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}

View File

@ -0,0 +1,110 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Test to stress directory listings
* @library /test/lib
* @run testng/othervm StressDirListings
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Path;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.net.URIBuilder;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static java.lang.System.out;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static org.testng.Assert.assertEquals;
public class StressDirListings {
static final Path CWD = Path.of(".").toAbsolutePath();
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = false;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
HttpServer simpleFileServer;
@BeforeTest
public void setup() throws IOException {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
simpleFileServer = SimpleFileServer.createFileServer(LOOPBACK_ADDR, CWD, OutputLevel.VERBOSE);
simpleFileServer.start();
}
@AfterTest
public void teardown() {
simpleFileServer.stop(0);
}
// Enough to trigger FileSystemException: Too many open files (machine dependent)
static final int TIMES = 11_000;
/**
* Issues a large number of identical GET requests sequentially, each of
* which returns the root directory listing. An IOException will be thrown
* if the server does not issue a valid reply, e.g. the server logic that
* enumerates the directory files fails to close the stream from Files::list.
*/
@Test
public void testDirListings() throws Exception {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(simpleFileServer)).build();
for (int i=0; i<TIMES; i++) {
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
if (i % 100 == 0) {
out.print(" " + i + " ");
}
}
}
static URI uri(HttpServer server) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/")
.buildUnchecked();
}
}

View File

@ -0,0 +1,413 @@
/*
* Copyright (c) 2021, 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.
*/
/*
* @test
* @summary Tests for SimpleFileServer with a root that is of a zip file system
* @library /test/lib
* @build jdk.test.lib.Platform jdk.test.lib.net.URIBuilder
* @run testng/othervm ZipFileSystemTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Map;
import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.SimpleFileServer;
import com.sun.net.httpserver.SimpleFileServer.OutputLevel;
import jdk.test.lib.net.URIBuilder;
import jdk.test.lib.util.FileUtils;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static java.net.http.HttpClient.Builder.NO_PROXY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.StandardOpenOption.CREATE;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
public class ZipFileSystemTest {
static final Path CWD = Path.of(".").toAbsolutePath();
static final Path TEST_DIR = CWD.resolve("ZipFileSystemTest");
static final InetSocketAddress LOOPBACK_ADDR = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
static final boolean ENABLE_LOGGING = true;
static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
@BeforeTest
public void setup() throws Exception {
if (ENABLE_LOGGING) {
ConsoleHandler ch = new ConsoleHandler();
LOGGER.setLevel(Level.ALL);
ch.setLevel(Level.ALL);
LOGGER.addHandler(ch);
}
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
Files.createDirectories(TEST_DIR);
}
@Test
public void testFileGET() throws Exception {
var root = createZipFs(TEST_DIR.resolve("testFileGET.zip"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), "some text");
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testFileGET.zip"));
}
}
@Test
public void testDirectoryGET() throws Exception {
var expectedBody = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createZipFs(TEST_DIR.resolve("testDirectoryGET.zip"));
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testDirectoryGET.zip"));
}
}
@Test
public void testFileHEAD() throws Exception {
var root = createZipFs(TEST_DIR.resolve("testFileHEAD.zip"));
var file = Files.writeString(root.resolve("aFile.txt"), "some text", CREATE);
var lastModified = getLastModified(file);
var expectedLength = Long.toString(Files.size(file));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile.txt"))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/plain");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testFileHEAD.zip"));
}
}
@Test
public void testDirectoryHEAD() throws Exception {
var expectedLength = Integer.toString(
(openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
</ul>
""" + closeHTML).getBytes(UTF_8).length);
var root = createZipFs(TEST_DIR.resolve("testDirectoryHEAD.zip"));
var lastModified = getLastModified(root);
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, ""))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), "text/html; charset=UTF-8");
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), "");
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testDirectoryHEAD.zip"));
}
}
@DataProvider
public Object[][] indexFiles() {
var fileContent = openHTML + """
<h1>This is an index file</h1>
""" + closeHTML;
var dirListing = openHTML + """
<h1>Directory listing for &#x2F;</h1>
<ul>
</ul>
""" + closeHTML;
return new Object[][] {
{"1", "index.html", "text/html", "116", fileContent, true},
{"2", "index.htm", "text/html", "116", fileContent, true},
{"3", "index.txt", "text/html; charset=UTF-8", "134", dirListing, false}
};
}
@Test(dataProvider = "indexFiles")
public void testDirectoryWithIndexGET(String id,
String filename,
String contentType,
String contentLength,
String expectedBody,
boolean serveIndexFile) throws Exception {
var root = createZipFs(TEST_DIR.resolve("testDirectoryWithIndexGET"+id+".zip"));
var lastModified = getLastModified(root);
if (serveIndexFile) {
var file = Files.writeString(root.resolve(filename), expectedBody, CREATE);
lastModified = getLastModified(file);
}
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 200);
assertEquals(response.headers().firstValue("content-type").get(), contentType);
assertEquals(response.headers().firstValue("content-length").get(), contentLength);
assertEquals(response.headers().firstValue("last-modified").get(), lastModified);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
if (serveIndexFile) {
Files.delete(root.resolve(filename));
}
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testDirectoryWithIndexGET"+id+".zip"));
}
}
// no testNotReadableGET() - Zip file system does not enforce access permissions
// no testSymlinkGET() - Zip file system does not support symlink creation
// no testHiddenFileGET() - Zip file system does not support hidden files
@Test
public void testInvalidRequestURIGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;aFile?#.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createZipFs(TEST_DIR.resolve("testInvalidRequestURIGET.zip"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "aFile?#.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testInvalidRequestURIGET.zip"));
}
}
@Test
public void testNotFoundGET() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
""" + closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createZipFs(TEST_DIR.resolve("testNotFoundGET.zip"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), expectedBody);
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testNotFoundGET.zip"));
}
}
@Test
public void testNotFoundHEAD() throws Exception {
var expectedBody = openHTML + """
<h1>File not found</h1>
<p>&#x2F;doesNotExist.txt</p>
"""
+ closeHTML;
var expectedLength = Integer.toString(expectedBody.getBytes(UTF_8).length);
var root = createZipFs(TEST_DIR.resolve("testNotFoundHEAD.zip"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "doesNotExist.txt"))
.method("HEAD", BodyPublishers.noBody()).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertEquals(response.headers().firstValue("content-length").get(), expectedLength);
assertEquals(response.body(), "");
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testNotFoundHEAD.zip"));
}
}
@Test
public void testMovedPermanently() throws Exception {
var root = createZipFs(TEST_DIR.resolve("testMovedPermanently.zip"));
Files.createDirectory(root.resolve("aDirectory"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var uri = uri(server, "aDirectory");
var request = HttpRequest.newBuilder(uri).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 301);
assertEquals(response.headers().firstValue("content-length").get(), "0");
assertEquals(response.headers().firstValue("location").get(), "/aDirectory/");
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testMovedPermanently.zip"));
}
}
static Path createZipFs(Path zipFile) throws Exception {
var p = zipFile.toAbsolutePath().normalize();
var fs = FileSystems.newFileSystem(p, Map.of("create", "true"));
assert fs != FileSystems.getDefault();
return fs.getPath("/"); // root entry
}
@Test
public void testXss() throws Exception {
var root = createZipFs(TEST_DIR.resolve("testXss.zip"));
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
server.start();
try {
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
var request = HttpRequest.newBuilder(uri(server, "beginDelim%3C%3EEndDelim")).build();
var response = client.send(request, BodyHandlers.ofString());
assertEquals(response.statusCode(), 404);
assertTrue(response.body().contains("beginDelim%3C%3EEndDelim"));
assertTrue(response.body().contains("File not found"));
} finally {
server.stop(0);
root.getFileSystem().close();
Files.deleteIfExists(TEST_DIR.resolve("testXss.zip"));
}
}
@AfterTest
public void teardown() throws IOException {
if (Files.exists(TEST_DIR)) {
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
}
}
static final String openHTML = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
""";
static final String closeHTML = """
</body>
</html>
""";
static URI uri(HttpServer server, String path) {
return URIBuilder.newBuilder()
.host("localhost")
.port(server.getAddress().getPort())
.scheme("http")
.path("/" + path)
.buildUnchecked();
}
static String getLastModified(Path path) throws IOException {
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
}