aboutsummaryrefslogtreecommitdiffstats
path: root/language-web/src/main/java/tools/refinery/language/web/ServerLauncher.java
blob: a71d8e931ba292bf8f54c51a3a09361063bef532 (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
/*
 * generated by Xtext 2.25.0
 */
package tools.refinery.language.web;

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

import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.session.SessionHandler;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jakarta.servlet.DispatcherType;
import jakarta.servlet.SessionTrackingMode;
import tools.refinery.language.web.xtext.servlet.XtextWebSocketServlet;

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, Resource baseResource, Optional<String[]> allowedOrigins) {
		server = new Server(bindAddress);
		var handler = new ServletContextHandler();
		addSessionHandler(handler);
		addProblemServlet(handler, allowedOrigins);
		if (baseResource != null) {
			handler.setBaseResource(baseResource);
			handler.setWelcomeFiles(new String[] { "index.html" });
			addDefaultServlet(handler);
		}
		handler.addFilter(CacheControlFilter.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, Optional<String[]> allowedOrigins) {
		var problemServletHolder = new ServletHolder(ProblemWebSocketServlet.class);
		if (allowedOrigins.isEmpty()) {
			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.get());
			problemServletHolder.setInitParameter(XtextWebSocketServlet.ALLOWED_ORIGINS_INIT_PARAM,
					allowedOriginsString);
		}
		handler.addServlet(problemServletHolder, "/xtext-service/*");
	}

	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, "/");
	}

	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 baseResource = getBaseResource();
			var allowedOrigins = getAllowedOrigins();
			var serverLauncher = new ServerLauncher(bindAddress, baseResource, allowedOrigins);
			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 Resource getBaseResource() throws IOException, URISyntaxException {
		var baseResourceOverride = System.getenv("BASE_RESOURCE");
		if (baseResourceOverride != null) {
			// If a user override is provided, use it.
			return Resource.newResource(baseResourceOverride);
		}
		var indexUrlInJar = ServerLauncher.class.getResource("/webapp/index.html");
		if (indexUrlInJar != null) {
			// If the app is packaged in the jar, serve it.
			var webRootUri = URI.create(indexUrlInJar.toURI().toASCIIString().replaceFirst("/index.html$", "/"));
			return Resource.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 Resource.newResource(unpackedResourceDir);
		}
		// Fall back to just serving a 404.
		return null;
	}

	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_LISTEN_PORT;
	}

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

	private static Optional<String[]> getAllowedOriginsFromPublicHostAndPort() {
		var publicHost = getPublicHost();
		if (publicHost == null) {
			return Optional.empty();
		}
		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 Optional.of(new String[] { urlWithPort, urlWithoutPort });
		}
		return Optional.of(new String[] { urlWithPort });
	}
}