undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 java -m jdk.httpserver command
|
||||
* @library /test/lib
|
||||
* @modules jdk.httpserver
|
||||
* @run junit/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 static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class CommandLineNegativeTest {
|
||||
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-Duser.language=en -Duser.country=US";
|
||||
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();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
Files.createDirectories(TEST_DIR);
|
||||
Files.createFile(TEST_FILE);
|
||||
}
|
||||
|
||||
public static Object[][] unknownOption() {
|
||||
return new Object[][] {
|
||||
{"--unknownOption"},
|
||||
{"null"}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("unknownOption")
|
||||
public void testBadOption(String opt) throws Throwable {
|
||||
out.println("\n--- testUnknownOption, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: unknown option: " + opt);
|
||||
}
|
||||
|
||||
public static 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
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tooManyOptionArgs")
|
||||
public void testTooManyOptionArgs(String opt, String arg) throws Throwable {
|
||||
out.println("\n--- testTooManyOptionArgs, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt, arg, arg)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: unknown option: " + arg);
|
||||
}
|
||||
|
||||
public static 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
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("noArg")
|
||||
public void testNoArg(String opt, String msg) throws Throwable {
|
||||
out.println("\n--- testNoArg, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: no value given for " + opt)
|
||||
.shouldContain(msg);
|
||||
}
|
||||
|
||||
public static 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", "+-"}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidValue")
|
||||
public void testInvalidValue(String opt, String val) throws Throwable {
|
||||
out.println("\n--- testInvalidValue, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt, val)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: invalid value given for " + opt + ": " + val);
|
||||
}
|
||||
|
||||
public static Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testPortOutOfRange(String opt) throws Throwable {
|
||||
out.println("\n--- testPortOutOfRange, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt, "65536") // range 0 to 65535
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "port out of range:65536");
|
||||
}
|
||||
|
||||
public static Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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, LOCALE_OPT, "-m", "jdk.httpserver", opt, file)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "Path is not a directory: " + file);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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, LOCALE_OPT, "-m", "jdk.httpserver", opt, root.toString())
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "Path does not exist: " + root.toString());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testRootNotReadable(String opt) throws Throwable {
|
||||
out.println("\n--- testRootNotReadable, opt=\"%s\" ".formatted(opt));
|
||||
Assumptions.assumeFalse(Platform.isWindows(), "cannot run on Windows"); // Not applicable to Windows. Reason: cannot revoke an owner's read
|
||||
// access to a directory that was created by that owner
|
||||
Path root = Files.createDirectories(TEST_DIR.resolve("not/readable/dir"));
|
||||
try {
|
||||
root.toFile().setReadable(false, false);
|
||||
assertFalse(Files.isReadable(root));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-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);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static 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 {
|
||||
String[] nargs = new String[args.length + 1];
|
||||
nargs[0] = args[0];
|
||||
System.arraycopy(args, 1, nargs, 2, args.length-1);
|
||||
nargs[1] = "-Djdk.includeInExceptions=hostInfo";
|
||||
var pb = new ProcessBuilder(nargs)
|
||||
.directory(TEST_DIR.toFile());
|
||||
var outputAnalyser = ProcessTools.executeCommand(pb)
|
||||
.outputTo(System.out)
|
||||
.errorTo(System.out);
|
||||
return outputAnalyser;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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
|
||||
* @bug 8276848
|
||||
* @summary Tests the java -m jdk.httpserver command with port not specified
|
||||
* @modules jdk.httpserver
|
||||
* @library /test/lib
|
||||
* @run junit/othervm/manual CommandLinePortNotSpecifiedTest
|
||||
*/
|
||||
|
||||
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 static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CommandLinePortNotSpecifiedTest {
|
||||
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-Duser.language=en -Duser.country=US";
|
||||
static final String JAVA = getJava(JAVA_HOME);
|
||||
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
|
||||
static final Path TEST_DIR = CWD.resolve("CommandLinePortNotSpecifiedTest");
|
||||
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();
|
||||
|
||||
@BeforeAll
|
||||
public static 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a manual test to confirm the command-line tool starts successfully
|
||||
* in the case where the port is not specified. In this case the server uses
|
||||
* the default port 8000. The test is manual to avoid a BindException in the
|
||||
* unlikely but not impossible case that the port is already in use.
|
||||
*/
|
||||
@Test
|
||||
public void testPortNotSpecified() throws Throwable {
|
||||
out.println("\n--- testPortNotSpecified");
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver")
|
||||
.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);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static 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://";
|
||||
|
||||
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 - Prints this help message and exits.
|
||||
-version, --version - Prints version information and exits.
|
||||
To stop the server, press Ctrl + C.""".formatted(LOOPBACK_ADDR);
|
||||
|
||||
// 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,304 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 java -m jdk.httpserver command
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.IPSupport
|
||||
* @modules jdk.httpserver
|
||||
* @run junit/othervm CommandLinePositiveTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
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.net.IPSupport;
|
||||
import jdk.test.lib.process.OutputAnalyzer;
|
||||
import jdk.test.lib.process.ProcessTools;
|
||||
import jdk.test.lib.util.FileUtils;
|
||||
import static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class CommandLinePositiveTest {
|
||||
|
||||
static final String JAVA_VERSION = System.getProperty("java.version");
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-Duser.language=en -Duser.country=US";
|
||||
static final String JAVA = getJava(JAVA_HOME);
|
||||
|
||||
/**
|
||||
* The <b>real path</b> to the current working directory where
|
||||
* <ol>
|
||||
* <li>the web server process will be started in,</li>
|
||||
* <li>and hence, unless given an explicit content root directory, the web
|
||||
* server will be serving from.</li>
|
||||
* </ol>
|
||||
*/
|
||||
private static final Path CWD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CWD = Path.of(".").toRealPath();
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CWD_STR = CWD.toString();
|
||||
|
||||
/**
|
||||
* The <b>real path</b> to the web server content root directory, if one
|
||||
* needs to be provided explicitly.
|
||||
*/
|
||||
private static final Path ROOT_DIR = CWD.resolve("www");
|
||||
|
||||
private static final String ROOT_DIR_STR = ROOT_DIR.toString();
|
||||
|
||||
static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (Files.exists(ROOT_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(ROOT_DIR);
|
||||
}
|
||||
Files.createDirectories(ROOT_DIR);
|
||||
Files.createFile(ROOT_DIR.resolve("file.txt"));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testAbsDirectory(String opt) throws Throwable {
|
||||
out.printf("\n--- testAbsDirectory, opt=\"%s\"%n", opt);
|
||||
testDirectory(opt, ROOT_DIR_STR);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testRelDirectory(String opt) throws Throwable {
|
||||
out.printf("\n--- testRelDirectory, opt=\"%s\"%n", opt);
|
||||
Path rootRelDir = CWD.relativize(ROOT_DIR);
|
||||
testDirectory(opt, rootRelDir.toString());
|
||||
}
|
||||
|
||||
private static void testDirectory(String opt, String rootDir) throws Throwable {
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", "-p", "0", opt, rootDir)
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
|
||||
.shouldContain("Serving " + ROOT_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testPort(String opt) throws Throwable {
|
||||
out.println("\n--- testPort, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static 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]
|
||||
[-version to show version information]""";
|
||||
|
||||
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 - Prints this help message and exits.
|
||||
-version, --version - Prints version information and exits.
|
||||
To stop the server, press Ctrl + C.""".formatted(LOOPBACK_ADDR);
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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, LOCALE_OPT, "-m", "jdk.httpserver", opt)
|
||||
.shouldHaveExitValue(0)
|
||||
.shouldContain(USAGE_TEXT)
|
||||
.shouldContain(OPTIONS_TEXT);
|
||||
}
|
||||
|
||||
public static Object[][] versionOptions() { return new Object[][] {{"-version"}, {"--version"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("versionOptions")
|
||||
public void testVersion(String opt) throws Throwable {
|
||||
out.println("\n--- testVersion, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(WaitForLine.VERSION_STARTUP_LINE,
|
||||
false, // do not explicitly destroy the process
|
||||
JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt)
|
||||
.shouldHaveExitValue(0);
|
||||
}
|
||||
|
||||
public static Object[][] bindOptions() { return new Object[][] {{"-b"}, {"--bind-address"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("bindOptions")
|
||||
public void testBindAllInterfaces(String opt) throws Throwable {
|
||||
out.println("\n--- testBindAllInterfaces, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", "-p", "0", opt, "0.0.0.0")
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
|
||||
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
|
||||
if (IPSupport.hasIPv6()) {
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", opt, "::0")
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
|
||||
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("bindOptions")
|
||||
public void testLastOneWinsBindAddress(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsBindAddress, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", "-p", "0", opt, "123.4.5.6", opt, LOOPBACK_ADDR)
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testLastOneWinsDirectory(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsDirectory, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-m", "jdk.httpserver", "-p", "0", opt, ROOT_DIR_STR, opt, ROOT_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 " + ROOT_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static Object[][] outputOptions() { return new Object[][] {{"-o"}, {"--output"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("outputOptions")
|
||||
public void testLastOneWinsOutput(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsOutput, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testLastOneWinsPort(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsPort, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JAVA, LOCALE_OPT, "-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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws IOException {
|
||||
if (Files.exists(ROOT_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(ROOT_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://";
|
||||
static final String VERSION_STARTUP_LINE_STRING = "java " + JAVA_VERSION;
|
||||
|
||||
// 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()),
|
||||
VERSION_STARTUP_LINE (VERSION_STARTUP_LINE_STRING);
|
||||
|
||||
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),
|
||||
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,981 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 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.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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(200, response.statusCode());
|
||||
assertEquals("some text", response.body());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals("", response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static 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}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(200, response.statusCode());
|
||||
assertEquals(contentType, response.headers().firstValue("content-type").get());
|
||||
assertEquals(contentLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
if (serveIndexFile) {
|
||||
Files.delete(root.resolve(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotReadableFileGET() throws Exception {
|
||||
Assumptions.assumeFalse(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
file.toFile().setReadable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotReadableSegmentGET() throws Exception {
|
||||
Assumptions.assumeFalse(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
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);
|
||||
createSymLink(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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);
|
||||
createSymLink(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSymLink(Path symlink, Path target) {
|
||||
try {
|
||||
Files.createSymbolicLink(symlink, target);
|
||||
} catch (UnsupportedOperationException uoe) {
|
||||
Assumptions.abort("sym link creation not supported");
|
||||
} catch (IOException ioe) {
|
||||
Assumptions.abort("probably insufficient privileges to create sym links (Windows)");
|
||||
}
|
||||
}
|
||||
|
||||
@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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static 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 static 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(301, response.statusCode());
|
||||
assertEquals("0", response.headers().firstValue("content-length").get());
|
||||
assertEquals("/aDirectory/", response.headers().firstValue("location").get());
|
||||
|
||||
// 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(301, res2.statusCode());
|
||||
assertEquals("0", res2.headers().firstValue("content-length").get());
|
||||
assertEquals("/aDirectory/?query", res2.headers().firstValue("location").get());
|
||||
}
|
||||
|
||||
{ // 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(200, response.statusCode());
|
||||
assertEquals(expectedBody, response.body());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
}
|
||||
} 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(404, response.statusCode());
|
||||
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,292 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 file permission checks during the creation of a `FileServerHandler`
|
||||
* @requires (os.family != "windows")
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.URIBuilder
|
||||
* @run main/othervm -ea DocRootDirPermissionsTest true
|
||||
* @run main/othervm -ea DocRootDirPermissionsTest false
|
||||
*/
|
||||
|
||||
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.nio.file.attribute.AclEntry;
|
||||
import java.nio.file.attribute.AclEntryPermission;
|
||||
import java.nio.file.attribute.AclEntryType;
|
||||
import java.nio.file.attribute.AclFileAttributeView;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.UserPrincipal;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
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 file permission checks during the creation of a FileServerHandler.
|
||||
*
|
||||
* A FileServerHandler can only be created if its root directory
|
||||
* is readable. The test consists of 2 runs:
|
||||
* 1) RootDir is readable
|
||||
* 2) RootDir is NOT readable
|
||||
* 2) reuses the test directory created in the previous run, revoking
|
||||
* read access.
|
||||
* */
|
||||
public class DocRootDirPermissionsTest {
|
||||
|
||||
private static final Path CWD = Path.of(".").toAbsolutePath().normalize();
|
||||
private static final Path TEST_DIR = CWD.resolve("RootDir");
|
||||
private static final InetSocketAddress LOOPBACK_ADDR =
|
||||
new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
|
||||
|
||||
private static final boolean ENABLE_LOGGING = true;
|
||||
private static final Logger LOGGER = Logger.getLogger("com.sun.net.httpserver");
|
||||
|
||||
private static boolean readPermitted;
|
||||
private static String lastModifiedDir;
|
||||
private static String lastModifiedFile;
|
||||
|
||||
private static Set<PosixFilePermission> posixPermissions;
|
||||
private static List<AclEntry> acls;
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
new DocRootDirPermissionsTest().run(args);
|
||||
}
|
||||
|
||||
protected void run(String[] args) throws Exception{
|
||||
setupLogging();
|
||||
readPermitted = Boolean.parseBoolean(args[0]);
|
||||
if (readPermitted) {
|
||||
createTestDir();
|
||||
testDirectoryGET();
|
||||
testFileGET();
|
||||
} else {
|
||||
revokePermissions();
|
||||
try {
|
||||
testCreateHandler();
|
||||
} finally {
|
||||
restorePermissions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void revokePermissions() throws IOException {
|
||||
if (!Files.isReadable(TEST_DIR)) {
|
||||
// good nothing to do:
|
||||
System.out.println("File is already not readable: nothing to do");
|
||||
return;
|
||||
}
|
||||
System.out.println("FileSystem: " + Files.getFileStore(TEST_DIR).type());
|
||||
if (Files.getFileStore(TEST_DIR).supportsFileAttributeView("posix")) {
|
||||
System.out.println("Revoking owner's read access in POSIX permissions for " + TEST_DIR);
|
||||
posixPermissions = Files.getPosixFilePermissions(TEST_DIR);
|
||||
Set<PosixFilePermission> newPerms = new HashSet<>(posixPermissions);
|
||||
newPerms.remove(PosixFilePermission.OWNER_READ);
|
||||
newPerms.remove(PosixFilePermission.OWNER_EXECUTE);
|
||||
Files.setPosixFilePermissions(TEST_DIR, newPerms);
|
||||
} else if (Files.getFileStore(TEST_DIR).supportsFileAttributeView("acl")) {
|
||||
System.out.println("Revoking owner's read access in ACLs for " + TEST_DIR);
|
||||
AclFileAttributeView view = Files.getFileAttributeView(TEST_DIR, AclFileAttributeView.class);
|
||||
acls = view.getAcl();
|
||||
List<AclEntry> entries = new ArrayList<>();
|
||||
UserPrincipal owner = view.getOwner();
|
||||
// Deny owner
|
||||
entries.add(AclEntry.newBuilder().setType(AclEntryType.DENY)
|
||||
.setPrincipal(owner).setPermissions(AclEntryPermission.READ_DATA,
|
||||
AclEntryPermission.READ_ATTRIBUTES, AclEntryPermission.READ_NAMED_ATTRS,
|
||||
AclEntryPermission.EXECUTE)
|
||||
.build());
|
||||
// Revoke read data and execute
|
||||
for (AclEntry entry : acls) {
|
||||
Set<AclEntryPermission> perms =
|
||||
new HashSet<>(entry.permissions());
|
||||
if (entry.type() == AclEntryType.ALLOW) {
|
||||
System.out.println("Revoking read access: " + entry);
|
||||
perms.remove(AclEntryPermission.READ_DATA);
|
||||
perms.remove(AclEntryPermission.LIST_DIRECTORY);
|
||||
perms.remove(AclEntryPermission.READ_ATTRIBUTES);
|
||||
perms.remove(AclEntryPermission.READ_NAMED_ATTRS);
|
||||
perms.remove(AclEntryPermission.EXECUTE);
|
||||
entries.add(AclEntry.newBuilder(entry).setPermissions(perms).build());
|
||||
}
|
||||
}
|
||||
view.setAcl(entries);
|
||||
System.out.println("ACLs: " + view.getAcl());
|
||||
try {
|
||||
System.out.println("File is readable: " + Files.isReadable(TEST_DIR));
|
||||
Thread.sleep(50);
|
||||
} catch (InterruptedException x) {}
|
||||
// The above does not always work, skip the test if we can't make TEST_DIR
|
||||
// read-only.
|
||||
if (Files.isReadable(TEST_DIR)) {
|
||||
throw new jtreg.SkippedException("Can't make directory read-only");
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Required attribute view not supported");
|
||||
}
|
||||
System.out.println("File is readable: " + Files.isReadable(TEST_DIR));
|
||||
}
|
||||
|
||||
private void restorePermissions() throws IOException {
|
||||
if (Files.getFileStore(TEST_DIR).supportsFileAttributeView("posix")) {
|
||||
if (posixPermissions != null) {
|
||||
System.out.println("Restoring original POSIX permissions");
|
||||
Files.setPosixFilePermissions(TEST_DIR, posixPermissions);
|
||||
}
|
||||
} else if (Files.getFileStore(TEST_DIR).supportsFileAttributeView("acl")) {
|
||||
if (acls != null) {
|
||||
System.out.println("Restoring original ACLs");
|
||||
AclFileAttributeView view = Files.getFileAttributeView(TEST_DIR, AclFileAttributeView.class);
|
||||
view.setAcl(acls);
|
||||
}
|
||||
} else {
|
||||
throw new RuntimeException("Required attribute view not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private void setupLogging() {
|
||||
if (ENABLE_LOGGING) {
|
||||
ConsoleHandler ch = new ConsoleHandler();
|
||||
LOGGER.setLevel(Level.ALL);
|
||||
ch.setLevel(Level.ALL);
|
||||
LOGGER.addHandler(ch);
|
||||
}
|
||||
}
|
||||
|
||||
private 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 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
private void testCreateHandler(){
|
||||
try {
|
||||
SimpleFileServer.createFileServer(LOOPBACK_ADDR, TEST_DIR, OutputLevel.NONE);
|
||||
throw new RuntimeException("Handler creation expected to fail");
|
||||
} catch (IllegalArgumentException expected) { }
|
||||
|
||||
try {
|
||||
SimpleFileServer.createFileHandler(TEST_DIR);
|
||||
throw new RuntimeException("Handler creation expected to fail");
|
||||
} catch (IllegalArgumentException expected) { }
|
||||
}
|
||||
|
||||
private static final String openHTML = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
""";
|
||||
|
||||
private static final String closeHTML = """
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
private URI uri(HttpServer server, String path) {
|
||||
return URIBuilder.newBuilder()
|
||||
.host("localhost")
|
||||
.port(server.getAddress().getPort())
|
||||
.scheme("http")
|
||||
.path("/" + path)
|
||||
.buildUnchecked();
|
||||
}
|
||||
|
||||
private String getLastModified(Path path) throws IOException {
|
||||
return Files.getLastModifiedTime(path).toInstant().atZone(ZoneId.of("GMT"))
|
||||
.format(DateTimeFormatter.RFC_1123_DATE_TIME);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2025, 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 file permission checks during the creation of a `FileServerHandler`
|
||||
* @requires (os.family == "windows")
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.URIBuilder
|
||||
* @run main/manual/othervm -ea DocRootDirPermissionsWinTest true
|
||||
* @run main/manual/othervm -ea DocRootDirPermissionsWinTest false
|
||||
*/
|
||||
|
||||
public class DocRootDirPermissionsWinTest extends DocRootDirPermissionsTest{
|
||||
public static void main(String[] args) throws Exception {
|
||||
new DocRootDirPermissionsWinTest().run(args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit 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 static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class FileServerHandlerTest {
|
||||
|
||||
static final Path CWD = Path.of(".").toAbsolutePath();
|
||||
static final Class<RuntimeException> RE = RuntimeException.class;
|
||||
|
||||
public static Object[][] notAllowedMethods() {
|
||||
var l = List.of("POST", "PUT", "DELETE", "TRACE", "OPTIONS");
|
||||
return l.stream().map(s -> new Object[] { s }).toArray(Object[][]::new);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("notAllowedMethods")
|
||||
public void testNotAllowedRequestMethod(String requestMethod) throws Exception {
|
||||
var handler = SimpleFileServer.createFileHandler(CWD);
|
||||
var exchange = new MethodHttpExchange(requestMethod);
|
||||
handler.handle(exchange);
|
||||
assertEquals(405, exchange.rCode);
|
||||
assertEquals("HEAD, GET", exchange.getResponseHeaders().getFirst("allow"));
|
||||
}
|
||||
|
||||
public static Object[][] notImplementedMethods() {
|
||||
var l = List.of("GARBAGE", "RUBBISH", "TRASH", "FOO", "BAR");
|
||||
return l.stream().map(s -> new Object[] { s }).toArray(Object[][]::new);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("notImplementedMethods")
|
||||
public void testNotImplementedRequestMethod(String requestMethod) throws Exception {
|
||||
var handler = SimpleFileServer.createFileHandler(CWD);
|
||||
var exchange = new MethodHttpExchange(requestMethod);
|
||||
handler.handle(exchange);
|
||||
assertEquals(501, exchange.rCode);
|
||||
}
|
||||
|
||||
// 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 = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getRequestBody", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange("GET") {
|
||||
public Headers getResponseHeaders() {
|
||||
throw new RuntimeException("getResponseHeaders");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getResponseHeaders", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange("GET") {
|
||||
public void sendResponseHeaders(int rCode, long responseLength) {
|
||||
throw new RuntimeException("sendResponseHeaders");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("sendResponseHeaders", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange("GET") {
|
||||
public OutputStream getResponseBody() {
|
||||
throw new RuntimeException("getResponseBody");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getResponseBody", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange("GET") {
|
||||
public void close() {
|
||||
throw new RuntimeException("close");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("close", t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
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,364 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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("bar", response.headers().firstValue("foo").get());
|
||||
assertEquals("0", response.headers().firstValue("content-length").get());
|
||||
assertEquals(3, response.headers().map().size());
|
||||
assertEquals(200, response.statusCode());
|
||||
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("bar", response.headers().firstValue("foo").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(3, response.headers().map().size());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("hello world", response.body());
|
||||
} 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(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(2, response.headers().map().size());
|
||||
assertEquals(200, response.statusCode());
|
||||
} 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("12345", response.headers().firstValue("date").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(2, response.headers().map().size());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("hello world", response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object[][] responseBodies() {
|
||||
return new Object[][] { {"hello world"}, {""} };
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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 = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getRequestBody", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange() {
|
||||
public Headers getResponseHeaders() {
|
||||
throw new RuntimeException("getResponseHeaders");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getResponseHeaders", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange() {
|
||||
public void sendResponseHeaders(int rCode, long responseLength) {
|
||||
throw new RuntimeException("sendResponseHeaders");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("sendResponseHeaders", t.getMessage());
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange() {
|
||||
public OutputStream getResponseBody() {
|
||||
throw new RuntimeException("getResponseBody");
|
||||
}
|
||||
};
|
||||
if (!body.isEmpty()) { // getResponseBody not called if no responseBody
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("getResponseBody", t.getMessage());
|
||||
}
|
||||
}
|
||||
{
|
||||
var exchange = new ThrowingHttpExchange() {
|
||||
public void close() {
|
||||
throw new RuntimeException("close");
|
||||
}
|
||||
};
|
||||
var t = assertThrows(RE, () -> h.handle(exchange));
|
||||
assertEquals("close", t.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@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(200, response.statusCode());
|
||||
assertEquals("TestHandler-1", response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("TestHandler-2", response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("TestHandler-2", response.body());
|
||||
} 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,112 @@
|
|||
/*
|
||||
* Copyright (c) 2023, 2025, 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
|
||||
* @bug 8315436
|
||||
* @summary Test if HttpsServer sends the TLS alerts produced
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.SimpleSSLContext
|
||||
* @run junit/othervm HttpsServerAlertTest
|
||||
*/
|
||||
|
||||
import com.sun.net.httpserver.HttpsConfigurator;
|
||||
import com.sun.net.httpserver.HttpsParameters;
|
||||
import com.sun.net.httpserver.HttpsServer;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLHandshakeException;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.logging.ConsoleHandler;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class HttpsServerAlertTest {
|
||||
|
||||
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");
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
static {
|
||||
SSLContext.setDefault(sslContext);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (ENABLE_LOGGING) {
|
||||
ConsoleHandler ch = new ConsoleHandler();
|
||||
LOGGER.setLevel(Level.ALL);
|
||||
ch.setLevel(Level.ALL);
|
||||
LOGGER.addHandler(ch);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProtocolMismatch() throws Exception {
|
||||
SSLSocketFactory sf = sslContext.getSocketFactory();
|
||||
var server = HttpsServer.create(LOOPBACK_ADDR, 0);
|
||||
server.setHttpsConfigurator(new Configurator(sslContext));
|
||||
server.start();
|
||||
try (SSLSocket s = (SSLSocket) sf.createSocket()) {
|
||||
// server only accepts TLS 1.3
|
||||
s.setEnabledProtocols(new String[]{"TLSv1.2"});
|
||||
s.connect(server.getAddress());
|
||||
s.startHandshake();
|
||||
fail("Expected a handshake failure");
|
||||
} catch (SSLHandshakeException e) {
|
||||
System.out.println("Got exception: " + e);
|
||||
if (e.getCause() instanceof EOFException ||
|
||||
!e.getMessage().contains("protocol_version"))
|
||||
throw e;
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Configurator extends HttpsConfigurator {
|
||||
public Configurator(SSLContext sslContext) {
|
||||
super(sslContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(HttpsParameters params) {
|
||||
SSLParameters sslParams = getSSLContext().getDefaultSSLParameters();
|
||||
sslParams.setProtocols(new String[]{"TLSv1.3"});
|
||||
params.setSSLParameters(sslParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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.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 static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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");
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
static {
|
||||
SSLContext.setDefault(sslContext);
|
||||
}
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (ENABLE_LOGGING) {
|
||||
ConsoleHandler ch = new ConsoleHandler();
|
||||
LOGGER.setLevel(Level.ALL);
|
||||
ch.setLevel(Level.ALL);
|
||||
LOGGER.addHandler(ch);
|
||||
}
|
||||
}
|
||||
|
||||
@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(LOOPBACK_ADDR.getAddress(), s1.getAddress().getAddress());
|
||||
|
||||
final var s2 = HttpsServer.create(null, 0, "/foo/", new Handler());
|
||||
assertNull(s2.getAddress());
|
||||
s2.bind(LOOPBACK_ADDR, 0);
|
||||
assertEquals(LOOPBACK_ADDR.getAddress(), s2.getAddress().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(200, response.statusCode());
|
||||
assertEquals("hello world", response.body());
|
||||
assertEquals( Integer.toString("hello world".length()), response.headers().firstValue("content-length").get());
|
||||
assertEquals(filter.responseCode.get().intValue(), response.statusCode());
|
||||
} 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,147 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
/*
|
||||
* @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 junit/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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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
|
||||
public static 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);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(e.respCode(), response.statusCode());
|
||||
if (e.contentType != null) {
|
||||
assertEquals(e.contentType(), response.headers().firstValue("content-type").get());
|
||||
} else {
|
||||
assertTrue(response.headers().firstValue("content-type").isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
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();
|
||||
}
|
||||
}
|
||||
391
test/jdk/com/sun/net/httpserver/simpleserver/MapToPathTest.java
Normal file
391
test/jdk/com/sun/net/httpserver/simpleserver/MapToPathTest.java
Normal file
|
|
@ -0,0 +1,391 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 static java.lang.System.out;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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 static 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(200, res1.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", res1.headers().firstValue("content-type").get());
|
||||
assertEquals(Long.toString(257L), res1.headers().firstValue("content-length").get());
|
||||
assertEquals(getLastModified(TEST_DIR), res1.headers().firstValue("last-modified").get());
|
||||
|
||||
var req2 = HttpRequest.newBuilder(uri(server, "/../")).build();
|
||||
var res2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(404, res2.statusCode()); // cannot escape root
|
||||
|
||||
var req3 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/c://")).build();
|
||||
var res3 = client.send(req3, BodyHandlers.ofString());
|
||||
assertEquals(404, res3.statusCode()); // not found
|
||||
|
||||
var req4 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/c:.//")).build();
|
||||
var res4 = client.send(req4, BodyHandlers.ofString());
|
||||
assertEquals(404, res4.statusCode()); // not found
|
||||
|
||||
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/c:..//")).build();
|
||||
var res5 = client.send(req5, BodyHandlers.ofString());
|
||||
assertEquals(404, res5.statusCode()); // not found
|
||||
|
||||
var req6 = HttpRequest.newBuilder(uri(server, "/foo/file:" + TEST_DIR.getParent())).build();
|
||||
var res6 = client.send(req6, BodyHandlers.ofString());
|
||||
assertEquals(404, res6.statusCode()); // not found
|
||||
|
||||
var req7 = HttpRequest.newBuilder(uri(server, "/foo/bar/\\..\\../")).build();
|
||||
var res7 = client.send(req7, BodyHandlers.ofString());
|
||||
assertEquals(404, res7.statusCode()); // not found
|
||||
|
||||
var req8 = HttpRequest.newBuilder(uri(server, "/foo")).build();
|
||||
var res8 = client.send(req8, BodyHandlers.ofString());
|
||||
assertEquals(301, res8.statusCode()); // redirect
|
||||
assertEquals("0", res8.headers().firstValue("content-length").get());
|
||||
assertEquals("/foo/", res8.headers().firstValue("location").get());
|
||||
} 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(200, res1.statusCode());
|
||||
assertEquals("testdir", res1.body());
|
||||
assertEquals("text/plain", res1.headers().firstValue("content-type").get());
|
||||
assertEquals(Long.toString(7L), res1.headers().firstValue("content-length").get());
|
||||
assertEquals(getLastModified(TEST_DIR.resolve("file.txt")), res1.headers().firstValue("last-modified").get());
|
||||
|
||||
var req2 = HttpRequest.newBuilder(uri(server, "/store/file.txt")).build();
|
||||
var res2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(404, res2.statusCode()); // 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(200, res1.statusCode());
|
||||
assertEquals("foo", res1.body());
|
||||
assertEquals("text/plain", res1.headers().firstValue("content-type").get());
|
||||
assertEquals(Long.toString(3L), res1.headers().firstValue("content-length").get());
|
||||
assertEquals(getLastModified(TEST_DIR.resolve("foo").resolve("file.txt")), res1.headers().firstValue("last-modified").get());
|
||||
|
||||
var req2 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
|
||||
var res2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(404, res2.statusCode()); // no context found
|
||||
|
||||
var req3 = HttpRequest.newBuilder(uri(server, "/foo/../foobar/file.txt")).build();
|
||||
var res3 = client.send(req3, BodyHandlers.ofString());
|
||||
assertEquals(404, res3.statusCode()); // cannot escape context
|
||||
|
||||
var req4 = HttpRequest.newBuilder(uri(server, "/foo/../..")).build();
|
||||
var res4 = client.send(req4, BodyHandlers.ofString());
|
||||
assertEquals(404, res4.statusCode()); // cannot escape root
|
||||
|
||||
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar")).build();
|
||||
var res5 = client.send(req5, BodyHandlers.ofString());
|
||||
assertEquals(301, res5.statusCode()); // redirect
|
||||
assertEquals("0", res5.headers().firstValue("content-length").get());
|
||||
assertEquals("/foo/bar/", res5.headers().firstValue("location").get());
|
||||
} 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(200, res1.statusCode());
|
||||
assertEquals("foo", res1.body());
|
||||
assertEquals("text/plain", res1.headers().firstValue("content-type").get());
|
||||
assertEquals(Long.toString(3L), res1.headers().firstValue("content-length").get());
|
||||
assertEquals(getLastModified(TEST_DIR.resolve("foo").resolve("file.txt")), res1.headers().firstValue("last-modified").get());
|
||||
|
||||
var req2 = HttpRequest.newBuilder(uri(server, "/foobar/")).build();
|
||||
var res2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(404, res2.statusCode()); // handler prevents mapping to /foo/bar
|
||||
|
||||
var req3 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
|
||||
var res3 = client.send(req3, BodyHandlers.ofString());
|
||||
assertEquals(404, res3.statusCode()); // 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(404, res4.statusCode());
|
||||
|
||||
var req5 = HttpRequest.newBuilder(uri(server, "/foo/bar")).build();
|
||||
var res5 = client.send(req5, BodyHandlers.ofString());
|
||||
assertEquals(301, res5.statusCode()); // redirect
|
||||
assertEquals("0", res5.headers().firstValue("content-length").get());
|
||||
assertEquals("/foo/bar/", res5.headers().firstValue("location").get());
|
||||
|
||||
var req6 = HttpRequest.newBuilder(uri(server, "/foo")).build();
|
||||
var res6 = client.send(req6, BodyHandlers.ofString());
|
||||
assertEquals(301, res6.statusCode()); // redirect
|
||||
assertEquals("0", res6.headers().firstValue("content-length").get());
|
||||
assertEquals("/foo/", res6.headers().firstValue("location").get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
{
|
||||
// Test that a request path segment that is a Windows root drive
|
||||
// does not circumvent access restrictions.
|
||||
//
|
||||
// For example, given the test directory tree:
|
||||
//
|
||||
// |-- TEST_DIR
|
||||
// |-- foo
|
||||
// |-- bar ----->>> if hidden, itself and any of its subdirectories are not accessible
|
||||
// |-- baz
|
||||
// |-- file.txt
|
||||
// ...
|
||||
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, "/foo/bar/c:/baz/")).build();
|
||||
var res1 = client.send(req1, BodyHandlers.ofString());
|
||||
assertEquals(404, res1.statusCode()); // not found
|
||||
} 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(200, res1.statusCode());
|
||||
assertEquals("root response body", res1.body());
|
||||
}
|
||||
{
|
||||
var req1 = HttpRequest.newBuilder(uri(server, "/foo/file.txt")).build();
|
||||
var res1 = client.send(req1, BodyHandlers.ofString());
|
||||
assertEquals(200, res1.statusCode());
|
||||
assertEquals("foo", res1.body());
|
||||
|
||||
var req2 = HttpRequest.newBuilder(uri(server, "/foo/bar/baz/file.txt")).build();
|
||||
var res2 = client.send(req2, BodyHandlers.ofString());
|
||||
assertEquals(200, res2.statusCode());
|
||||
assertEquals("foo/bar/baz", res2.body());
|
||||
}
|
||||
{
|
||||
var req1 = HttpRequest.newBuilder(uri(server, "/foobar/file.txt")).build();
|
||||
var res1 = client.send(req1, BodyHandlers.ofString());
|
||||
assertEquals(200, res1.statusCode());
|
||||
assertEquals("foobar", res1.body());
|
||||
}
|
||||
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(200, res1.statusCode());
|
||||
assertEquals("bar response body", res1.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(200, res.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", res.headers().firstValue("content-type").get());
|
||||
assertEquals(Long.toString(257L), res.headers().firstValue("content-length").get());
|
||||
assertEquals(getLastModified(TEST_DIR), res.headers().firstValue("last-modified").get());
|
||||
}
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static 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,344 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 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.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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("HttpExchange OutputFilter (outputLevel: VERBOSE)", filter.description());
|
||||
|
||||
filter = SimpleFileServer.createOutputFilter(OUT, INFO);
|
||||
assertEquals("HttpExchange OutputFilter (outputLevel: INFO)", filter.description());
|
||||
}
|
||||
|
||||
@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(200, response.statusCode());
|
||||
assertEquals(3, response.headers().map().size());
|
||||
assertEquals("hello world", response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals(2, response.headers().map().size());
|
||||
assertEquals("hello world", response.body());
|
||||
} 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
|
||||
* <
|
||||
* """;
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
public static 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.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(expectedOutput, baos.toString(UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(404, response.statusCode());
|
||||
assertEquals(3, response.headers().map().size());
|
||||
} 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
168
test/jdk/com/sun/net/httpserver/simpleserver/RequestTest.java
Normal file
168
test/jdk/com/sun/net/httpserver/simpleserver/RequestTest.java
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit 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 static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class RequestTest {
|
||||
|
||||
@Test
|
||||
public void testAddToEmpty() {
|
||||
var headers = new Headers();
|
||||
Request request = new TestHttpExchange(headers);
|
||||
request = request.with("Foo", List.of("Bar"));
|
||||
assertEquals(1, request.getRequestHeaders().size());
|
||||
assertEquals(List.of("Bar"), request.getRequestHeaders().get("Foo"));
|
||||
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(2, request.getRequestHeaders().size());
|
||||
assertEquals(List.of("Bar"), request.getRequestHeaders().get("Foo"));
|
||||
assertEquals(List.of("Bar"), request.getRequestHeaders().get("X-Foo"));
|
||||
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(1, request.getRequestHeaders().size());
|
||||
assertEquals(List.of("Bar"), request.getRequestHeaders().get(headerName));
|
||||
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(4, request.getRequestHeaders().size());
|
||||
assertEquals("Bar", request.getRequestHeaders().getFirst("Foo"));
|
||||
assertEquals("a", request.getRequestHeaders().getFirst("Larry"));
|
||||
assertEquals("b", request.getRequestHeaders().getFirst("Curly"));
|
||||
assertEquals("c", request.getRequestHeaders().getFirst("Moe" ));
|
||||
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,210 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 sun.net.www.MimeTable;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/*
|
||||
* @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 junit/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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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 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(200, response.statusCode());
|
||||
assertEquals(expectedMimeType, response.headers().firstValue("content-type").get());
|
||||
}
|
||||
|
||||
static URI uri(HttpServer server, String path) {
|
||||
return URIBuilder.newBuilder()
|
||||
.host("localhost")
|
||||
.port(server.getAddress().getPort())
|
||||
.scheme("http")
|
||||
.path("/" + path)
|
||||
.buildUnchecked();
|
||||
}
|
||||
|
||||
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,769 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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");
|
||||
|
||||
static final String EXPECTED_LAST_MODIFIED_OF_FAVICON = "Mon, 23 May 1995 11:11:11 GMT";
|
||||
|
||||
@BeforeAll
|
||||
public static 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(200, response.statusCode());
|
||||
assertEquals("some text", response.body());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFavIconGET() throws Exception {
|
||||
var root = Files.createDirectory(TEST_DIR.resolve("testFavIconGET"));
|
||||
|
||||
var server = SimpleFileServer.createFileServer(LOOPBACK_ADDR, root, OutputLevel.VERBOSE);
|
||||
server.start();
|
||||
try {
|
||||
// expect built-in icon
|
||||
var client = HttpClient.newBuilder().proxy(NO_PROXY).build();
|
||||
var request = HttpRequest.newBuilder(uri(server, "favicon.ico")).build();
|
||||
var response = client.send(request, BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("image/x-icon", response.headers().firstValue("content-type").get());
|
||||
assertEquals(EXPECTED_LAST_MODIFIED_OF_FAVICON, response.headers().firstValue("last-modified").get());
|
||||
|
||||
// expect custom (and broken) icon
|
||||
var file = Files.writeString(root.resolve("favicon.ico"), "broken icon", CREATE);
|
||||
try {
|
||||
var lastModified = getLastModified(file);
|
||||
var expectedLength = Long.toString(Files.size(file));
|
||||
response = client.send(request, BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("application/octet-stream", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
} finally {
|
||||
Files.delete(file);
|
||||
}
|
||||
|
||||
// expect built-in icon
|
||||
response = client.send(request, BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("image/x-icon", response.headers().firstValue("content-type").get());
|
||||
assertEquals(EXPECTED_LAST_MODIFIED_OF_FAVICON, response.headers().firstValue("last-modified").get());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFavIconHEAD() throws Exception {
|
||||
var root = Files.createDirectory(TEST_DIR.resolve("testFavIconHEAD"));
|
||||
|
||||
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, "favicon.ico"))
|
||||
.method("HEAD", BodyPublishers.noBody()).build();
|
||||
var response = client.send(request, BodyHandlers.ofString());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertEquals("image/x-icon", response.headers().firstValue("content-type").get());
|
||||
assertEquals(EXPECTED_LAST_MODIFIED_OF_FAVICON, response.headers().firstValue("last-modified").get());
|
||||
assertEquals("", response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals("", response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
public static 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}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(200, response.statusCode());
|
||||
assertEquals(contentType, response.headers().firstValue("content-type").get());
|
||||
assertEquals(contentLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
if (serveIndexFile) {
|
||||
Files.delete(root.resolve(filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotReadableFileGET() throws Exception {
|
||||
Assumptions.assumeFalse(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
file.toFile().setReadable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNotReadableSegmentGET() throws Exception {
|
||||
Assumptions.assumeFalse(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
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);
|
||||
createSymLink(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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);
|
||||
createSymLink(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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSymLink(Path symlink, Path target) {
|
||||
try {
|
||||
Files.createSymbolicLink(symlink, target);
|
||||
} catch (UnsupportedOperationException uoe) {
|
||||
Assumptions.abort("sym link creation not supported");
|
||||
} catch (IOException ioe) {
|
||||
Assumptions.abort("probably insufficient privileges to create sym links (Windows)");
|
||||
}
|
||||
}
|
||||
|
||||
@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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
private static 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 static 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(301, response.statusCode());
|
||||
assertEquals("0", response.headers().firstValue("content-length").get());
|
||||
assertEquals("/aDirectory/", response.headers().firstValue("location").get());
|
||||
|
||||
// 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(301, res2.statusCode());
|
||||
assertEquals("0", res2.headers().firstValue("content-length").get());
|
||||
assertEquals("/aDirectory/?query", res2.headers().firstValue("location").get());
|
||||
}
|
||||
|
||||
{ // 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(200, response.statusCode());
|
||||
assertEquals(expectedBody, response.body());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
}
|
||||
} 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 a directory
|
||||
Path p = Files.createFile(TEST_DIR.resolve("aFile"));
|
||||
assert !Files.isDirectory(p);
|
||||
var iae = assertThrows(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 = assertThrows(IAE, () -> SimpleFileServer.createFileServer(addr, p, OutputLevel.INFO));
|
||||
assertTrue(iae.getMessage().contains("does not exist"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonReadablePath() throws Exception {
|
||||
Assumptions.assumeFalse(Platform.isWindows(), "Not applicable on Windows");
|
||||
var addr = LOOPBACK_ADDR;
|
||||
Path p = Files.createDirectory(TEST_DIR.resolve("aDir"));
|
||||
p.toFile().setReadable(false, false);
|
||||
assert !Files.isReadable(p);
|
||||
try {
|
||||
var iae = assertThrows(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(404, response.statusCode());
|
||||
assertTrue(response.body().contains("beginDelim%3C%3EEndDelim"));
|
||||
assertTrue(response.body().contains("File not found"));
|
||||
} finally {
|
||||
server.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static 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,128 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/othervm/timeout=180 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 static java.lang.System.out;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
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");
|
||||
static final long start = System.nanoTime();
|
||||
public static String now() {
|
||||
long now = System.nanoTime() - start;
|
||||
long secs = now / 1000_000_000;
|
||||
long mill = (now % 1000_000_000) / 1000_000;
|
||||
long nan = now % 1000_000;
|
||||
return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan);
|
||||
}
|
||||
|
||||
static HttpServer simpleFileServer;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
out.println(now() + " creating server");
|
||||
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();
|
||||
out.println(now() + " server started");
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() {
|
||||
out.println(now() + " stopping server");
|
||||
simpleFileServer.stop(0);
|
||||
out.println(now() + " server stopped");
|
||||
}
|
||||
|
||||
// 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 {
|
||||
out.println(now() + " starting test");
|
||||
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(200, response.statusCode());
|
||||
if (i % 100 == 0) {
|
||||
out.print(" " + i + " ");
|
||||
}
|
||||
if (i % 1000 == 0) {
|
||||
out.println(now());
|
||||
}
|
||||
}
|
||||
out.println(now() + " test finished");
|
||||
}
|
||||
|
||||
static URI uri(HttpServer server) {
|
||||
return URIBuilder.newBuilder()
|
||||
.host("localhost")
|
||||
.port(server.getAddress().getPort())
|
||||
.scheme("http")
|
||||
.path("/")
|
||||
.buildUnchecked();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,415 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 junit/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 static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.nio.file.StandardOpenOption.CREATE;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
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");
|
||||
|
||||
@BeforeAll
|
||||
public static 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(200, response.statusCode());
|
||||
assertEquals("some text", response.body());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(200, response.statusCode());
|
||||
assertEquals("text/plain", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
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(200, response.statusCode());
|
||||
assertEquals("text/html; charset=UTF-8", response.headers().firstValue("content-type").get());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals("", response.body());
|
||||
} finally {
|
||||
server.stop(0);
|
||||
root.getFileSystem().close();
|
||||
Files.deleteIfExists(TEST_DIR.resolve("testDirectoryHEAD.zip"));
|
||||
}
|
||||
}
|
||||
|
||||
public static 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}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(200, response.statusCode());
|
||||
assertEquals(contentType, response.headers().firstValue("content-type").get());
|
||||
assertEquals(contentLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(lastModified, response.headers().firstValue("last-modified").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
assertEquals(expectedBody, response.body());
|
||||
} 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(404, response.statusCode());
|
||||
assertEquals(expectedLength, response.headers().firstValue("content-length").get());
|
||||
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(301, response.statusCode());
|
||||
assertEquals("0", response.headers().firstValue("content-length").get());
|
||||
assertEquals("/aDirectory/", response.headers().firstValue("location").get());
|
||||
} 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(404, response.statusCode());
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 the jwebserver command-line tool
|
||||
* @library /test/lib
|
||||
* @modules jdk.httpserver
|
||||
* @run junit/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 static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class CommandLineNegativeTest {
|
||||
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String JWEBSERVER = getJwebserver(JAVA_HOME);
|
||||
static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US";
|
||||
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();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
Files.createDirectories(TEST_DIR);
|
||||
Files.createFile(TEST_FILE);
|
||||
}
|
||||
|
||||
public static Object[][] unknownOption() {
|
||||
return new Object[][] {
|
||||
{"--unknownOption"},
|
||||
{"null"}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("unknownOption")
|
||||
public void testBadOption(String opt) throws Throwable {
|
||||
out.println("\n--- testUnknownOption, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: unknown option: " + opt);
|
||||
}
|
||||
|
||||
public static 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
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("tooManyOptionArgs")
|
||||
public void testTooManyOptionArgs(String opt, String arg) throws Throwable {
|
||||
out.println("\n--- testTooManyOptionArgs, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt, arg, arg)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: unknown option: " + arg);
|
||||
}
|
||||
|
||||
public static 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
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("noArg")
|
||||
public void testNoArg(String opt, String msg) throws Throwable {
|
||||
out.println("\n--- testNoArg, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: no value given for " + opt)
|
||||
.shouldContain(msg);
|
||||
}
|
||||
|
||||
public static 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", "+-"}
|
||||
};
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidValue")
|
||||
public void testInvalidValue(String opt, String val) throws Throwable {
|
||||
out.println("\n--- testInvalidValue, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt, val)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: invalid value given for " + opt + ": " + val);
|
||||
}
|
||||
|
||||
public static Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testPortOutOfRange(String opt) throws Throwable {
|
||||
out.println("\n--- testPortOutOfRange, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt, "65536") // range 0 to 65535
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "port out of range:65536");
|
||||
}
|
||||
|
||||
public static Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(JWEBSERVER, LOCALE_OPT, opt, file)
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "Path is not a directory: " + file);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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(JWEBSERVER, LOCALE_OPT, opt, root.toString())
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "Path does not exist: " + root.toString());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testRootNotReadable(String opt) throws Throwable {
|
||||
out.println("\n--- testRootNotReadable, opt=\"%s\" ".formatted(opt));
|
||||
Assumptions.assumeFalse(Platform.isWindows(), "cannot run on Windows"); // Not applicable to Windows. Reason: cannot revoke an owner's read
|
||||
// access to a directory that was created by that owner
|
||||
Path root = Files.createDirectories(TEST_DIR.resolve("not/readable/dir"));
|
||||
try {
|
||||
root.toFile().setReadable(false, false);
|
||||
assertFalse(Files.isReadable(root));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt, root.toString())
|
||||
.shouldNotHaveExitValue(0)
|
||||
.shouldContain("Error: server config failed: " + "Path is not readable: " + root.toString());
|
||||
} finally {
|
||||
root.toFile().setReadable(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
// --- infra ---
|
||||
|
||||
static String getJwebserver(Path image) {
|
||||
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
|
||||
Path jwebserver = image.resolve("bin").resolve(isWindows ? "jwebserver.exe" : "jwebserver");
|
||||
if (Files.notExists(jwebserver))
|
||||
throw new RuntimeException(jwebserver + " not found");
|
||||
return jwebserver.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,156 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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
|
||||
* @bug 8276848
|
||||
* @summary Tests the jwebserver tool with port not specified
|
||||
* @modules jdk.httpserver
|
||||
* @library /test/lib
|
||||
* @run junit/othervm/manual CommandLinePortNotSpecifiedTest
|
||||
*/
|
||||
|
||||
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 static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CommandLinePortNotSpecifiedTest {
|
||||
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US";
|
||||
static final String JWEBSERVER = getJwebserver(JAVA_HOME);
|
||||
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
|
||||
static final Path TEST_DIR = CWD.resolve("CommandLinePortNotSpecifiedTest");
|
||||
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();
|
||||
|
||||
@BeforeAll
|
||||
public static 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a manual test to confirm the command-line tool starts successfully
|
||||
* in the case where the port is not specified. In this case the server uses
|
||||
* the default port 8000. The test is manual to avoid a BindException in the
|
||||
* unlikely but not impossible case that the port is already in use.
|
||||
*/
|
||||
@Test
|
||||
public void testPortNotSpecified() throws Throwable {
|
||||
out.println("\n--- testPortNotSpecified");
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT)
|
||||
.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);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
// --- infra ---
|
||||
|
||||
static String getJwebserver(Path image) {
|
||||
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
|
||||
Path jwebserver = image.resolve("bin").resolve(isWindows ? "jwebserver.exe" : "jwebserver");
|
||||
if (Files.notExists(jwebserver))
|
||||
throw new RuntimeException(jwebserver + " not found");
|
||||
return jwebserver.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
static final String REGULAR_STARTUP_LINE1_STRING = "Serving";
|
||||
static final String REGULAR_STARTUP_LINE2_STRING = "URL http://";
|
||||
|
||||
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 - Prints this help message and exits.
|
||||
-version, --version - Prints version information and exits.
|
||||
To stop the server, press Ctrl + C.""".formatted(LOOPBACK_ADDR);
|
||||
|
||||
// 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,304 @@
|
|||
/*
|
||||
* Copyright (c) 2021, 2025, 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 the jwebserver command-line tool
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.IPSupport
|
||||
* @modules jdk.httpserver
|
||||
* @run junit/othervm CommandLinePositiveTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
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.net.IPSupport;
|
||||
import jdk.test.lib.process.OutputAnalyzer;
|
||||
import jdk.test.lib.process.ProcessTools;
|
||||
import jdk.test.lib.util.FileUtils;
|
||||
import static java.lang.System.out;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
public class CommandLinePositiveTest {
|
||||
|
||||
static final String JAVA_VERSION = System.getProperty("java.version");
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US";
|
||||
static final String JWEBSERVER = getJwebserver(JAVA_HOME);
|
||||
|
||||
/**
|
||||
* The <b>real path</b> to the current working directory where
|
||||
* <ol>
|
||||
* <li>the web server process will be started in,</li>
|
||||
* <li>and hence, unless given an explicit content root directory, the web
|
||||
* server will be serving from.</li>
|
||||
* </ol>
|
||||
*/
|
||||
private static final Path CWD;
|
||||
|
||||
static {
|
||||
try {
|
||||
CWD = Path.of(".").toRealPath();
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CWD_STR = CWD.toString();
|
||||
|
||||
/**
|
||||
* The <b>real path</b> to the web server content root directory, if one
|
||||
* needs to be provided explicitly.
|
||||
*/
|
||||
private static final Path ROOT_DIR = CWD.resolve("www");
|
||||
|
||||
private static final String ROOT_DIR_STR = ROOT_DIR.toString();
|
||||
|
||||
static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (Files.exists(ROOT_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(ROOT_DIR);
|
||||
}
|
||||
Files.createDirectories(ROOT_DIR);
|
||||
Files.createFile(ROOT_DIR.resolve("file.txt"));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object[][] directoryOptions() { return new Object[][] {{"-d"}, {"--directory"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testAbsDirectory(String opt) throws Throwable {
|
||||
out.printf("\n--- testAbsDirectory, opt=\"%s\"%n", opt);
|
||||
testDirectory(opt, ROOT_DIR_STR);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testRelDirectory(String opt) throws Throwable {
|
||||
out.printf("\n--- testRelDirectory, opt=\"%s\"%n", opt);
|
||||
Path rootRelDir = CWD.relativize(ROOT_DIR);
|
||||
testDirectory(opt, rootRelDir.toString());
|
||||
}
|
||||
|
||||
private static void testDirectory(String opt, String rootDir) throws Throwable {
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, "-p", "0", opt, rootDir)
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
|
||||
.shouldContain("Serving " + ROOT_DIR_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static Object[][] portOptions() { return new Object[][] {{"-p"}, {"--port"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testPort(String opt) throws Throwable {
|
||||
out.println("\n--- testPort, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, 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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static Object[][] helpOptions() { return new Object[][] {{"-h"}, {"-?"}, {"--help"}}; }
|
||||
|
||||
static final String USAGE_TEXT = """
|
||||
Usage: jwebserver [-b bind address] [-p port] [-d directory]
|
||||
[-o none|info|verbose] [-h to show options]
|
||||
[-version to show version information]""";
|
||||
|
||||
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 - Prints this help message and exits.
|
||||
-version, --version - Prints version information and exits.
|
||||
To stop the server, press Ctrl + C.""".formatted(LOOPBACK_ADDR);
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("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
|
||||
JWEBSERVER, LOCALE_OPT, opt)
|
||||
.shouldHaveExitValue(0)
|
||||
.shouldContain(USAGE_TEXT)
|
||||
.shouldContain(OPTIONS_TEXT);
|
||||
}
|
||||
|
||||
public static Object[][] versionOptions() { return new Object[][] {{"-version"}, {"--version"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("versionOptions")
|
||||
public void testVersion(String opt) throws Throwable {
|
||||
out.println("\n--- testVersion, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(WaitForLine.VERSION_STARTUP_LINE,
|
||||
false, // do not explicitly destroy the process
|
||||
JWEBSERVER, LOCALE_OPT, opt)
|
||||
.shouldHaveExitValue(0);
|
||||
}
|
||||
|
||||
public static Object[][] bindOptions() { return new Object[][] {{"-b"}, {"--bind-address"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("bindOptions")
|
||||
public void testBindAllInterfaces(String opt) throws Throwable {
|
||||
out.println("\n--- testBindAllInterfaces, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, "-p", "0", opt, "0.0.0.0")
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
|
||||
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
|
||||
if (IPSupport.hasIPv6()) {
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, opt, "::0")
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on 0.0.0.0 (all interfaces) port")
|
||||
.shouldContain("URL http://" + InetAddress.getLocalHost().getHostAddress());
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("bindOptions")
|
||||
public void testLastOneWinsBindAddress(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsBindAddress, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, "-p", "0", opt, "123.4.5.6", opt, LOOPBACK_ADDR)
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("directoryOptions")
|
||||
public void testLastOneWinsDirectory(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsDirectory, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, "-p", "0", opt, CWD_STR, opt, CWD_STR)
|
||||
.shouldHaveExitValue(NORMAL_EXIT_CODE)
|
||||
.shouldContain("Binding to loopback by default. For all interfaces use \"-b 0.0.0.0\" or \"-b ::\".")
|
||||
.shouldContain("Serving " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
public static Object[][] outputOptions() { return new Object[][] {{"-o"}, {"--output"}}; }
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("outputOptions")
|
||||
public void testLastOneWinsOutput(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsOutput, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, "-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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("portOptions")
|
||||
public void testLastOneWinsPort(String opt) throws Throwable {
|
||||
out.println("\n--- testLastOneWinsPort, opt=\"%s\" ".formatted(opt));
|
||||
simpleserver(JWEBSERVER, LOCALE_OPT, 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 " + CWD_STR + " and subdirectories on " + LOOPBACK_ADDR + " port")
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws IOException {
|
||||
if (Files.exists(ROOT_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(ROOT_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
// --- infra ---
|
||||
|
||||
static String getJwebserver(Path image) {
|
||||
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
|
||||
Path jwebserver = image.resolve("bin").resolve(isWindows ? "jwebserver.exe" : "jwebserver");
|
||||
if (Files.notExists(jwebserver))
|
||||
throw new RuntimeException(jwebserver + " not found");
|
||||
return jwebserver.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
static final String REGULAR_STARTUP_LINE1_STRING = "Serving";
|
||||
static final String REGULAR_STARTUP_LINE2_STRING = "URL http://";
|
||||
static final String VERSION_STARTUP_LINE_STRING = "jwebserver " + JAVA_VERSION;
|
||||
|
||||
// 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()),
|
||||
VERSION_STARTUP_LINE (VERSION_STARTUP_LINE_STRING);
|
||||
|
||||
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),
|
||||
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,100 @@
|
|||
/*
|
||||
* Copyright (c) 2024, 2025, 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.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import jdk.internal.util.OperatingSystem;
|
||||
import jdk.test.lib.net.IPSupport;
|
||||
import jdk.test.lib.process.ProcessTools;
|
||||
import jtreg.SkippedException;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8332020
|
||||
* @summary verifies that when jwebserver is launched with a IPv6 bind address
|
||||
* then the URL printed contains the correct host literal
|
||||
* @modules jdk.httpserver java.base/jdk.internal.util
|
||||
* @library /test/lib
|
||||
* @build jdk.test.lib.net.IPSupport
|
||||
* @run driver IPv6BoundHost
|
||||
*/
|
||||
public class IPv6BoundHost {
|
||||
|
||||
private static final Path JDK_BIN_DIR = Path.of(System.getProperty("java.home")).resolve("bin");
|
||||
private static final Path JWEBSERVER_BINARY = OperatingSystem.isWindows()
|
||||
? JDK_BIN_DIR.resolve("jwebserver.exe") : JDK_BIN_DIR.resolve("jwebserver");
|
||||
private static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US";
|
||||
|
||||
public static void main(final String[] args) throws Exception {
|
||||
IPSupport.printPlatformSupport(System.err); // for debug purposes
|
||||
if (!IPSupport.hasIPv6()) {
|
||||
throw new SkippedException("Skipping test - IPv6 is not supported");
|
||||
}
|
||||
final String output = launchJwebserverAndExit(List.of("-b", "::1", "-p", "0"));
|
||||
if (output.contains("URL http://[::1]:")
|
||||
|| output.contains("URL http://[0:0:0:0:0:0:0:1]:")) {
|
||||
// found expected content
|
||||
System.out.println("found expected URL in jwebserver output");
|
||||
} else {
|
||||
throw new AssertionError("missing IPv6 address in jwebserver process output");
|
||||
}
|
||||
}
|
||||
|
||||
private static String launchJwebserverAndExit(final List<String> args) throws Exception {
|
||||
final Predicate<String> waitForLine = (s) -> s.startsWith("URL http://");
|
||||
final StringBuilder sb = new StringBuilder(); // stdout & stderr
|
||||
final List<String> cmd = new ArrayList<>();
|
||||
cmd.add(JWEBSERVER_BINARY.toString());
|
||||
cmd.add(LOCALE_OPT);
|
||||
cmd.addAll(args);
|
||||
// start the process and await the waitForLine before returning
|
||||
final Process p = ProcessTools.startProcess("8332020-test", new ProcessBuilder(cmd),
|
||||
line -> sb.append(line).append("\n"),
|
||||
waitForLine,
|
||||
30, // suitably high default timeout, not expected to timeout
|
||||
TimeUnit.SECONDS);
|
||||
System.out.println(sb.toString()); // print the process' stdout/stderr
|
||||
// the process has started and it is confirmed that the process output has the line
|
||||
// we were waiting for. now kill the process.
|
||||
p.destroy();
|
||||
final int exitCode = p.waitFor();
|
||||
if (!isNormalExitCode(exitCode)) {
|
||||
throw new AssertionError("jwebserver exited with unexpected exit code: " + exitCode);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static boolean isNormalExitCode(final int exitCode) {
|
||||
final int SIGTERM = 15;
|
||||
if (OperatingSystem.isWindows()) {
|
||||
return exitCode == 1; // we expect exit code == 1 on Windows for Process.destroy()
|
||||
} else {
|
||||
// signal terminated exit code on Unix is 128 + signal value
|
||||
return exitCode == (128 + SIGTERM);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright (c) 2022, 2025, 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
|
||||
* @bug 8278398
|
||||
* @summary Tests the jwebserver's maximum request time
|
||||
* @modules jdk.httpserver
|
||||
* @library /test/lib
|
||||
* @run junit/othervm MaxRequestTimeTest
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
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.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLException;
|
||||
import jdk.test.lib.Platform;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import jdk.test.lib.process.OutputAnalyzer;
|
||||
import jdk.test.lib.process.ProcessTools;
|
||||
import jdk.test.lib.util.FileUtils;
|
||||
import static java.lang.System.out;
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* This test confirms that the jwebserver does not wait indefinitely for
|
||||
* a request to arrive.
|
||||
*
|
||||
* The jwebserver has a maximum request time of 5 seconds, which is set with the
|
||||
* "sun.net.httpserver.maxReqTime" system property. If this threshold is
|
||||
* reached, for example in the case of an HTTPS request where the server keeps
|
||||
* waiting for a plaintext request, the server closes the connection. Subsequent
|
||||
* requests are expected to be handled as normal.
|
||||
*
|
||||
* The test checks in the following order that:
|
||||
* 1. an HTTP request is handled successfully,
|
||||
* 2. an HTTPS request fails due to the server closing the connection
|
||||
* 3. another HTTP request is handled successfully.
|
||||
*/
|
||||
public class MaxRequestTimeTest {
|
||||
static final Path JAVA_HOME = Path.of(System.getProperty("java.home"));
|
||||
static final String LOCALE_OPT = "-J-Duser.language=en -J-Duser.country=US";
|
||||
static final String JWEBSERVER = getJwebserver(JAVA_HOME);
|
||||
static final Path CWD = Path.of(".").toAbsolutePath().normalize();
|
||||
static final Path TEST_DIR = CWD.resolve("MaxRequestTimeTest");
|
||||
static final String LOOPBACK_ADDR = InetAddress.getLoopbackAddress().getHostAddress();
|
||||
static final AtomicInteger PORT = new AtomicInteger();
|
||||
|
||||
private static final SSLContext sslContext = SimpleSSLContext.findSSLContext();
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
Files.createDirectories(TEST_DIR);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMaxRequestTime() throws Throwable {
|
||||
final var sb = new StringBuffer(); // stdout & stderr
|
||||
final var p = startProcess("jwebserver", sb);
|
||||
try {
|
||||
sendHTTPSRequest(); // server expected to terminate connection
|
||||
sendHTTPRequest(); // server expected to respond successfully
|
||||
sendHTTPSRequest(); // server expected to terminate connection
|
||||
sendHTTPRequest(); // server expected to respond successfully
|
||||
} finally {
|
||||
p.destroy();
|
||||
int exitCode = p.waitFor();
|
||||
checkOutput(sb, exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
static String expectedBody = """
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Directory listing for /</h1>
|
||||
<ul>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
""";
|
||||
|
||||
static void sendHTTPRequest() throws IOException, InterruptedException {
|
||||
out.println("\n--- sendHTTPRequest");
|
||||
var client = HttpClient.newBuilder()
|
||||
.proxy(NO_PROXY)
|
||||
.build();
|
||||
var request = HttpRequest.newBuilder(URI.create("http://localhost:" + PORT.get() + "/")).build();
|
||||
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(expectedBody, response.body());
|
||||
}
|
||||
|
||||
static void sendHTTPSRequest() throws IOException, InterruptedException {
|
||||
out.println("\n--- sendHTTPSRequest");
|
||||
var client = HttpClient.newBuilder()
|
||||
.sslContext(sslContext)
|
||||
.proxy(NO_PROXY)
|
||||
.build();
|
||||
var request = HttpRequest.newBuilder(URI.create("https://localhost:" + PORT.get() + "/")).build();
|
||||
try {
|
||||
client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
throw new RuntimeException("Expected SSLException not thrown");
|
||||
} catch (SSLException expected) { // server closes connection when max request time is reached
|
||||
expected.printStackTrace(System.out);
|
||||
}
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void teardown() throws IOException {
|
||||
if (Files.exists(TEST_DIR)) {
|
||||
FileUtils.deleteFileTreeWithRetry(TEST_DIR);
|
||||
}
|
||||
}
|
||||
|
||||
// --- infra ---
|
||||
|
||||
static String getJwebserver(Path image) {
|
||||
boolean isWindows = System.getProperty("os.name").startsWith("Windows");
|
||||
Path jwebserver = image.resolve("bin").resolve(isWindows ? "jwebserver.exe" : "jwebserver");
|
||||
if (Files.notExists(jwebserver))
|
||||
throw new RuntimeException(jwebserver + " not found");
|
||||
return jwebserver.toAbsolutePath().toString();
|
||||
}
|
||||
|
||||
// The stdout/stderr output line to wait for when starting the jwebserver
|
||||
static final String REGULAR_STARTUP_LINE_STRING_1 = "URL http://";
|
||||
static final String REGULAR_STARTUP_LINE_STRING_2 = "Serving ";
|
||||
|
||||
static void parseAndSetPort(String line) {
|
||||
PORT.set(Integer.parseInt(line.split(" port ")[1]));
|
||||
}
|
||||
|
||||
static Process startProcess(String name, StringBuffer sb) throws Throwable {
|
||||
// starts the process, parses the port and awaits startup line before sending requests
|
||||
return ProcessTools.startProcess(name,
|
||||
new ProcessBuilder(JWEBSERVER, LOCALE_OPT, "-p", "0").directory(TEST_DIR.toFile()),
|
||||
line -> {
|
||||
if (line.startsWith(REGULAR_STARTUP_LINE_STRING_2)) { parseAndSetPort(line); }
|
||||
sb.append(line + "\n");
|
||||
},
|
||||
line -> line.startsWith(REGULAR_STARTUP_LINE_STRING_1),
|
||||
30, // suitably high default timeout, not expected to timeout
|
||||
TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
static void checkOutput(StringBuffer sb, int exitCode) {
|
||||
out.println("\n--- server output: \n" + sb);
|
||||
var outputAnalyser = new OutputAnalyzer(sb.toString(), "", exitCode);
|
||||
outputAnalyser.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 + " and subdirectories on " + LOOPBACK_ADDR + " port " + PORT)
|
||||
.shouldContain("URL http://" + LOOPBACK_ADDR);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue