HttpServer.java 7.72 KB
Newer Older
1
2
3
4
package emu.grasscutter.server.http;

import emu.grasscutter.Grasscutter;
import emu.grasscutter.Grasscutter.ServerDebugMode;
KingRainbow44's avatar
KingRainbow44 committed
5
import emu.grasscutter.utils.FileUtils;
6
import express.Express;
KingRainbow44's avatar
KingRainbow44 committed
7
import express.http.MediaType;
8
9
10
11
12
13
import io.javalin.Javalin;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.ssl.SslContextFactory;

import java.io.File;
14
import java.io.UnsupportedEncodingException;
15

16
import static emu.grasscutter.config.Configuration.*;
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import static emu.grasscutter.utils.Language.translate;

/**
 * Manages all HTTP-related classes.
 * (including dispatch, announcements, gacha, etc.)
 */
public final class HttpServer {
    private final Express express;

    /**
     * Configures the Express application.
     */
    public HttpServer() {
        this.express = new Express(config -> {
            // Set the Express HTTP server.
            config.server(HttpServer::createServer);
github-actions's avatar
github-actions committed
33

34
35
            // Configure encryption/HTTPS/SSL.
            config.enforceSsl = HTTP_ENCRYPTION.useEncryption;
github-actions's avatar
github-actions committed
36

37
            // Configure HTTP policies.
github-actions's avatar
github-actions committed
38
            if (HTTP_POLICIES.cors.enabled) {
39
40
41
42
43
                var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins;
                if (allowedOrigins.length > 0)
                    config.enableCorsForOrigin(allowedOrigins);
                else config.enableCorsForAllOrigins();
            }
github-actions's avatar
github-actions committed
44

45
            // Configure debug logging.
github-actions's avatar
github-actions committed
46
            if (DISPATCH_INFO.logRequests == ServerDebugMode.ALL)
47
                config.enableDevLogging();
github-actions's avatar
github-actions committed
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
            // Disable compression on static files.
            config.precompressStaticFiles = false;
        });
    }

    /**
     * Creates an HTTP(S) server.
     * @return A server instance.
     */
    @SuppressWarnings("resource")
    private static Server createServer() {
        Server server = new Server();
        ServerConnector serverConnector
                = new ServerConnector(server);
github-actions's avatar
github-actions committed
63
64

        if (HTTP_ENCRYPTION.useEncryption) {
65
66
            var sslContextFactory = new SslContextFactory.Server();
            var keystoreFile = new File(HTTP_ENCRYPTION.keystore);
github-actions's avatar
github-actions committed
67
68

            if (!keystoreFile.exists()) {
69
70
                HTTP_ENCRYPTION.useEncryption = false;
                HTTP_ENCRYPTION.useInRouting = false;
github-actions's avatar
github-actions committed
71

72
73
74
75
76
77
                Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.no_keystore_error"));
            } else try {
                sslContextFactory.setKeyStorePath(keystoreFile.getPath());
                sslContextFactory.setKeyStorePassword(HTTP_ENCRYPTION.keystorePassword);
            } catch (Exception ignored) {
                Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.password_error"));
github-actions's avatar
github-actions committed
78

79
80
81
                try {
                    sslContextFactory.setKeyStorePath(keystoreFile.getPath());
                    sslContextFactory.setKeyStorePassword("123456");
github-actions's avatar
github-actions committed
82

83
84
85
86
87
88
89
90
                    Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.default_password"));
                } catch (Exception exception) {
                    Grasscutter.getLogger().warn(translate("messages.dispatch.keystore.general_error"), exception);
                }
            } finally {
                serverConnector = new ServerConnector(server, sslContextFactory);
            }
        }
github-actions's avatar
github-actions committed
91

92
93
        serverConnector.setPort(HTTP_INFO.bindPort);
        server.setConnectors(new ServerConnector[]{serverConnector});
github-actions's avatar
github-actions committed
94

95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
        return server;
    }

    /**
     * Returns the handle for the Express application.
     * @return A Javalin instance.
     */
    public Javalin getHandle() {
        return this.express.raw();
    }

