aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
blob: ad19e77d0eb7b12fc6fabe22477c39d09df6d64d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/*
 * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors <https://refinery.tools/>
 *
 * SPDX-License-Identifier: EPL-2.0
 */

/*
 * generated by Xtext 2.25.0
 */
package tools.refinery.language.web;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.SessionTrackingMode;
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.servlet.ServletHolder;
import org.eclipse.jetty.ee10.servlet.SessionHandler;
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.refinery.language.web.config.BackendConfigServlet;
import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;

import java.io.File;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.EnumSet;
import java.util.Set;

public class ServerLauncher {
	public static final String DEFAULT_LISTEN_ADDRESS = "localhost";

	public static final int DEFAULT_LISTEN_PORT = 1312;

	public static final int DEFAULT_PUBLIC_PORT = 443;

	public static final int HTTP_DEFAULT_PORT = 80;

	public static final int HTTPS_DEFAULT_PORT = 443;

	public static final String ALLOWED_ORIGINS_SEPARATOR = ",";

	private static final Logger LOG = LoggerFactory.getLogger(ServerLauncher.class);

	private final Server server;

	public ServerLauncher(InetSocketAddress bindAddress, String[] allowedOrigins, String webSocketUrl) {
		server = new Server(bindAddress);
		((QueuedThreadPool) server.getThreadPool()).setName("jetty");
		var handler = new ServletContextHandler();
		addSessionHandler(handler);
		addProblemServlet(handler, allowedOrigins);
		addBackendConfigServlet(handler, webSocketUrl);
		var baseResource = getBaseResource();
		if (baseResource != null) {
			handler.setBaseResource(baseResource);
			handler.setWelcomeFiles(new String[]{"index.html"});
			addDefaultServlet(handler);
		}
		handler.addFilter(CacheControlFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
		handler.addFilter(SecurityHeadersFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
		server.setHandler(handler);
	}

	private void addSessionHandler(ServletContextHandler handler) {
		var sessionHandler = new SessionHandler();
		sessionHandler.setSessionTrackingModes(Set.of(SessionTrackingMode.COOKIE));
		handler.setSessionHandler(sessionHandler);
	}

	private void addProblemServlet(ServletContextHandler handler, String[] allowedOrigins) {
		var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class);
		if (allowedOrigins == null) {
			LOG.warn("All WebSocket origins are allowed! This setting should not be used in production!");
		} else {
			var allowedOriginsString = String.join(XtextWebSocketServlet.ALLOWED_ORIGINS_SEPARATOR,
					allowedOrigins);
			problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM,
					allowedOriginsString);
		}
		handler.addServlet(problemServletHolder, "/xtext-service");
		JettyWebSocketServletContainerInitializer.configure(handler, null);
	}

	private void addBackendConfigServlet(ServletContextHandler handler, String webSocketUrl) {
		var backendConfigServletHolder = new ServletHolder(BackendConfigServlet.class);
		backendConfigServletHolder.setInitParameter(BackendConfigServlet.WEBSOCKET_URL_INIT_PARAM, webSocketUrl);
		handler.addServlet(backendConfigServletHolder, "/config.json");
	}

	private void addDefaultServlet(ServletContextHandler handler) {
		var defaultServletHolder = new ServletHolder(DefaultServlet.class);
		var isWindows = System.getProperty("os.name").toLowerCase().contains("win");
		// Avoid file locking on Windows: https://stackoverflow.com/a/4985717
		// See also the related Jetty ticket:
		// https://github.com/eclipse/jetty.project/issues/2925
		defaultServletHolder.setInitParameter("useFileMappedBuffer", isWindows ? "false" : "true");
		handler.addServlet(defaultServletHolder, "/");
	}

