blob: abcf91ad1148d406222ff13cc03d9cb232d079ed [file] [log] [blame]
/*
* Copyright (c) 2017, 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 Basic security checks for WebSocket URI from the Builder
* @compile ../DummyWebSocketServer.java ../../ProxyServer.java
* @run testng/othervm/java.security.policy=httpclient.policy WSURLPermissionTest
*/
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.URLPermission;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permission;
import java.security.Permissions;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import org.testng.annotations.AfterTest;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
public class WSURLPermissionTest {
static AccessControlContext withPermissions(Permission... perms) {
Permissions p = new Permissions();
for (Permission perm : perms) {
p.add(perm);
}
ProtectionDomain pd = new ProtectionDomain(null, p);
return new AccessControlContext(new ProtectionDomain[]{ pd });
}
static AccessControlContext noPermissions() {
return withPermissions(/*empty*/);
}
URI wsURI;
DummyWebSocketServer webSocketServer;
InetSocketAddress proxyAddress;
@BeforeTest
public void setup() throws Exception {
ProxyServer proxyServer = new ProxyServer(0, true);
proxyAddress = new InetSocketAddress(InetAddress.getLoopbackAddress(),
proxyServer.getPort());
webSocketServer = new DummyWebSocketServer();
webSocketServer.open();
wsURI = webSocketServer.getURI();
System.out.println("Proxy Server: " + proxyAddress);
System.out.println("DummyWebSocketServer: " + wsURI);
}
@AfterTest
public void teardown() {
webSocketServer.close();
}
static class NoOpListener implements WebSocket.Listener {}
static final WebSocket.Listener noOpListener = new NoOpListener();
@DataProvider(name = "passingScenarios")
public Object[][] passingScenarios() {
HttpClient noProxyClient = HttpClient.newHttpClient();
return new Object[][]{
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // no actions
new URLPermission[] { new URLPermission(wsURI.toString()) },
"0" /* for log file identification */ },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // scheme wildcard
new URLPermission[] { new URLPermission("ws://*") },
"0.1" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // port wildcard
new URLPermission[] { new URLPermission("ws://"+wsURI.getHost()+":*") },
"0.2" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // empty actions
new URLPermission[] { new URLPermission(wsURI.toString(), "") },
"1" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // colon
new URLPermission[] { new URLPermission(wsURI.toString(), ":") },
"2" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // wildcard
new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") },
"3" },
// WS permission checking is agnostic of method, any/none will do
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // specific method
new URLPermission[] { new URLPermission(wsURI.toString(), "GET") },
"3.1" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // specific method
new URLPermission[] { new URLPermission(wsURI.toString(), "POST") },
"3.2" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // path
new URLPermission[] { new URLPermission(wsURI.resolve("/path/x").toString()) },
"4" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // same dir wildcard
new URLPermission[] { new URLPermission(wsURI.resolve("/path/*").toString()) },
"5" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // recursive
new URLPermission[] { new URLPermission(wsURI.resolve("/path/-").toString()) },
"6" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // recursive top
new URLPermission[] { new URLPermission(wsURI.resolve("/-").toString()) },
"7" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // header
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") },
"8" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // header
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // wildcard
new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
"9" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // headers
.header("B-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header,B-Header") },
"10" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // headers
.header("B-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // wildcard
new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
"11" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // headers
.header("B-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // wildcards
new URLPermission[] { new URLPermission(wsURI.toString(), "*:*") },
"12" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // multi-value
.header("A-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // wildcard
new URLPermission[] { new URLPermission(wsURI.toString(), ":*") },
"13" },
{ (PrivilegedExceptionAction<?>)() -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value") // multi-value
.header("A-Header", "B-Value") // headers
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // single grant
new URLPermission[] { new URLPermission(wsURI.toString(), ":A-Header") },
"14" },
// client with a DIRECT proxy
{ (PrivilegedExceptionAction<?>)() -> {
ProxySelector ps = ProxySelector.of(null);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] { new URLPermission(wsURI.toString()) },
"15" },
// client with a SOCKS proxy! ( expect implementation to ignore SOCKS )
{ (PrivilegedExceptionAction<?>)() -> {
ProxySelector ps = new ProxySelector() {
@Override public List<Proxy> select(URI uri) {
return List.of(new Proxy(Proxy.Type.SOCKS, proxyAddress)); }
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { }
};
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] { new URLPermission(wsURI.toString()) },
"16" },
// client with a HTTP/HTTPS proxy
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()), // CONNECT action string
new URLPermission("socket://"+proxyAddress.getHostName()
+":"+proxyAddress.getPort(), "CONNECT")},
"17" },
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()), // no action string
new URLPermission("socket://"+proxyAddress.getHostName()
+":"+proxyAddress.getPort())},
"18" },
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()), // wildcard headers
new URLPermission("socket://"+proxyAddress.getHostName()
+":"+proxyAddress.getPort(), "CONNECT:*")},
"19" },
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
CountingProxySelector ps = CountingProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
assertEquals(ps.count(), 1); // ps.select only invoked once
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()), // empty headers
new URLPermission("socket://"+proxyAddress.getHostName()
+":"+proxyAddress.getPort(), "CONNECT:")},
"20" },
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()),
new URLPermission("socket://*")}, // wildcard socket URL
"21" },
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission("ws://*"), // wildcard ws URL
new URLPermission("socket://*")}, // wildcard socket URL
"22" },
};
}
@Test(dataProvider = "passingScenarios")
public void testWithNoSecurityManager(PrivilegedExceptionAction<?> action,
URLPermission[] unused,
String dataProviderId)
throws Exception
{
// sanity ( no security manager )
System.setSecurityManager(null);
try {
AccessController.doPrivileged(action);
} finally {
System.setSecurityManager(new SecurityManager());
}
}
@Test(dataProvider = "passingScenarios")
public void testWithAllPermissions(PrivilegedExceptionAction<?> action,
URLPermission[] unused,
String dataProviderId)
throws Exception
{
// Run with all permissions, i.e. no further restrictions than test's AllPermission
assert System.getSecurityManager() != null;
AccessController.doPrivileged(action);
}
@Test(dataProvider = "passingScenarios")
public void testWithMinimalPermissions(PrivilegedExceptionAction<?> action,
URLPermission[] perms,
String dataProviderId)
throws Exception
{
// Run with minimal permissions, i.e. just what is required
assert System.getSecurityManager() != null;
AccessControlContext minimalACC = withPermissions(perms);
AccessController.doPrivileged(action, minimalACC);
}
@Test(dataProvider = "passingScenarios")
public void testWithNoPermissions(PrivilegedExceptionAction<?> action,
URLPermission[] unused,
String dataProviderId)
throws Exception
{
// Run with NO permissions, i.e. expect SecurityException
assert System.getSecurityManager() != null;
try {
AccessController.doPrivileged(action, noPermissions());
fail("EXPECTED SecurityException");
} catch (PrivilegedActionException expected) {
Throwable t = expected.getCause();
if (t instanceof ExecutionException)
t = t.getCause();
if (t instanceof SecurityException)
System.out.println("Caught expected SE:" + expected);
else
fail("Expected SecurityException, but got: " + t);
}
}
// --- Negative tests ---
@DataProvider(name = "failingScenarios")
public Object[][] failingScenarios() {
HttpClient noProxyClient = HttpClient.newHttpClient();
return new Object[][]{
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null;
},
new URLPermission[]{ /* no permissions */ },
"50" /* for log file identification */},
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // wrong scheme
new URLPermission[]{ new URLPermission("http://*") },
"51" },
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // wrong scheme
new URLPermission[]{ new URLPermission("socket://*") },
"52" },
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // wrong host
new URLPermission[]{ new URLPermission("ws://foo.com/") },
"53" },
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // wrong port
new URLPermission[]{ new URLPermission("ws://"+ wsURI.getHost()+":5") },
"54" },
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value")
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // only perm to set B not A
new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") },
"55" },
{ (PrivilegedExceptionAction<?>) () -> {
noProxyClient.newWebSocketBuilder()
.header("A-Header", "A-Value")
.header("B-Header", "B-Value")
.buildAsync(wsURI, noOpListener).get().abort();
return null;
}, // only perm to set B not A
new URLPermission[] { new URLPermission(wsURI.toString(), "*:B-Header") },
"56" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // wrong path
new URLPermission[] { new URLPermission(wsURI.resolve("/aDiffPath/").toString()) },
"57" },
{ (PrivilegedExceptionAction<?>)() -> {
URI uriWithPath = wsURI.resolve("/path/x");
noProxyClient.newWebSocketBuilder()
.buildAsync(uriWithPath, noOpListener).get().abort();
return null; }, // more specific path
new URLPermission[] { new URLPermission(wsURI.resolve("/path/x/y").toString()) },
"58" },
// client with a HTTP/HTTPS proxy
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; }, // missing proxy perm
new URLPermission[] { new URLPermission(wsURI.toString()) },
"100" },
// client with a HTTP/HTTPS proxy
{ (PrivilegedExceptionAction<?>)() -> {
assert proxyAddress != null;
ProxySelector ps = ProxySelector.of(proxyAddress);
HttpClient client = HttpClient.newBuilder().proxy(ps).build();
client.newWebSocketBuilder()
.buildAsync(wsURI, noOpListener).get().abort();
return null; },
new URLPermission[] {
new URLPermission(wsURI.toString()), // missing proxy CONNECT
new URLPermission("socket://*", "GET") },
"101" },
};
}
@Test(dataProvider = "failingScenarios")
public void testWithoutEnoughPermissions(PrivilegedExceptionAction<?> action,
URLPermission[] perms,
String dataProviderId)
throws Exception
{
// Run without Enough permissions, i.e. expect SecurityException
assert System.getSecurityManager() != null;
AccessControlContext notEnoughPermsACC = withPermissions(perms);
try {
AccessController.doPrivileged(action, notEnoughPermsACC);
fail("EXPECTED SecurityException");
} catch (PrivilegedActionException expected) {
Throwable t = expected.getCause();
if (t instanceof ExecutionException)
t = t.getCause();
if (t instanceof SecurityException)
System.out.println("Caught expected SE:" + expected);
else
fail("Expected SecurityException, but got: " + t);
}
}
/**
* A Proxy Selector that wraps a ProxySelector.of(), and counts the number
* of times its select method has been invoked. This can be used to ensure
* that the Proxy Selector is invoked only once per WebSocket.Builder::buildAsync
* invocation.
*/
static class CountingProxySelector extends ProxySelector {
private final ProxySelector proxySelector;
private volatile int count; // 0
private CountingProxySelector(InetSocketAddress proxyAddress) {
proxySelector = ProxySelector.of(proxyAddress);
}
public static CountingProxySelector of(InetSocketAddress proxyAddress) {
return new CountingProxySelector(proxyAddress);
}
int count() { return count; }
@Override
public List<Proxy> select(URI uri) {
System.out.println("PS: uri");
Throwable t = new Throwable();
t.printStackTrace(System.out);
count++;
return proxySelector.select(uri);
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
proxySelector.connectFailed(uri, sa, ioe);
}
}
}