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:
parent
947d52c4c3
commit
9d191fce55
@ -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.
|
||||
#
|
||||
# This code is free software; you can redistribute it and/or modify it
|
||||
@ -196,6 +196,11 @@ else # not java.base
|
||||
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.
|
||||
# The variable JMOD_CMD could contain an environment variable assignment before
|
||||
# the actual command. Filter that out using wildcard before adding to DEPS.
|
||||
|
@ -123,6 +123,7 @@ import com.sun.tools.classfile.InnerClasses_attribute;
|
||||
import com.sun.tools.classfile.InnerClasses_attribute.Info;
|
||||
import com.sun.tools.classfile.Method;
|
||||
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.ModuleTarget_attribute;
|
||||
import com.sun.tools.classfile.Module_attribute;
|
||||
@ -928,6 +929,12 @@ public class CreateSymbols {
|
||||
attributes.put(Attribute.ModuleTarget,
|
||||
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);
|
||||
attributes.put(Attribute.Module,
|
||||
new Module_attribute(attrIdx,
|
||||
@ -2294,6 +2301,13 @@ public class CreateSymbols {
|
||||
chd.isSealed = true;
|
||||
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:
|
||||
throw new IllegalStateException("Unhandled attribute: " +
|
||||
attrName);
|
||||
@ -2731,6 +2745,7 @@ public class CreateSymbols {
|
||||
List<ProvidesDescription> provides = new ArrayList<>();
|
||||
Integer moduleResolution;
|
||||
String moduleTarget;
|
||||
String moduleMainClass;
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
@ -2743,6 +2758,7 @@ public class CreateSymbols {
|
||||
hash = 83 * hash + Objects.hashCode(this.provides);
|
||||
hash = 83 * hash + Objects.hashCode(this.moduleResolution);
|
||||
hash = 83 * hash + Objects.hashCode(this.moduleTarget);
|
||||
hash = 83 * hash + Objects.hashCode(this.moduleMainClass);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@ -2781,6 +2797,10 @@ public class CreateSymbols {
|
||||
other.moduleResolution)) {
|
||||
return false;
|
||||
}
|
||||
if (!Objects.equals(this.moduleMainClass,
|
||||
other.moduleMainClass)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -2818,6 +2838,8 @@ public class CreateSymbols {
|
||||
output.append(" resolution " +
|
||||
quote(Integer.toHexString(moduleResolution),
|
||||
true));
|
||||
if (moduleMainClass != null)
|
||||
output.append(" moduleMainClass " + quote(moduleMainClass, true));
|
||||
writeAttributes(output);
|
||||
output.append("\n");
|
||||
writeInnerClasses(output, baselineVersion, version);
|
||||
@ -2862,6 +2884,8 @@ public class CreateSymbols {
|
||||
moduleResolution = Integer.parseInt(resolutionFlags, 16);
|
||||
}
|
||||
|
||||
moduleMainClass = reader.attributes.get("moduleMainClass");
|
||||
|
||||
readAttributes(reader);
|
||||
reader.moveNext();
|
||||
readInnerClasses(reader);
|
||||
|
41
make/modules/jdk.httpserver/Gensrc.gmk
Normal file
41
make/modules/jdk.httpserver/Gensrc.gmk
Normal 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)
|
@ -28,10 +28,13 @@ package com.sun.net.httpserver;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Objects;
|
||||
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
|
||||
@ -134,7 +137,6 @@ public abstract class Filter {
|
||||
*/
|
||||
public abstract void doFilter (HttpExchange exchange, Chain chain)
|
||||
throws IOException;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@
|
||||
|
||||
package com.sun.net.httpserver;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
@ -33,6 +34,8 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
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
|
||||
@ -65,6 +68,14 @@ import java.util.function.BiFunction;
|
||||
* value given overwriting any existing values in the value list.
|
||||
* </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.
|
||||
* {@code null} keys will never be present in HTTP request or response headers.
|
||||
* @since 1.6
|
||||
@ -78,6 +89,25 @@ public class Headers implements Map<String,List<String>> {
|
||||
*/
|
||||
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.
|
||||
* First {@code char} upper case, rest lower case.
|
||||
@ -254,4 +284,55 @@ public class Headers implements Map<String,List<String>> {
|
||||
sb.append(" }");
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ import java.net.URI;
|
||||
* @since 1.6
|
||||
*/
|
||||
|
||||
public abstract class HttpExchange implements AutoCloseable {
|
||||
public abstract class HttpExchange implements AutoCloseable, Request {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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.
|
||||
* {@inheritDoc}
|
||||
* @return {@inheritDoc}
|
||||
*/
|
||||
public abstract Headers getRequestHeaders();
|
||||
|
||||
@ -111,16 +100,14 @@ public abstract class HttpExchange implements AutoCloseable {
|
||||
public abstract Headers getResponseHeaders();
|
||||
|
||||
/**
|
||||
* Returns the request {@link URI}.
|
||||
*
|
||||
* @return the request {@code URI}
|
||||
* {@inheritDoc}
|
||||
* @return {@inheritDoc}
|
||||
*/
|
||||
public abstract URI getRequestURI();
|
||||
|
||||
/**
|
||||
* Returns the request method.
|
||||
*
|
||||
* @return the request method
|
||||
* {@inheritDoc}
|
||||
* @return {@inheritDoc}
|
||||
*/
|
||||
public abstract String getRequestMethod();
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* 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.net.BindException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
@ -150,6 +152,58 @@ public abstract class HttpServer {
|
||||
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
|
||||
* port number. A maximum backlog can also be specified. This is the maximum
|
||||
|
@ -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.
|
||||
*
|
||||
* 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.net.BindException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.sun.net.httpserver.spi.HttpServerProvider;
|
||||
|
||||
/**
|
||||
@ -92,6 +95,61 @@ public abstract class HttpsServer extends HttpServer {
|
||||
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.
|
||||
*
|
||||
|
@ -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; }
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -120,6 +120,12 @@
|
||||
}
|
||||
});
|
||||
</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
|
||||
*/
|
||||
|
@ -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.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
@ -25,6 +25,40 @@
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -85,7 +85,7 @@ class ExchangeImpl {
|
||||
String m, URI u, Request req, long len, HttpConnection connection
|
||||
) throws IOException {
|
||||
this.req = req;
|
||||
this.reqHdrs = new UnmodifiableHeaders(req.headers());
|
||||
this.reqHdrs = Headers.of(req.headers());
|
||||
this.rspHdrs = new Headers();
|
||||
this.method = m;
|
||||
this.uri = u;
|
||||
|
@ -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) '&' , "&" ,
|
||||
(int) '<' , "<" ,
|
||||
(int) '>' , ">" ,
|
||||
(int) '"' , """ ,
|
||||
(int) '\'' , "'" ,
|
||||
(int) '/' , "/" );
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
@ -27,8 +27,9 @@ exclusiveAccess.dirs=java/math/BigInteger/largeMemory \
|
||||
java/rmi/Naming java/util/prefs sun/management/jmxremote \
|
||||
sun/tools/jstatd sun/tools/jcmd \
|
||||
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 \
|
||||
java/util/BitSet/stream javax/rmi java/net/httpclient/websocket
|
||||
com/sun/tools/attach sun/security/mscapi java/util/stream java/util/Arrays/largeMemory \
|
||||
java/util/BitSet/stream javax/rmi java/net/httpclient/websocket \
|
||||
com/sun/net/httpserver/simpleserver
|
||||
|
||||
# Group definitions
|
||||
groups=TEST.groups
|
||||
|
@ -40,6 +40,8 @@ import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Level;
|
||||
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.HttpHandler;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import org.testng.annotations.DataProvider;
|
||||
import org.testng.annotations.Test;
|
||||
import org.testng.annotations.BeforeTest;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static org.testng.Assert.*;
|
||||
|
||||
public class FilterTest {
|
||||
@ -79,6 +81,9 @@ public class FilterTest {
|
||||
|
||||
expectThrows(NPE, () -> Filter.afterHandler("Some description", null));
|
||||
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
|
||||
@ -90,6 +95,9 @@ public class FilterTest {
|
||||
|
||||
var afterFilter = Filter.afterHandler(desc, HttpExchange::getResponseCode);
|
||||
assertEquals(desc, afterFilter.description());
|
||||
|
||||
var adaptFilter = Filter.adaptRequest(desc, r -> r.with("Foo", List.of("Bar")));
|
||||
assertEquals(desc, adaptFilter.description());
|
||||
}
|
||||
|
||||
@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) {
|
||||
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
|
||||
*/
|
||||
|
@ -26,6 +26,7 @@
|
||||
* @bug 8251496 8268960
|
||||
* @summary Tests for methods in Headers class
|
||||
* @modules jdk.httpserver/com.sun.net.httpserver:+open
|
||||
* jdk.httpserver/sun.net.httpserver:+open
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.URIBuilder
|
||||
* @run testng/othervm HeadersTest
|
||||
@ -55,10 +56,12 @@ import com.sun.net.httpserver.HttpServer;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.testng.annotations.DataProvider;
|
||||
import org.testng.annotations.Test;
|
||||
import sun.net.httpserver.UnmodifiableHeaders;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
import static org.testng.Assert.assertFalse;
|
||||
import static org.testng.Assert.assertNotEquals;
|
||||
import static org.testng.Assert.assertThrows;
|
||||
import static org.testng.Assert.assertTrue;
|
||||
|
||||
@ -314,4 +317,131 @@ public class HeadersTest {
|
||||
h2.put("b", List.of("22"));
|
||||
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
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ 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 org.testng.annotations.DataProvider;
|
||||
import org.testng.annotations.Test;
|
||||
import sun.net.httpserver.UnmodifiableHeaders;
|
||||
import static org.testng.Assert.assertEquals;
|
||||
@ -62,14 +63,24 @@ public class UnmodifiableHeadersTest {
|
||||
assertEquals(unmodifiableHeaders2.get("Foo"), headers.get("Foo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public static void testUnmodifiableHeaders() {
|
||||
@DataProvider
|
||||
public Object[][] headers() {
|
||||
var headers = new Headers();
|
||||
headers.add("Foo", "Bar");
|
||||
HttpExchange exchange = new TestHttpExchange(headers);
|
||||
var exchange = new TestHttpExchange(headers);
|
||||
|
||||
assertUnsupportedOperation(exchange.getRequestHeaders());
|
||||
assertUnmodifiableCollection(exchange.getRequestHeaders());
|
||||
return new Object[][] {
|
||||
{ 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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 /</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 /</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 /</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>/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>/dir/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>/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>/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>/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>/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>/symlink/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>/""" + 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>/.hiddenDirectory/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 /aDirectory/</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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
359
test/jdk/com/sun/net/httpserver/simpleserver/MapToPathTest.java
Normal file
359
test/jdk/com/sun/net/httpserver/simpleserver/MapToPathTest.java
Normal 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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
167
test/jdk/com/sun/net/httpserver/simpleserver/RequestTest.java
Normal file
167
test/jdk/com/sun/net/httpserver/simpleserver/RequestTest.java
Normal 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; }
|
||||
}
|
||||
}
|
@ -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 /</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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
};
|
@ -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";
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
@ -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 /</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 /</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 /</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>/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>/dir/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>/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>/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>/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>/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>/symlink/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>/""" + 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>/.hiddenDirectory/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 /aDirectory/</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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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 /</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 /</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 /</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>/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>/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>/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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user