Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions impl/src/main/java/com/sun/faces/context/ExternalContextImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

import static com.sun.faces.RIConstants.PUSH_RESOURCE_URLS_KEY_NAME;
import static com.sun.faces.context.ContextParam.SendPoweredByHeader;
import static com.sun.faces.context.UrlBuilder.PROTOCOL_SEPARATOR;
import static com.sun.faces.context.UrlBuilder.WEBSOCKET_PROTOCOL;
import static com.sun.faces.util.Util.isEmpty;

import java.io.BufferedWriter;
Expand Down Expand Up @@ -573,15 +571,9 @@ public String encodeWebsocketURL(String url) {
port = requestURL.getPort();
}

final String fixedPortRequestURL = new URL(requestURL.getProtocol(), requestURL.getHost(), port, url).toExternalForm();
final String websocketURL = new URL(requestURL.getProtocol(), requestURL.getHost(), port, url).toExternalForm();

// the index after http or https ...
int protocolEndIndex = fixedPortRequestURL.indexOf(PROTOCOL_SEPARATOR);

// ws://[...]
final String websocketURL = WEBSOCKET_PROTOCOL + fixedPortRequestURL.substring(protocolEndIndex);

return encodeResourceURL( websocketURL );
return encodeResourceURL(websocketURL.replaceFirst("http", "ws"));
}
catch (MalformedURLException e) {
return url;
Expand Down
2 changes: 0 additions & 2 deletions impl/src/main/java/com/sun/faces/context/UrlBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ class UrlBuilder {
public static final char PARAMETER_PAIR_SEPARATOR = '&';
public static final char PARAMETER_NAME_VALUE_SEPARATOR = '=';
public static final char FRAGMENT_SEPARATOR = '#';
public static final String WEBSOCKET_PROTOCOL = "ws";
public static final String PROTOCOL_SEPARATOR = "://";

private final StringBuilder url;
private String path;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

import java.io.IOException;
Expand All @@ -33,10 +38,16 @@
import java.util.Map.Entry;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import jakarta.faces.context.FacesContext;
import jakarta.faces.push.PushContext;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -47,6 +58,8 @@
*/
public class ExternalContextImplTest {

private static final String WEBSOCKET_PATH = "/ctx/jakarta.faces.push/x";

/**
* Test getRequestCookieMap method.
*/
Expand Down Expand Up @@ -242,6 +255,91 @@ public void testReleaseDrainsWithoutFlushing() throws IOException {
assertFalse(container.flushed, "container writer must not be flushed by release()");
}

/**
* Test that encodeWebsocketURL derives the websocket scheme from the request scheme: {@code https} gives {@code wss}
* and {@code http} gives {@code ws}, as a browser refuses {@code ws} on a page served over {@code https}. The port
* comes from the context param when that is positive, else from the request.
*/
@ParameterizedTest
@CsvSource({
"https://host:8443/ctx/page.xhtml, , wss://host:8443/ctx/jakarta.faces.push/x",
"https://host/ctx/page.xhtml, , wss://host/ctx/jakarta.faces.push/x",
"https://host:443/ctx/page.xhtml, , wss://host:443/ctx/jakarta.faces.push/x",
"http://host:8080/ctx/page.xhtml, , ws://host:8080/ctx/jakarta.faces.push/x",
"http://host/ctx/page.xhtml, , ws://host/ctx/jakarta.faces.push/x",
"http://host:80/ctx/page.xhtml, , ws://host:80/ctx/jakarta.faces.push/x",
"HTTPS://host/ctx/page.xhtml, , wss://host/ctx/jakarta.faces.push/x",
"HTTP://host/ctx/page.xhtml, , ws://host/ctx/jakarta.faces.push/x",
"https://[::1]:8443/ctx/page.xhtml, , wss://[::1]:8443/ctx/jakarta.faces.push/x",
"https://http.example.com/ctx/page.xhtml, , wss://http.example.com/ctx/jakarta.faces.push/x",
"http://https.example.com/ctx/page.xhtml, , ws://https.example.com/ctx/jakarta.faces.push/x",
"https://host:8443/ctx/page.xhtml?a=b#c, , wss://host:8443/ctx/jakarta.faces.push/x",
"https://host:8443/ctx/page.xhtml, 9443, wss://host:9443/ctx/jakarta.faces.push/x",
"https://host/ctx/page.xhtml, 9443, wss://host:9443/ctx/jakarta.faces.push/x",
"http://host:8080/ctx/page.xhtml, 9080, ws://host:9080/ctx/jakarta.faces.push/x",
"http://host/ctx/page.xhtml, 9080, ws://host:9080/ctx/jakarta.faces.push/x",
"https://host:8443/ctx/page.xhtml, 0, wss://host:8443/ctx/jakarta.faces.push/x",
"https://host:8443/ctx/page.xhtml, -1, wss://host:8443/ctx/jakarta.faces.push/x",
"http://host:8080/ctx/page.xhtml, 0, ws://host:8080/ctx/jakarta.faces.push/x",
})
public void testEncodeWebsocketURLSchemeAndPort(String requestURL, String configuredPort, String expected) {
assertEquals(expected, encodeWebsocketURL(requestURL, configuredPort, WEBSOCKET_PATH, UnaryOperator.identity()));
}

/**
* Test that only the scheme of the request is replaced, so an occurrence of "http" elsewhere in the URL survives.
*/
@ParameterizedTest
@CsvSource({
"https://host/page.xhtml, /http/jakarta.faces.push/x, wss://host/http/jakarta.faces.push/x",
"https://host/page.xhtml, /https/jakarta.faces.push/http, wss://host/https/jakarta.faces.push/http",
"http://host/page.xhtml, /http/jakarta.faces.push/https, ws://host/http/jakarta.faces.push/https",
})
public void testEncodeWebsocketURLKeepsHttpOutsideScheme(String requestURL, String url, String expected) {
assertEquals(expected, encodeWebsocketURL(requestURL, null, url, UnaryOperator.identity()));
}

/**
* Test that the websocket URL is passed through the response URL encoding, which is what
* {@code ExternalContext#encodeResourceURL} contributes.
*/
@Test
public void testEncodeWebsocketURLAppliesResponseEncoding() {
String encoded = encodeWebsocketURL("https://host/ctx/page.xhtml", null, WEBSOCKET_PATH, url -> url + ";jsessionid=abc");

assertEquals("wss://host/ctx/jakarta.faces.push/x;jsessionid=abc", encoded);
}

/**
* Test that a request URL which cannot be parsed yields the input URL unchanged.
*/
@Test
public void testEncodeWebsocketURLReturnsInputOnMalformedRequestURL() {
assertEquals(WEBSOCKET_PATH, encodeWebsocketURL("not-a-url", null, WEBSOCKET_PATH, UnaryOperator.identity()));
}

/**
* Test that a null URL is refused with a NullPointerException, as the method contract demands.
*/
@Test
public void testEncodeWebsocketURLRejectsNull() {
assertThrows(NullPointerException.class, () -> encodeWebsocketURL("https://host/ctx/page.xhtml", null, null, UnaryOperator.identity()));
}

private String encodeWebsocketURL(String requestURL, String configuredPort, String url, UnaryOperator<String> responseEncoder) {
ServletContext servletContext = mock(ServletContext.class);
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);
when(servletContext.getInitParameter(PushContext.WEBSOCKET_ENDPOINT_PORT_PARAM_NAME)).thenReturn(configuredPort);
when(request.getRequestURL()).thenReturn(new StringBuffer(requestURL));
when(response.encodeURL(anyString())).thenAnswer(invocation -> responseEncoder.apply(invocation.getArgument(0)));

try (MockedStatic<FacesContext> facesContext = mockStatic(FacesContext.class)) {
facesContext.when(FacesContext::getCurrentInstance).thenReturn(mock(FacesContext.class, RETURNS_DEEP_STUBS));
return new ExternalContextImpl(servletContext, request, response).encodeWebsocketURL(url);
}
}

private static class FlushRecordingWriter extends StringWriter {

private boolean flushed;
Expand Down
Loading