aboutsummaryrefslogtreecommitdiffstats
path: root/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JettyConnectionLogger.java
blob: 7b045b7fb0899f6e82f7da817c4315470ca1dcaa (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
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.jdisc.http.server.jetty;

import com.yahoo.container.logging.ConnectionLog;
import com.yahoo.container.logging.ConnectionLogEntry;
import com.yahoo.container.logging.ConnectionLogEntry.SslHandshakeFailure.ExceptionEntry;
import com.yahoo.io.HexDump;
import com.yahoo.jdisc.http.ServerConfig;
import com.yahoo.security.SubjectAlternativeName;
import com.yahoo.security.X509CertificateUtils;
import org.eclipse.jetty.alpn.server.ALPNServerConnection;
import org.eclipse.jetty.http2.server.HTTP2ServerConnection;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.SocketChannelEndPoint;
import org.eclipse.jetty.io.ssl.SslConnection;
import org.eclipse.jetty.io.ssl.SslHandshakeListener;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.ProxyConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.util.component.AbstractLifeCycle;

import javax.net.ssl.ExtendedSSLSession;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.StandardConstants;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Jetty integration for jdisc connection log ({@link ConnectionLog}).
 *
 * @author bjorncs
 */
class JettyConnectionLogger extends AbstractLifeCycle implements Connection.Listener, HttpChannel.Listener, SslHandshakeListener {

    static final String CONNECTION_ID_REQUEST_ATTRIBUTE = "jdisc.request.connection.id";

    private static final Logger log = Logger.getLogger(JettyConnectionLogger.class.getName());

    private final SimpleConcurrentIdentityHashMap<SocketChannelEndPoint, ConnectionInfo> connectionInfos = new SimpleConcurrentIdentityHashMap<>();
    private final SimpleConcurrentIdentityHashMap<SocketChannelEndPoint, SSLEngine> sslEngines = new SimpleConcurrentIdentityHashMap<>();
    // Extra mapping as callbacks in SslHandshakeListener only provides SSLEngine (no connection reference) as argument
    private final SimpleConcurrentIdentityHashMap<SSLEngine, ConnectionInfo> sslToConnectionInfo = new SimpleConcurrentIdentityHashMap<>();

    private final boolean enabled;
    private final ConnectionLog connectionLog;

    JettyConnectionLogger(ServerConfig.ConnectionLog config, ConnectionLog connectionLog) {
        this.enabled = config.enabled();
        this.connectionLog = connectionLog;
        log.log(Level.FINE, () -> "Jetty connection logger is " + (config.enabled() ? "enabled" : "disabled"));
    }

    //
    // AbstractLifeCycle methods start
    //
    @Override
    protected void doStop() {
        handleListenerInvocation("AbstractLifeCycle", "doStop", "", List.of(), () -> {
            log.log(Level.FINE, () -> "Jetty connection logger is stopped");
        });
    }

    @Override
    protected void doStart() {
        handleListenerInvocation("AbstractLifeCycle", "doStart", "", List.of(), () -> {
            log.log(Level.FINE, () -> "Jetty connection logger is started");
        });
    }
    //
    // AbstractLifeCycle methods stop
    //

    //
    // Connection.Listener methods start
    //
    @Override
    public void onOpened(Connection connection) {
        handleListenerInvocation("Connection.Listener", "onOpened", "%h", List.of(connection), () -> {
            SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint());
            ConnectionInfo info = connectionInfos.computeIfAbsent(endpoint, ConnectionInfo::from);
            String connectionClassName = connection.getClass().getSimpleName(); // For hidden implementations of Connection
            if (connection instanceof SslConnection) {
                SSLEngine sslEngine = ((SslConnection) connection).getSSLEngine();
                addReferenceToSslEngine(endpoint, info, sslEngine);
            } else if (connection instanceof ALPNServerConnection) {
                SSLEngine sslEngine = ((ALPNServerConnection) connection).getSSLEngine();
                addReferenceToSslEngine(endpoint, info, sslEngine);
            } else if (connection instanceof HttpConnection) {
                info.setHttpProtocol("HTTP/1.1");
            } else if (connection instanceof HTTP2ServerConnection) {
                info.setHttpProtocol("HTTP/2.0");
            } else if (connectionClassName.endsWith("ProxyProtocolV1Connection")) {
                info.setProxyProtocolVersion("v1");
            } else if (connectionClassName.endsWith("ProxyProtocolV2Connection")) {
                info.setProxyProtocolVersion("v2");
            }
            if (connection.getEndPoint() instanceof ProxyConnectionFactory.ProxyEndPoint) {
                var remoteAddress = connection.getEndPoint().getRemoteSocketAddress();
                info.setRemoteAddress(remoteAddress);
            }
        });
    }

    private void addReferenceToSslEngine(SocketChannelEndPoint endpoint, ConnectionInfo info, SSLEngine sslEngine) {
        if (sslEngine != null) {
            sslEngines.put(endpoint, sslEngine)
                    .ifPresent(sslToConnectionInfo::remove);
            sslToConnectionInfo.put(sslEngine, info);
        }
    }

    @Override
    public void onClosed(Connection connection) {
        handleListenerInvocation("Connection.Listener", "onClosed", "%h", List.of(connection), () -> {
            SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(connection.getEndPoint());
            ConnectionInfo info = connectionInfos.get(endpoint).orElse(null);
            if (info == null) return; // Closed connection already handled
            if (connection instanceof HttpConnection) {
                info.setHttpBytes(connection.getBytesIn(), connection.getBytesOut());
            }
            if (connection.getEndPoint() instanceof SslConnection.DecryptedEndPoint ssl) {
                info.setSslBytes(ssl.getSslConnection().getBytesIn(), ssl.getSslConnection().getBytesOut());
            }
            if (!endpoint.isOpen()) {
                info.setClosedAt(System.currentTimeMillis());
                connectionLog.log(info.toLogEntry());
                connectionInfos.remove(endpoint);
                sslEngines.remove(endpoint)
                        .ifPresent(sslToConnectionInfo::remove);
            }
        });
    }
    //
    // Connection.Listener methods end
    //

    //
    // HttpChannel.Listener methods start
    //
    @Override
    public void onRequestBegin(Request request) {
        handleListenerInvocation("HttpChannel.Listener", "onRequestBegin", "%h", List.of(request), () -> {
            SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint());
            ConnectionInfo info = connectionInfos.get(endpoint).get();
            info.incrementRequests();
            request.setAttribute(CONNECTION_ID_REQUEST_ATTRIBUTE, info.uuid());
        });
    }

    @Override
    public void onResponseBegin(Request request) {
        handleListenerInvocation("HttpChannel.Listener", "onResponseBegin", "%h", List.of(request), () -> {
            SocketChannelEndPoint endpoint = findUnderlyingSocketEndpoint(request.getHttpChannel().getEndPoint());
            ConnectionInfo info = connectionInfos.get(endpoint).orElse(null);
            if (info == null) return; // Connection closed before response started - observed during Jetty server shutdown
            info.incrementResponses();
        });
    }
    //
    // HttpChannel.Listener methods end
    //

    //
    // SslHandshakeListener methods start
    //
    @Override
    public void handshakeSucceeded(Event event) {
        SSLEngine sslEngine = event.getSSLEngine();
        handleListenerInvocation("SslHandshakeListener", "handshakeSucceeded", "sslEngine=%h", List.of(sslEngine), () -> {
            ConnectionInfo info = sslToConnectionInfo.get(sslEngine).orElse(null);
            if (info == null) return;
            info.setSslSessionDetails(sslEngine.getSession());
        });
    }

    @Override
    public void handshakeFailed(Event event, Throwable failure) {
        SSLEngine sslEngine = event.getSSLEngine();
        handleListenerInvocation("SslHandshakeListener", "handshakeFailed", "sslEngine=%h,failure=%s", List.of(sslEngine, failure), () -> {
            log.log(Level.FINE, failure, failure::toString);
            ConnectionInfo info = sslToConnectionInfo.get(sslEngine).orElse(null);
            if (info == null) return;
            info.setSslHandshakeFailure((SSLHandshakeException)failure);
        });
    }
    //
    // SslHandshakeListener methods end
    //

    private void handleListenerInvocation(
            String listenerType, String methodName, String methodArgumentsFormat, List<Object> methodArguments, ListenerHandler handler) {
        if (!enabled) return;
        try {
            log.log(Level.FINE, () -> String.format(listenerType + "." + methodName + "(" + methodArgumentsFormat + ")", methodArguments.toArray()));
            handler.run();
        } catch (Exception e) {
            log.log(Level.WARNING, String.format("Exception in %s.%s listener: %s", listenerType, methodName, e.getMessage()), e);
        }
    }

    /**
     * Protocol layers are connected through each {@link Connection}'s {@link EndPoint} reference.
     * This methods iterates through the endpoints recursively to find the underlying socket endpoint.
     */
    private static SocketChannelEndPoint findUnderlyingSocketEndpoint(EndPoint endpoint) {
        if (endpoint instanceof SocketChannelEndPoint) {
            return (SocketChannelEndPoint) endpoint;
        } else if (endpoint instanceof SslConnection.DecryptedEndPoint) {
            var decryptedEndpoint = (SslConnection.DecryptedEndPoint) endpoint;
            return findUnderlyingSocketEndpoint(decryptedEndpoint.getSslConnection().getEndPoint());
        } else if (endpoint instanceof ProxyConnectionFactory.ProxyEndPoint) {
            var proxyEndpoint = (ProxyConnectionFactory.ProxyEndPoint) endpoint;
            return findUnderlyingSocketEndpoint(proxyEndpoint.unwrap());
        } else {
            throw new IllegalArgumentException("Unknown connection endpoint type: " + endpoint.getClass().getName());
        }
    }
    @FunctionalInterface private interface ListenerHandler { void run() throws Exception; }

    private static class ConnectionInfo {
        private final UUID uuid;
        private final long createdAt;
        private final InetSocketAddress localAddress;
        private final InetSocketAddress peerAddress;

        private long closedAt = 0;
        private long httpBytesReceived = 0;
        private long httpBytesSent = 0;
        private long requests = 0;
        private long responses = 0;
        private SocketAddress remoteAddress;
        private byte[] sslSessionId;
        private String sslProtocol;
        private String sslCipherSuite;
        private String sslPeerSubject;
        private Date sslPeerNotBefore;
        private Date sslPeerNotAfter;
        private List<SNIServerName> sslSniServerNames;
        private String sslPeerIssuerSubject;
        private byte[] sslPeerEncodedCertificate;
        private SSLHandshakeException sslHandshakeException;
        private List<String> sslSubjectAlternativeNames;
        private String proxyProtocolVersion;
        private String httpProtocol;
        private long sslBytesReceived = 0;
        private long sslBytesSent = 0;

        private ConnectionInfo(UUID uuid, long createdAt, InetSocketAddress localAddress, InetSocketAddress peerAddress) {
            this.uuid = uuid;
            this.createdAt = createdAt;
            this.localAddress = localAddress;
            this.peerAddress = peerAddress;
        }

        static ConnectionInfo from(SocketChannelEndPoint endpoint) {
            return new ConnectionInfo(
                    UUID.randomUUID(),
                    endpoint.getCreatedTimeStamp(),
                    endpoint.getLocalAddress(),
                    endpoint.getRemoteAddress());
        }

        synchronized UUID uuid() { return uuid; }

        synchronized ConnectionInfo setClosedAt(long closedAt) {
            this.closedAt = closedAt;
            return this;
        }

        synchronized ConnectionInfo setHttpBytes(long received, long sent) {
            this.httpBytesReceived = received;
            this.httpBytesSent = sent;
            return this;
        }

        synchronized ConnectionInfo incrementRequests() { ++this.requests; return this; }

        synchronized ConnectionInfo incrementResponses() { ++this.responses; return this; }

        synchronized ConnectionInfo setRemoteAddress(SocketAddress remoteAddress) {
            this.remoteAddress = remoteAddress;
            return this;
        }

        synchronized ConnectionInfo setSslSessionDetails(SSLSession session) {
            this.sslCipherSuite = session.getCipherSuite();
            this.sslProtocol = session.getProtocol();
            this.sslSessionId = session.getId();
            if (session instanceof ExtendedSSLSession) {
                ExtendedSSLSession extendedSession = (ExtendedSSLSession) session;
                this.sslSniServerNames = extendedSession.getRequestedServerNames();
            }
            try {
                this.sslPeerSubject = session.getPeerPrincipal().getName();
                X509Certificate peerCertificate = (X509Certificate) session.getPeerCertificates()[0];
                this.sslPeerNotBefore = peerCertificate.getNotBefore();
                this.sslPeerNotAfter = peerCertificate.getNotAfter();
                this.sslSubjectAlternativeNames = X509CertificateUtils.getSubjectAlternativeNames(peerCertificate).stream()
                        .map(SubjectAlternativeName::getValue)
                        .toList();
                this.sslPeerIssuerSubject = peerCertificate.getIssuerX500Principal().getName();
                this.sslPeerEncodedCertificate = peerCertificate.getEncoded();
            } catch (SSLPeerUnverifiedException | CertificateEncodingException e) {
                // Throw if peer is not authenticated (e.g when client auth is disabled)
                // JSSE provides no means of checking for client authentication without catching this exception
            }
            return this;
        }

        synchronized ConnectionInfo setSslHandshakeFailure(SSLHandshakeException exception) {
            this.sslHandshakeException = exception;
            return this;
        }

        synchronized ConnectionInfo setHttpProtocol(String protocol) { this.httpProtocol = protocol; return this; }

        synchronized ConnectionInfo setProxyProtocolVersion(String version) { this.proxyProtocolVersion = version; return this; }

        synchronized ConnectionInfo setSslBytes(long received, long sent) {
            this.sslBytesReceived = received;
            this.sslBytesSent = sent;
            return this;
        }

        synchronized ConnectionLogEntry toLogEntry() {
            ConnectionLogEntry.Builder builder = ConnectionLogEntry.builder(uuid, Instant.ofEpochMilli(createdAt));
            if (closedAt > 0) {
                builder.withDuration((closedAt - createdAt) / 1000D);
            }
            if (httpBytesReceived > 0) {
                builder.withHttpBytesReceived(httpBytesReceived);
            }
            if (httpBytesSent > 0) {
                builder.withHttpBytesSent(httpBytesSent);
            }
            if (requests > 0) {
                builder.withRequests(requests);
            }
            if (responses > 0) {
                builder.withResponses(responses);
            }
            if (peerAddress != null) {
                builder.withPeerAddress(peerAddress.getHostString())
                        .withPeerPort(peerAddress.getPort());
            }
            if (localAddress != null) {
                builder.withLocalAddress(localAddress.getHostString())
                        .withLocalPort(localAddress.getPort());
            }
            if (remoteAddress instanceof InetSocketAddress isa) {
                builder.withRemoteAddress(isa.getHostString())
                        .withRemotePort(isa.getPort());
            }
            if (sslProtocol != null && sslCipherSuite != null && sslSessionId != null) {
                builder.withSslProtocol(sslProtocol)
                        .withSslCipherSuite(sslCipherSuite)
                        .withSslSessionId(HexDump.toHexString(sslSessionId));
            }
            if (sslSniServerNames != null) {
                sslSniServerNames.stream()
                        .filter(name -> name instanceof SNIHostName && name.getType() == StandardConstants.SNI_HOST_NAME)
                        .map(name -> ((SNIHostName) name).getAsciiName())
                        .findAny()
                        .ifPresent(builder::withSslSniServerName);
            }
            if (sslPeerSubject != null && sslPeerNotAfter != null && sslPeerNotBefore != null
                    && sslPeerIssuerSubject != null && sslPeerEncodedCertificate != null) {
                builder.withSslPeerSubject(sslPeerSubject)
                        .withSslPeerIssuerSubject(sslPeerIssuerSubject)
                        .withSslPeerNotAfter(sslPeerNotAfter.toInstant())
                        .withSslPeerNotBefore(sslPeerNotBefore.toInstant())
                        .withSslPeerFingerprint(certificateFingerprint(sslPeerEncodedCertificate));
            }
            if (sslSubjectAlternativeNames != null && !sslSubjectAlternativeNames.isEmpty()) {
                builder.withSslSubjectAlternativeNames(sslSubjectAlternativeNames);
            }
            if (sslHandshakeException != null) {
                List<ExceptionEntry> exceptionChain = new ArrayList<>();
                Throwable cause = sslHandshakeException;
                while (cause != null) {
                    exceptionChain.add(new ExceptionEntry(cause.getClass().getName(), cause.getMessage()));
                    cause = cause.getCause();
                }
                String type = SslHandshakeFailure.fromSslHandshakeException(sslHandshakeException)
                        .map(SslHandshakeFailure::failureType)
                        .orElse("UNKNOWN");
                builder.withSslHandshakeFailure(new ConnectionLogEntry.SslHandshakeFailure(type, exceptionChain));
            }
            if (httpProtocol != null) {
                builder.withHttpProtocol(httpProtocol);
            }
            if (proxyProtocolVersion != null) {
                builder.withProxyProtocolVersion(proxyProtocolVersion);
            }
            if (sslBytesReceived > 0) {
                builder.withSslBytesReceived(sslBytesReceived);
            }
            if (sslBytesSent > 0) {
                builder.withSslBytesSent(sslBytesSent);
            }
            return builder.build();
        }

        private static String certificateFingerprint(byte[] derEncoded) {
            try {
                return HexDump.toHexString(MessageDigest.getInstance("SHA-1").digest(derEncoded));
            } catch (NoSuchAlgorithmException e) {
                throw new RuntimeException(e);
            }
        }

    }

}