    /**
     * Initializes the provided class.
     * @param router The router class.
     * @return Method chaining.
     */
    @SuppressWarnings("UnusedReturnValue")
    public HttpServer addRouter(Class<? extends Router> router, Object... args) {
        // Get all constructor parameters.
        Class<?>[] types = new Class<?>[args.length];
github-actions's avatar
github-actions committed
115
        for (var argument : args)
116
            types[args.length - 1] = argument.getClass();
github-actions's avatar
github-actions committed
117

118
119
120
121
122
123
124
125
126
127
128
        try { // Create a router instance & apply routes.
            var constructor = router.getDeclaredConstructor(types); // Get the constructor.
            var routerInstance = constructor.newInstance(args); // Create instance.
            routerInstance.applyRoutes(this.express, this.getHandle()); // Apply routes.
        } catch (Exception exception) {
            Grasscutter.getLogger().warn(translate("messages.dispatch.router_error"), exception);
        } return this;
    }

    /**
     * Starts listening on the HTTP server.
129
     * @throws UnsupportedEncodingException
130
     */
131
    public void start() throws UnsupportedEncodingException {
132
        // Attempt to start the HTTP server.
github-actions's avatar
github-actions committed
133
        if (HTTP_INFO.bindAddress.equals("")) {
134
            this.express.listen(HTTP_INFO.bindPort);
github-actions's avatar
github-actions committed
135
        }else {
136
137
            this.express.listen(HTTP_INFO.bindAddress, HTTP_INFO.bindPort);
        }
138

139
140
141
142
143
144
145
146
147
        // Log bind information.
        Grasscutter.getLogger().info(translate("messages.dispatch.port_bind", Integer.toString(this.express.raw().port())));
    }

    /**
     * Handles the '/' (index) endpoint on the Express application.
     */
    public static class DefaultRequestRouter implements Router {
        @Override public void applyRoutes(Express express, Javalin handle) {
KingRainbow44's avatar
KingRainbow44 committed
148
            express.get("/", (request, response) -> {
6ixfalls's avatar
6ixfalls committed
149
                File file = new File(HTTP_STATIC_FILES.indexFile);
github-actions's avatar
github-actions committed
150
                if (!file.exists())
KingRainbow44's avatar
KingRainbow44 committed
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
                    response.send("""
                            <!DOCTYPE html>
                            <html>
                                <head>
                                    <meta charset="utf8">
                                </head>
                                <body>%s</body>
                            </html>
                            """.formatted(translate("messages.status.welcome")));
                else {
                    final var filePath = file.getPath();
                    final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
                    response.type((fromExtension != null) ? fromExtension.getMIME() : "text/plain")
                            .send(FileUtils.read(filePath));
                }
            });
167
168
169
170
171
172
173
174
175
        }
    }

    /**
     * Handles unhandled endpoints on the Express application.
     */
    public static class UnhandledRequestRouter implements Router {
        @Override public void applyRoutes(Express express, Javalin handle) {
            handle.error(404, context -> {
github-actions's avatar
github-actions committed
176
                if (DISPATCH_INFO.logRequests == ServerDebugMode.MISSING)
177
178
                    Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url()));
                context.contentType("text/html");
github-actions's avatar
github-actions committed
179

KingRainbow44's avatar
KingRainbow44 committed
180
                File file = new File(HTTP_STATIC_FILES.errorFile);
github-actions's avatar
github-actions committed
181
                if (!file.exists())
KingRainbow44's avatar
KingRainbow44 committed
182
                    context.result("""
183
184
185
                        <!DOCTYPE html>
                        <html>
                            <head>
186
                                <meta charset="utf8">
187
                            </head>
github-actions's avatar
github-actions committed
188

189
                            <body>
190
                                <img src="https://http.cat/404" />
191
192
193
                            </body>
                        </html>
                        """);
KingRainbow44's avatar
KingRainbow44 committed
194
195
196
197
198
199
                else {
                    final var filePath = file.getPath();
                    final MediaType fromExtension = MediaType.getByExtension(filePath.substring(filePath.lastIndexOf(".") + 1));
                    context.contentType((fromExtension != null) ? fromExtension.getMIME() : "text/plain")
                            .result(FileUtils.read(filePath));
                }
200
201
202
203
            });
        }
    }
}