aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web/src/main/java
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-10-02 15:45:49 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-10-03 20:06:53 +0200
commit70ac4438b05136c54017d98a883bf6bf0f938d47 (patch)
tree2418f8670bf8ff2d7e6f6be2768c112e56de2a79 /subprojects/language-web/src/main/java
parentfeat(frontend): animate GenerateButton (diff)
downloadrefinery-70ac4438b05136c54017d98a883bf6bf0f938d47.tar.gz
refinery-70ac4438b05136c54017d98a883bf6bf0f938d47.tar.zst
refinery-70ac4438b05136c54017d98a883bf6bf0f938d47.zip
feat(frontend): enable cross-origin isolation
Diffstat (limited to 'subprojects/language-web/src/main/java')
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java26
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java32
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java57
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java27
4 files changed, 78 insertions, 64 deletions
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
index 8ac8a21e..fbce62c1 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/CacheControlFilter.java
@@ -1,21 +1,15 @@
1package tools.refinery.language.web; 1package tools.refinery.language.web;
2 2
3import jakarta.servlet.*;
4import jakarta.servlet.http.HttpServletRequest;
5import jakarta.servlet.http.HttpServletResponse;
6import org.eclipse.jetty.http.HttpHeader;
7
3import java.io.IOException; 8import java.io.IOException;
4import java.time.Duration; 9import java.time.Duration;
5import java.util.Set; 10import java.util.Set;
6import java.util.regex.Pattern; 11import java.util.regex.Pattern;
7 12
8import org.eclipse.jetty.http.HttpHeader;
9
10import jakarta.servlet.Filter;
11import jakarta.servlet.FilterChain;
12import jakarta.servlet.FilterConfig;
13import jakarta.servlet.ServletException;
14import jakarta.servlet.ServletRequest;
15import jakarta.servlet.ServletResponse;
16import jakarta.servlet.http.HttpServletRequest;
17import jakarta.servlet.http.HttpServletResponse;
18
19public class CacheControlFilter implements Filter { 13public class CacheControlFilter implements Filter {
20 private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2?)"); 14 private static final Pattern CACHE_URI_PATTERN = Pattern.compile(".*\\.(css|gif|js|map|png|svg|woff2?)");
21 15
@@ -29,11 +23,6 @@ public class CacheControlFilter implements Filter {
29 private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate"; 23 private static final String CACHE_CONTROL_NO_CACHE_VALUE = "no-cache, no-store, max-age: 0, must-revalidate";
30 24
31 @Override 25 @Override
32 public void init(FilterConfig filterConfig) throws ServletException {
33 // Nothing to initialize.
34 }
35
36 @Override
37 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 26 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
38 throws IOException, ServletException { 27 throws IOException, ServletException {
39 if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) { 28 if (request instanceof HttpServletRequest httpRequest && response instanceof HttpServletResponse httpResponse) {
@@ -49,9 +38,4 @@ public class CacheControlFilter implements Filter {
49 } 38 }
50 chain.doFilter(request, response); 39 chain.doFilter(request, response);
51 } 40 }
52
53 @Override
54 public void destroy() {
55 // Nothing to dispose.
56 }
57} 41}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
new file mode 100644
index 00000000..40dd7ee5
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/SecurityHeadersFilter.java
@@ -0,0 +1,32 @@
1package tools.refinery.language.web;
2
3import jakarta.servlet.*;
4import jakarta.servlet.http.HttpServletResponse;
5
6import java.io.IOException;
7
8public class SecurityHeadersFilter implements Filter {
9 @Override
10 public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
11 ServletException {
12 if (response instanceof HttpServletResponse httpResponse) {
13 httpResponse.setHeader("Content-Security-Policy", "default-src 'self'; " +
14 // CodeMirror needs inline styles, see e.g.,
15 // https://discuss.codemirror.net/t/inline-styles-and-content-security-policy/1311/2
16 "style-src 'self' 'unsafe-inline'; " +
17 // Use 'data:' for displaying inline SVG backgrounds.
18 "img-src 'self' data:; " +
19 "object-src 'none'; " +
20 "base-uri 'none';");
21 httpResponse.setHeader("X-Content-Type-Options", "nosniff");
22 httpResponse.setHeader("X-Frame-Options", "DENY");
23 httpResponse.setHeader("Referrer-Policy", "strict-origin");
24 // Enable cross-origin isolation, https://web.dev/cross-origin-isolation-guide/
25 httpResponse.setHeader("Cross-Origin-Opener-Policy", "same-origin");
26 httpResponse.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
27 // We do not expose any sensitive data over HTTP, so <code>cross-origin</code> is safe here.
28 httpResponse.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
29 }
30 chain.doFilter(request, response);
31 }
32}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
index ffd903d0..ffa61321 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
@@ -3,15 +3,8 @@
3 */ 3 */
4package tools.refinery.language.web; 4package tools.refinery.language.web;
5 5
6import java.io.File; 6import jakarta.servlet.DispatcherType;
7import java.io.IOException; 7import jakarta.servlet.SessionTrackingMode;
8import java.net.InetSocketAddress;
9import java.net.URI;
10import java.net.URISyntaxException;
11import java.util.EnumSet;
12import java.util.Optional;
13import java.util.Set;
14
15import org.eclipse.jetty.server.Server; 8import org.eclipse.jetty.server.Server;
16import org.eclipse.jetty.server.session.SessionHandler; 9import org.eclipse.jetty.server.session.SessionHandler;
17import org.eclipse.jetty.servlet.DefaultServlet; 10import org.eclipse.jetty.servlet.DefaultServlet;
@@ -21,11 +14,16 @@ import org.eclipse.jetty.util.resource.Resource;
21import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; 14import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer;
22import org.slf4j.Logger; 15import org.slf4j.Logger;
23import org.slf4j.LoggerFactory; 16import org.slf4j.LoggerFactory;
24
25import jakarta.servlet.DispatcherType;
26import jakarta.servlet.SessionTrackingMode;
27import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet; 17import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;
28 18
19import java.io.File;
20import java.io.IOException;
21import java.net.InetSocketAddress;
22import java.net.URI;
23import java.net.URISyntaxException;
24import java.util.EnumSet;
25import java.util.Set;
26
29public class ServerLauncher { 27public class ServerLauncher {
30 public static final String DEFAULT_LISTEN_ADDRESS = "localhost"; 28 public static final String DEFAULT_LISTEN_ADDRESS = "localhost";
31 29
@@ -36,24 +34,25 @@ public class ServerLauncher {
36 public static final int HTTP_DEFAULT_PORT = 80; 34 public static final int HTTP_DEFAULT_PORT = 80;
37 35
38 public static final int HTTPS_DEFAULT_PORT = 443; 36 public static final int HTTPS_DEFAULT_PORT = 443;
39 37
40 public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; 38 public static final String ALLOWED_ORIGINS_SEPARATOR = ",";
41 39
42 private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class); 40 private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class);
43 41
44 private final Server server; 42 private final Server server;
45 43
46 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, Optional<String[]> allowedOrigins) { 44 public ServerLauncher(InetSocketAddress bindAddress, Resource baseResource, String[] allowedOrigins) {
47 server = new Server(bindAddress); 45 server = new Server(bindAddress);
48 var handler = new ServletContextHandler(); 46 var handler = new ServletContextHandler();
49 addSessionHandler(handler); 47 addSessionHandler(handler);
50 addProblemServlet(handler, allowedOrigins); 48 addProblemServlet(handler, allowedOrigins);
51 if (baseResource != null) { 49 if (baseResource != null) {
52 handler.setBaseResource(baseResource); 50 handler.setBaseResource(baseResource);
53 handler.setWelcomeFiles(new String[] { "index.html" }); 51 handler.setWelcomeFiles(new String[]{"index.html"});
54 addDefaultServlet(handler); 52 addDefaultServlet(handler);
55 } 53 }
56 handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); 54 handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
55 handler.addFilter(SecurityHeadersFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
57 server.setHandler(handler); 56 server.setHandler(handler);
58 } 57 }
59 58
@@ -63,13 +62,13 @@ public class ServerLauncher {
63 handler.setSessionHandler(sessionHandler); 62 handler.setSessionHandler(sessionHandler);
64 } 63 }
65 64
66 private void addProblemServlet(ServletContextHandler handler, Optional<String[]> allowedOrigins) { 65 private void addProblemServlet(ServletContextHandler handler, String[] allowedOrigins) {
67 var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class); 66 var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class);
68 if (allowedOrigins.isEmpty()) { 67 if (allowedOrigins == null) {
69 LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!"); 68 LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!");
70 } else { 69 } else {
71 var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR, 70 var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR,
72 allowedOrigins.get()); 71 allowedOrigins);
73 problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM, 72 problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM,
74 allowedOriginsString); 73 allowedOriginsString);
75 } 74 }
@@ -141,8 +140,8 @@ public class ServerLauncher {
141 return Resource.newResource(webRootUri); 140 return Resource.newResource(webRootUri);
142 } 141 }
143 // Look for unpacked production artifacts (convenience for running from IDE). 142 // Look for unpacked production artifacts (convenience for running from IDE).
144 var unpackedResourcePathComponents = new String[] { System.getProperty("user.dir"), "build", "webpack", 143 var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack",
145 "production" }; 144 "production"};
146 var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents)); 145 var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents));
147 if (unpackedResourceDir.isDirectory()) { 146 if (unpackedResourceDir.isDirectory()) {
148 return Resource.newResource(unpackedResourceDir); 147 return Resource.newResource(unpackedResourceDir);
@@ -164,29 +163,31 @@ public class ServerLauncher {
164 if (portStr != null) { 163 if (portStr != null) {
165 return Integer.parseInt(portStr); 164 return Integer.parseInt(portStr);
166 } 165 }
167 return DEFAULT_LISTEN_PORT; 166 return DEFAULT_PUBLIC_PORT;
168 } 167 }
169 168
170 private static Optional<String[]> getAllowedOrigins() { 169 private static String[] getAllowedOrigins() {
171 var allowedOrigins = System.getenv("ALLOWED_ORIGINS"); 170 var allowedOrigins = System.getenv("ALLOWED_ORIGINS");
172 if (allowedOrigins != null) { 171 if (allowedOrigins != null) {
173 return Optional.of(allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR)); 172 return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR);
174 } 173 }
175 return getAllowedOriginsFromPublicHostAndPort(); 174 return getAllowedOriginsFromPublicHostAndPort();
176 } 175 }
177 176
178 private static Optional<String[]> getAllowedOriginsFromPublicHostAndPort() { 177 // This method returns <code>null</code> to indicate that all origins are allowed.
178 @SuppressWarnings("squid:S1168")
179 private static String[] getAllowedOriginsFromPublicHostAndPort() {
179 var publicHost = getPublicHost(); 180 var publicHost = getPublicHost();
180 if (publicHost == null) { 181 if (publicHost == null) {
181 return Optional.empty(); 182 return null;
182 } 183 }
183 int publicPort = getPublicPort(); 184 int publicPort = getPublicPort();
184 var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http"; 185 var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http";
185 var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort); 186 var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort);
186 if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) { 187 if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) {
187 var urlWithoutPort = String.format("%s://%s", scheme, publicHost); 188 var urlWithoutPort = String.format("%s://%s", scheme, publicHost);
188 return Optional.of(new String[] { urlWithPort, urlWithoutPort }); 189 return new String[]{urlWithPort, urlWithoutPort};
189 } 190 }
190 return Optional.of(new String[] { urlWithPort }); 191 return new String[]{urlWithPort};
191 } 192 }
192} 193}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
index 942ca380..a2ad2943 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocketServlet.java
@@ -1,28 +1,25 @@
1package tools.refinery.language.web.xtext.servlet; 1package tools.refinery.language.web.xtext.servlet;
2 2
3import java.io.IOException; 3import jakarta.servlet.ServletConfig;
4import java.time.Duration; 4import jakarta.servlet.ServletException;
5import java.util.Set; 5import org.eclipse.jetty.websocket.server.*;
6
7import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest;
8import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse;
9import org.eclipse.jetty.websocket.server.JettyWebSocketCreator;
10import org.eclipse.jetty.websocket.server.JettyWebSocketServlet;
11import org.eclipse.jetty.websocket.server.JettyWebSocketServletFactory;
12import org.eclipse.xtext.resource.IResourceServiceProvider; 6import org.eclipse.xtext.resource.IResourceServiceProvider;
13import org.slf4j.Logger; 7import org.slf4j.Logger;
14import org.slf4j.LoggerFactory; 8import org.slf4j.LoggerFactory;
15 9
16import jakarta.servlet.ServletConfig; 10import java.io.IOException;
17import jakarta.servlet.ServletException; 11import java.io.Serial;
12import java.time.Duration;
13import java.util.Set;
18 14
19public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implements JettyWebSocketCreator { 15public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implements JettyWebSocketCreator {
20 16 @Serial
21 private static final long serialVersionUID = -3772740838165122685L; 17 private static final long serialVersionUID = -3772740838165122685L;
22 18
23 public static final String ALLOWED_ORIGINS_SEPARATOR = ";"; 19 public static final String ALLOWED_ORIGINS_SEPARATOR = ",";
24 20
25 public static final String ALLOWED_ORIGINS_INIT_PARAM = "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin"; 21 public static final String ALLOWED_ORIGINS_INIT_PARAM =
22 "tools.refinery.language.web.xtext.XtextWebSocketServlet.allowedOrigin";
26 23
27 public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1"; 24 public static final String XTEXT_SUBPROTOCOL_V1 = "tools.refinery.language.web.xtext.v1";
28 25
@@ -33,7 +30,7 @@ public abstract class XtextWebSocketServlet extends JettyWebSocketServlet implem
33 30
34 private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30); 31 private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30);
35 32
36 private transient Logger log = LoggerFactory.getLogger(getClass()); 33 private final transient Logger log = LoggerFactory.getLogger(getClass());
37 34
38 private transient Set<String> allowedOrigins = null; 35 private transient Set<String> allowedOrigins = null;
39 36