• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jhannes / logevents / #114

07 Aug 2024 11:00PM UTC coverage: 88.672% (-0.03%) from 88.704%
#114

push

jhannes
Regenerate JavaDoc

5691 of 6418 relevant lines covered (88.67%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.21
/logevents/src/main/java/org/logevents/observers/web/LogEventHttpServer.java
1
package org.logevents.observers.web;
2

3
import com.sun.net.httpserver.HttpExchange;
4
import com.sun.net.httpserver.HttpServer;
5
import com.sun.net.httpserver.HttpsExchange;
6
import com.sun.net.httpserver.HttpsServer;
7
import org.logevents.observers.LogEventSource;
8
import org.logevents.observers.WebLogEventObserver;
9
import org.logevents.query.LogEventQuery;
10
import org.logevents.query.LogEventQueryResult;
11
import org.logevents.status.LogEventStatus;
12
import org.logevents.util.JsonParser;
13
import org.logevents.util.JsonUtil;
14
import org.logevents.util.openid.OpenIdConfiguration;
15
import org.slf4j.Logger;
16
import org.slf4j.LoggerFactory;
17
import org.slf4j.Marker;
18
import org.slf4j.MarkerFactory;
19

20
import java.io.IOException;
21
import java.net.InetAddress;
22
import java.net.InetSocketAddress;
23
import java.time.Instant;
24
import java.util.Collections;
25
import java.util.HashMap;
26
import java.util.LinkedHashMap;
27
import java.util.Map;
28
import java.util.Optional;
29

30
// TODO state
31
/**
32
 * The simplest way to expose your logs in your web browser, LogEventHttpServer runs an embedded http server
33
 * in your application for the logs app console. Requires that you configure an {@link OpenIdConfiguration}
34
 * so you don't accidentally expose it insecurely. See {@link OpenIdConfiguration} for details on how to
35
 * set this up.
36
 *
37
 * <h2>Running with http (unencrypted)</h2>
38
 *
39
 * The simplest way to get {@link LogEventHttpServer} up a running is to use <code>http</code>. However,
40
 * this will probably only work for localhost, because OpenID Connect providers generally only allow
41
 * apps to use http for localhost.
42
 *
43
 * <ol>
44
 *     <li><code>observer.web=WebLogEventServer</code></li>
45
 *     <li>Register a {@link WebLogEventObserver} with an <code>httpPort</code>.
46
 *     (E.g. <code>observer.web.httpPort=8080</code>)
47
 *     </li>
48
 *     <li>Setup a {@link OpenIdConfiguration} (e.g. <code>observer.web.openIdIssuer=https://login.microsoft.com</code>,
49
 *     <code>observer.web.clientId=...</code>, <code>observer.web.clientSecret=...</code>)
50
 *     </li>
51
 *     <li>Start your application with the configuration</li>
52
 *     <li>Open a web browser to e.g. <code>http://localhost:8080/logs</code>. You will now be logged in
53
 *     with your Open ID Connect provider and the see your logs</li>
54
 * </ol>
55
 *
56
 * This allows you to access you log from localhost. If you want remote access, you should put your
57
 * application behind a https reverse proxy.
58
 *
59
 * <h2>Sample config</h2>
60
 *
61
 * <pre>
62
 * observer.web=WebLogEventObserver
63
 * observer.web.openIdIssuer=https://login.microsoftonline.com/common
64
 * observer.web.clientId=12345678-abcd-pqrs-9876-9abcdef01234
65
 * observer.web.clientSecret=3¤..¤!?qwer
66
 * observer.web.source=DatabaseLogEventObserver
67
 * observer.web.source.jdbcUrl=...
68
 * observer.web.httpsPort=8443
69
 * observer.web.keyStore=MyCertificate.p12
70
 * observer.web.keyStorePassword=mfldnlsnaa
71
 * observer.web.hostKeyPassword=2112wfsasa
72
 * </pre>
73
 *
74
 */
75
public class LogEventHttpServer extends AbstractLogEventHttpServer {
1✔
76

77
    private static final Logger logger = LoggerFactory.getLogger(LogEventHttpServer.class);
1✔
78
    private static final Marker AUDIT = MarkerFactory.getMarker("AUDIT");
1✔
79

80
    private String hostname = null;
1✔
81
    private Optional<Integer> httpPort = Optional.empty();
1✔
82
    private HttpServer httpServer;
83
    private String logEventsHtml = "/org/logevents/logevents.html";
1✔
84
    private OpenIdConfiguration openIdConfiguration;
85
    private LogEventSource logEventSource;
86
    private CryptoVault cookieVault;
87

88
    public void setHostname(String hostname) {
89
        this.hostname = hostname;
1✔
90
    }
1✔
91

92
    public void setHttpPort(Optional<Integer> httpPort) {
93
        this.httpPort = httpPort;
1✔
94
    }
1✔
95

96
    public void setLogEventsHtml(String logEventsHtml) {
97
        this.logEventsHtml = logEventsHtml;
1✔
98
    }
1✔
99

100
    public void setOpenIdConfiguration(OpenIdConfiguration openIdConfiguration) {
101
        this.openIdConfiguration = openIdConfiguration;
1✔
102
    }
1✔
103

104
    public void setLogEventSource(LogEventSource logEventSource) {
105
        this.logEventSource = logEventSource;
1✔
106
    }
1✔
107

108
    public void start() {
109
        LogEventStatus.getInstance().addConfig(this, "Starting server on port " + httpPort);
1✔
110
        try {
111
            if (hostname == null) {
1✔
112
                hostname = InetAddress.getLocalHost().getHostName();
1✔
113
            }
114
            if (httpPort.isPresent()) {
1✔
115
                this.httpServer = HttpServer.create(new InetSocketAddress(hostname, httpPort.get()), 0);
1✔
116
            } else {
117
                LogEventStatus.getInstance().addError(this, "httpPort or httpsPort must be configured", null);
×
118
                return;
×
119
            }
120
            LogEventStatus.getInstance().addConfig(this, "Started on " + getUrl());
1✔
121

122
            this.httpServer.createContext("/", this::httpHandler);
1✔
123
            this.httpServer.start();
1✔
124
        } catch (IOException e) {
×
125
            LogEventStatus.getInstance().addError(this, "Failed to start server", e);
×
126
        }
1✔
127
    }
1✔
128

129
    public String getUrl() {
130
        String scheme = httpServer instanceof HttpsServer ? "https" : "http";
1✔
131
        return scheme + "://" + hostname + ":" + httpServer.getAddress().getPort() + "/logs";
1✔
132
    }
133

134
    protected void httpHandler(HttpExchange exchange) throws IOException {
135
        try {
136
            String path = exchange.getRequestURI().getPath();
1✔
137
            if (path.equals("/logs")) {
1✔
138
                exchange.getResponseHeaders().add("Location", getAuthority(exchange) + "/logs/");
1✔
139
                exchange.sendResponseHeaders(302, 0);
1✔
140
            } else if (path.equals("/logs/")) {
1✔
141
                serveResource(exchange, logEventsHtml, "text/html");
1✔
142
            } else if (path.matches("/logs/[a-zA-Z._-]+\\.css")) {
1✔
143
                serveResource(exchange, "/org/logevents" + path.substring("/logs".length()), "text/css");
1✔
144
            } else if (path.matches("/logs/[a-zA-Z._-]+\\.js")) {
1✔
145
                serveResource(exchange, "/org/logevents" + path.substring("/logs".length()), "text/javascript");
1✔
146
            } else if (path.equals("/logs/openapi.json")) {
1✔
147
                Map<String, Object> api = JsonParser.parseObject(getResourceFileAsString("/org/logevents/openapi.json"));
1✔
148
                HashMap<Object, Object> localServer = new HashMap<>();
1✔
149
                localServer.put("url", getAuthority(exchange) + "/logs");
1✔
150
                api.put("servers", Collections.singletonList(localServer));
1✔
151
                exchange.getResponseHeaders().add("Content-type", "application/json");
1✔
152
                sendResponse(exchange, JsonUtil.toIndentedJson(api), 200);
1✔
153
            } else if (path.equals("/logs/login")) {
1✔
154
                String state = OpenIdConfiguration.randomString(50);
×
155
                exchange.getResponseHeaders().add("Location", openIdConfiguration.getAuthorizationUrl(
×
156
                        state, getAuthority(exchange) + "/logs/oauth2callback"
×
157
                ));
158
                exchange.getResponseHeaders().set("Set-Cookie",
×
159
                        "logevents.query=" + exchange.getRequestURI().getRawQuery() + ";Max-Age: 300"
×
160
                        +", logevents.login.state=" + state + ";Max-Age; 300");
161
                exchange.sendResponseHeaders(302, 0);
×
162
            } else if (path.equals("/logs/oauth2callback")) {
1✔
163
                Map<String, String[]> parameters = parseParameters(exchange.getRequestURI().getQuery());
1✔
164
                Map<String, Object> idToken = openIdConfiguration.fetchIdToken(
1✔
165
                        parameters.get("code")[0],getAuthority(exchange) + "/logs/oauth2callback"
1✔
166
                );
167
                if (!openIdConfiguration.isAuthorizedToken(idToken)) {
1✔
168
                    logger.warn(AUDIT, "Unknown user tried to log in {}", idToken);
1✔
169
                    exchange.sendResponseHeaders(403, 0);
1✔
170
                    return;
1✔
171
                }
172

173
                logger.warn(AUDIT, "User logged in {}", idToken);
1✔
174
                LogEventStatus.getInstance().addConfig(this, "User logged in " + idToken);
1✔
175

176
                exchange.getResponseHeaders().set("Set-Cookie", createSessionCookie(idToken));
1✔
177
                exchange.getResponseHeaders().add("Location", getAuthority(exchange) + "/logs");
1✔
178
                exchange.sendResponseHeaders(302, 0);
1✔
179
            } else if (!isAuthenticated(exchange)) {
1✔
180
                sendResponse(exchange, "Please log in", 401);
1✔
181
            } else if (path.equals("/logs/events")) {
1✔
182
                LogEventQuery query = new LogEventQuery(parseParameters(exchange.getRequestURI().getQuery()));
1✔
183
                LogEventQueryResult queryResult = logEventSource.query(query);
1✔
184

185
                Map<String, Object> result = new LinkedHashMap<>();
1✔
186
                result.put("facets", queryResult.getSummary().toJson());
1✔
187
                result.put("events", queryResult.getEventsAsJson());
1✔
188

189
                exchange.getResponseHeaders().add("Content-type", "application/json");
1✔
190
                sendResponse(exchange, JsonUtil.toIndentedJson(result), 200);
1✔
191
            } else {
1✔
192
                sendResponse(exchange, "Unknown file", 404);
×
193
            }
194
        } catch (Exception e) {
×
195
            logger.error("While processing {}", exchange, e);
×
196
            sendResponse(exchange, e.toString(), 500);
×
197
        }
1✔
198
    }
1✔
199

200
    private void serveResource(HttpExchange exchange, String logEventsHtml, String s) throws IOException {
201
        String text = getResourceFileAsString(logEventsHtml);
1✔
202
        if (text != null) {
1✔
203
            exchange.getResponseHeaders().add("Content-type", s);
1✔
204
            sendResponse(exchange, text, 200);
1✔
205
        } else {
206
            sendResponse(exchange, "File not found", 404);
1✔
207
        }
208
    }
1✔
209

210
    private String getAuthority(HttpExchange exchange) {
211
        String scheme = exchange instanceof HttpsExchange ? "https" : "http";
1✔
212
        return scheme + "://" + exchange.getRequestHeaders().getFirst("Host");
1✔
213
    }
214

215
    public String createSessionCookie(Map<String, Object> idToken) {
216
        Map<String, Object> session = new HashMap<>();
1✔
217
        session.put("subject", idToken.get("sub"));
1✔
218
        Instant sessionTime = Instant.ofEpochSecond(Long.parseLong(idToken.get("iat").toString()));
1✔
219
        session.put("sessionTime", sessionTime.toString());
1✔
220
        return "logevents.session=" + cookieVault.encrypt(JsonUtil.toIndentedJson(session));
1✔
221
    }
222

223
    private boolean isAuthenticated(HttpExchange exchange) {
224
        Optional<String> sessionCookie = getCookie(exchange, "logevents.session");
1✔
225
        if (!sessionCookie.isPresent()) {
1✔
226
            return false;
1✔
227
        }
228
        try {
229
            Map<String, Object> session = JsonParser.parseObject(cookieVault.decrypt(sessionCookie.get()));
1✔
230
            if (session.containsKey("sessionTime")) {
1✔
231
                Instant sessionTime = Instant.parse(session.get("sessionTime").toString());
1✔
232
                if (Instant.now().isBefore(sessionTime.plusSeconds(60*60))) {
1✔
233
                    return true;
1✔
234
                }
235
            }
236
        } catch (Exception e) {
1✔
237
            LogEventStatus.getInstance().addConfig(this, "Failed to decode session cookie");
1✔
238
        }
×
239
        exchange.getResponseHeaders().set("Set-Cookie", "logevents.session=; max-age=-1");
1✔
240
        return false;
1✔
241
    }
242

243
    @Override
244
    public String toString() {
245
        return getClass().getSimpleName() + "{" +
×
246
                "hostname='" + hostname + '\'' +
247
                ", httpPort=" + httpPort +
248
                '}';
249
    }
250

251
    public CryptoVault getCookieVault() {
252
        return cookieVault;
1✔
253
    }
254

255
    public void setCookieVault(CryptoVault cookieVault) {
256
        this.cookieVault = cookieVault;
1✔
257
    }
1✔
258

259
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc