HttpServer.java 7.87 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
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

import static emu.grasscutter.Configuration.*;
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);
            
            // Configure encryption/HTTPS/SSL.
            config.enforceSsl = HTTP_ENCRYPTION.useEncryption;
            
            // Configure HTTP policies.
            if(HTTP_POLICIES.cors.enabled) {
                var allowedOrigins = HTTP_POLICIES.cors.allowedOrigins;
                if (allowedOrigins.length > 0)
                    config.enableCorsForOrigin(allowedOrigins);
                else config.enableCorsForAllOrigins();
            }
            
            // Configure debug logging.
            if(SERVER.debugLevel == ServerDebugMode.ALL)
                config.enableDevLogging();
            
            // 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);
        
        if(HTTP_ENCRYPTION.useEncryption) {
            var sslContextFactory = new SslContextFactory.Server();
            var keystoreFile = new File(HTTP_ENCRYPTION.keystore);
            
KingRainbow44's avatar
KingRainbow44 committed
68
            if(!keystoreFile.exists()) {
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
                HTTP_ENCRYPTION.useEncryption = false;
                HTTP_ENCRYPTION.useInRouting = false;
                
                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"));
                
                try {
                    sslContextFactory.setKeyStorePath(keystoreFile.getPath());
                    sslContextFactory.setKeyStorePassword("123456");
                    
                    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);
            }
        }
        
        serverConnector.setPort(HTTP_INFO.bindPort);
        server.setConnectors(new ServerConnector[]{serverConnector});
        
        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];
        for(var argument : args)
            types[args.length - 1] = argument.getClass();
        
        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.
133
134
135
136
137
        if(HTTP_INFO.bindAddress.equals("")){
            this.express.listen(HTTP_INFO.bindPort);
        }else{
            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);
KingRainbow44's avatar
KingRainbow44 committed
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
                if(!file.exists())
                    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
176
177
178
        }
    }

    /**
     * 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 -> {
                if(SERVER.debugLevel == ServerDebugMode.MISSING)
                    Grasscutter.getLogger().info(translate("messages.dispatch.unhandled_request_error", context.method(), context.url()));
                context.contentType("text/html");
KingRainbow44's avatar
KingRainbow44 committed
179
180
181
182
                
                File file = new File(HTTP_STATIC_FILES.errorFile);
                if(!file.exists())
                    context.result("""
183
184
185
                        <!DOCTYPE html>
                        <html>
                            <head>
186
                                <meta charset="utf8">
187
188
189
                            </head>
                            
                            <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
            });
        }
    }
}