	private Resource getBaseResource() {
		var factory = ResourceFactory.of(server);
		var baseResourceOverride = System.getenv("BASE_RESOURCE");
		if (baseResourceOverride != null) {
			// If a user override is provided, use it.
			return factory.newResource(baseResourceOverride);
		}
		var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html");
		if (indexUrlInJar != null) {
			// If the app is packaged in the jar, serve it.
			URI webRootUri;
			try {
				webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/"));
			} catch (URISyntaxException e) {
				throw new IllegalStateException("Jar has invalid base resource URI", e);
			}
			return factory.newResource(webRootUri);
		}
		// Look for unpacked production artifacts (convenience for running from IDE).
		var unpackedResourcePathComponents = new String[]{System.getProperty("user.dir"), "build", "webpack",
				"production"};
		var unpackedResourceDir = new File(String.join(File.separator, unpackedResourcePathComponents));
		if (unpackedResourceDir.isDirectory()) {
			return factory.newResource(unpackedResourceDir.toPath());
		}
		// Fall back to just serving a 404.
		return null;
	}

	public void start() throws Exception {
		server.start();
		LOG.info("Server started on {}", server.getURI());
		server.join();
	}

	public static void main(String[] args) {
		try {
			var bindAddress = getBindAddress();
			var allowedOrigins = getAllowedOrigins();
			var webSocketUrl = getWebSocketUrl();
			var serverLauncher = new ServerLauncher(bindAddress, allowedOrigins, webSocketUrl);
			serverLauncher.start();
		} catch (Exception exception) {
			LOG.error("Fatal server error", exception);
			System.exit(1);
		}
	}

	private static String getListenAddress() {
		var listenAddress = System.getenv("LISTEN_ADDRESS");
		if (listenAddress == null) {
			return DEFAULT_LISTEN_ADDRESS;
		}
		return listenAddress;
	}

	private static int getListenPort() {
		var portStr = System.getenv("LISTEN_PORT");
		if (portStr != null) {
			return Integer.parseInt(portStr);
		}
		return DEFAULT_LISTEN_PORT;
	}

	private static InetSocketAddress getBindAddress() {
		var listenAddress = getListenAddress();
		var listenPort = getListenPort();
		return new InetSocketAddress(listenAddress, listenPort);
	}

	private static String getPublicHost() {
		var publicHost = System.getenv("PUBLIC_HOST");
		if (publicHost != null) {
			return publicHost.toLowerCase();
		}
		return null;
	}

	private static int getPublicPort() {
		var portStr = System.getenv("PUBLIC_PORT");
		if (portStr != null) {
			return Integer.parseInt(portStr);
		}
		return DEFAULT_PUBLIC_PORT;
	}

	private static String[] getAllowedOrigins() {
		var allowedOrigins = System.getenv("ALLOWED_ORIGINS");
		if (allowedOrigins != null) {
			return allowedOrigins.split(ALLOWED_ORIGINS_SEPARATOR);
		}
		return getAllowedOriginsFromPublicHostAndPort();
	}

	// This method returns <code>null</code> to indicate that all origins are allowed.
	@SuppressWarnings("squid:S1168")
	private static String[] getAllowedOriginsFromPublicHostAndPort() {
		var publicHost = getPublicHost();
		if (publicHost == null) {
			return null;
		}
		int publicPort = getPublicPort();
		var scheme = publicPort == HTTPS_DEFAULT_PORT ? "https" : "http";
		var urlWithPort = String.format("%s://%s:%d", scheme, publicHost, publicPort);
		if (publicPort == HTTPS_DEFAULT_PORT || publicPort == HTTP_DEFAULT_PORT) {
			var urlWithoutPort = String.format("%s://%s", scheme, publicHost);
			return new String[]{urlWithPort, urlWithoutPort};
		}
		return new String[]{urlWithPort};
	}

	private static String getWebSocketUrl() {
		String host;
		int port;
		var publicHost = getPublicHost();
		if (publicHost == null) {
			host = getListenAddress();
			port = getListenPort();
		} else {
			host = publicHost;
			port = getPublicPort();
		}
		var scheme = port == HTTPS_DEFAULT_PORT ? "wss" : "ws";
		return String.format("%s://%s:%d/xtext-service", scheme, host, port);
	}
}