Skip to content

Commit

Permalink
Support unix domain sockets for the access log handler (micronaut-pro…
Browse files Browse the repository at this point in the history
…jects#10940)

* Support unix domain sockets for the access log handler
Fixes LMP-319

* fix checkstyle
  • Loading branch information
yawkat authored Jul 6, 2024
1 parent 6f27055 commit 4e4a84f
Show file tree
Hide file tree
Showing 14 changed files with 425 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
import io.micronaut.core.annotation.Internal;
import io.micronaut.http.server.netty.handler.accesslog.element.AccessLog;
import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser;
import io.micronaut.http.server.netty.handler.accesslog.element.ConnectionMetadata;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http2.Http2Connection;
import org.slf4j.Logger;
Expand Down Expand Up @@ -68,7 +68,7 @@ public void logHeaders(ChannelHandlerContext ctx, int streamId, HttpRequest requ
accessLog = formatParser.newAccessLogger();
}
connection.stream(streamId).setProperty(accessLogKey, accessLog);
accessLog.onRequestHeaders((SocketChannel) ctx.channel(), request.method().name(), request.headers(), request.uri(), HttpAccessLogHandler.H2_PROTOCOL_NAME);
accessLog.onRequestHeaders(ConnectionMetadata.ofNettyChannel(ctx.channel()), request.method().name(), request.headers(), request.uri(), HttpAccessLogHandler.H2_PROTOCOL_NAME);
}

public record Factory(Logger logger, String spec, Predicate<String> uriInclusion) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.server.netty.handler.accesslog.element.AccessLog;
import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser;
import io.micronaut.http.server.netty.handler.accesslog.element.ConnectionMetadata;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
Expand Down Expand Up @@ -108,21 +107,9 @@ public HttpAccessLogHandler(Logger logger, String spec, Predicate<String> uriInc
this.uriInclusion = uriInclusion;
}

private SocketChannel findSocketChannel(Channel channel) {
if (channel instanceof SocketChannel socketChannel) {
return socketChannel;
}
Channel parent = channel.parent();
if (parent == null) {
throw new IllegalArgumentException("No socket channel available");
}
return findSocketChannel(parent);
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Http2Exception {
if (logger.isInfoEnabled() && msg instanceof HttpRequest request) {
final SocketChannel channel = findSocketChannel(ctx.channel());
AccessLogHolder accessLogHolder = getAccessLogHolder(ctx, true);
assert accessLogHolder != null; // can only return null when createIfMissing is false
if (uriInclusion == null || uriInclusion.test(request.uri())) {
Expand All @@ -134,7 +121,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Http2Excep
} else {
protocol = request.protocolVersion().text();
}
accessLogHolder.createLogForRequest().onRequestHeaders(channel, request.method().name(), request.headers(), request.uri(), protocol);
accessLogHolder.createLogForRequest().onRequestHeaders(ConnectionMetadata.ofNettyChannel(ctx.channel()), request.method().name(), request.headers(), request.uri(), protocol);
} else {
accessLogHolder.excludeRequest();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ public void reset() {
onLastResponseWriteElements.forEach(this::resetIndexedLogElement);
}

/**
* Triggers LogElements for the ON_REQUEST_HEADERS event.
*
* @param metadata The connection metadata.
* @param method The http method.
* @param headers The request headers.
* @param uri The uri.
* @param protocol The protocol.
*/
public void onRequestHeaders(ConnectionMetadata metadata, String method, HttpHeaders headers, String uri, String protocol) {
for (IndexedLogElement element: onRequestHeadersElements) {
elements[element.index] = element.onRequestHeaders(metadata, method, headers, uri, protocol);
}
}

/**
* Triggers LogElements for the ON_REQUEST_HEADERS event.
*
Expand All @@ -71,11 +86,11 @@ public void reset() {
* @param headers The request headers.
* @param uri The uri.
* @param protocol The protocol.
* @deprecated Use {@link #onRequestHeaders(ConnectionMetadata, String, HttpHeaders, String, String)} instead
*/
@Deprecated
public void onRequestHeaders(SocketChannel channel, String method, HttpHeaders headers, String uri, String protocol) {
for (IndexedLogElement element: onRequestHeadersElements) {
elements[element.index] = element.onRequestHeaders(channel, method, headers, uri, protocol);
}
onRequestHeaders(new ConnectionMetadataImpl.SocketChannelMetadata(channel), method, headers, uri, protocol);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
*/
package io.micronaut.http.server.netty.handler.accesslog.element;

import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.io.service.SoftServiceLoader;
import io.micronaut.core.order.OrderUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.EmptyHttpHeaders;
import io.netty.handler.codec.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -153,7 +155,7 @@ private void parse(String spec) {
// constants
constantElements.add(indexedLogElement);
// pre-fill log values with constant
elements[i] = element.onRequestHeaders(null, null, null, null, null);
elements[i] = element.onRequestHeaders(ConnectionMetadata.empty(), "", EmptyHttpHeaders.INSTANCE, "", "");
continue;
}
if (element.events().contains(LogElement.Event.ON_LAST_RESPONSE_WRITE)) {
Expand Down Expand Up @@ -291,8 +293,8 @@ public void reset() {
}

@Override
public String onRequestHeaders(SocketChannel channel, String method, io.netty.handler.codec.http.HttpHeaders headers, String uri, String protocol) {
return delegate.onRequestHeaders(channel, method, headers, uri, protocol);
public String onRequestHeaders(@NonNull ConnectionMetadata metadata, @NonNull String method, @NonNull HttpHeaders headers, @NonNull String uri, @NonNull String protocol) {
return delegate.onRequestHeaders(metadata, method, headers, uri, protocol);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.netty.handler.accesslog.element;

import io.micronaut.core.annotation.NonNull;
import io.netty.channel.Channel;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioDomainSocketChannel;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.UnixDomainSocketAddress;
import java.util.Optional;

/**
* Connection-level metadata for logging e.g. addresses or ports.
*
* @since 4.6.0
* @author Jonas Konrad
*/
public interface ConnectionMetadata {
/**
* The local address of this connection, if applicable.
*
* @return The local address
*/
@NonNull
Optional<SocketAddress> localAddress();

/**
* The remote address of this connection, if applicable.
*
* @return The remote address
*/
@NonNull
Optional<SocketAddress> remoteAddress();

/**
* Get the host address string of the given {@link SocketAddress} instance. This is usually the
* numeric IP, but for unix domain socket it is the file path.
*
* @param a The address
* @return The string representation, or {@link Optional#empty()} if this type of address is
* not supported
*/
static Optional<String> getHostAddress(@NonNull SocketAddress a) {
if (a instanceof InetSocketAddress addr) {
return Optional.of(addr.getAddress().getHostAddress());
} else {
return getHostName(a);
}
}

/**
* Get the host name of the given {@link SocketAddress} instance. This is usually the DNS name
* or IP, but for unix domain socket it is the file path.
*
* @param a The address
* @return The string representation, or {@link Optional#empty()} if this type of address is
* not supported
*/
static Optional<String> getHostName(@NonNull SocketAddress a) {
if (a instanceof InetSocketAddress addr) {
return Optional.of(addr.getAddress().getHostName());
} else if (a instanceof UnixDomainSocketAddress addr) {
return Optional.of(addr.getPath().toString())
// remote address is empty string
.filter(s -> !s.isEmpty());
} else if (ConnectionMetadataImpl.DOMAIN_SOCKET_ADDRESS.isInstance(a)) {
String path = ConnectionMetadataImpl.DomainSocketUtil.getPath(a);
if (path.isEmpty() || path.equals("\0")) {
return Optional.empty();
}
if (path.startsWith("\0")) {
// *abstract* unix domain sockets start with a NUL byte
path = "@" + path.substring(1);
}
return Optional.of(path);
} else {
return Optional.empty();
}
}

/**
* Create a new {@link ConnectionMetadata} instance for the given netty channel.
*
* @param channel The channel
* @return The metadata, potentially {@link #empty()}
*/
@NonNull
static ConnectionMetadata ofNettyChannel(@NonNull Channel channel) {
if (channel instanceof SocketChannel sc) {
return new ConnectionMetadataImpl.SocketChannelMetadata(sc);
} else if (ConnectionMetadataImpl.QUIC_CHANNEL != null && ConnectionMetadataImpl.QUIC_CHANNEL.isInstance(channel)) {
return new ConnectionMetadataImpl.QuicChannelMetadata(channel);
} else if (channel instanceof DatagramChannel || channel instanceof NioDomainSocketChannel || (ConnectionMetadataImpl.DOMAIN_SOCKET_CHANNEL != null && ConnectionMetadataImpl.DOMAIN_SOCKET_CHANNEL.isInstance(channel))) {
return new ConnectionMetadataImpl.GenericChannelMetadata(channel);
} else if (channel.parent() != null) {
// QUIC / HTTP/2 stream channels
return ofNettyChannel(channel.parent());
} else {
return empty();
}
}

/**
* Placeholder metadata for unsupported channel types.
*
* @return An empty metadata instance
*/
@NonNull
static ConnectionMetadata empty() {
return ConnectionMetadataImpl.Empty.INSTANCE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright 2017-2024 original authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.micronaut.http.server.netty.handler.accesslog.element;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.netty.channel.Channel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.unix.DomainSocketAddress;
import io.netty.channel.unix.DomainSocketChannel;
import io.netty.incubator.codec.quic.QuicChannel;

import java.net.SocketAddress;
import java.util.Optional;

/**
* Implementations of {@link ConnectionMetadata}.
*
* @since 4.6.0
* @author Jonas Konrad
*/
@Internal
final class ConnectionMetadataImpl {
static final Class<?> QUIC_CHANNEL;
static final Class<?> DOMAIN_SOCKET_ADDRESS;
static final Class<?> DOMAIN_SOCKET_CHANNEL;

static {
Class<QuicChannel> quicChannelClass;
try {
quicChannelClass = QuicChannel.class;
} catch (Exception e) {
quicChannelClass = null;
}
QUIC_CHANNEL = quicChannelClass;

Class<DomainSocketAddress> domainSocketAddressClass;
Class<DomainSocketChannel> domainSocketChannelClass;
try {
domainSocketAddressClass = DomainSocketAddress.class;
domainSocketChannelClass = DomainSocketChannel.class;
} catch (Exception e) {
domainSocketAddressClass = null;
domainSocketChannelClass = null;
}
DOMAIN_SOCKET_ADDRESS = domainSocketAddressClass;
DOMAIN_SOCKET_CHANNEL = domainSocketChannelClass;
}

static class DomainSocketUtil {
static String getPath(SocketAddress address) {
return ((DomainSocketAddress) address).path();
}
}

/**
* This one is separate from {@link GenericChannelMetadata} because it has special handling for
* compatibility.
*
* @param ch The channel
*/
@Internal
record SocketChannelMetadata(SocketChannel ch) implements ConnectionMetadata {
@Override
public @NonNull Optional<SocketAddress> localAddress() {
return Optional.of(ch.localAddress());
}

@Override
public @NonNull Optional<SocketAddress> remoteAddress() {
return Optional.of(ch.remoteAddress());
}
}

@Internal
record GenericChannelMetadata(Channel ch) implements ConnectionMetadata {
@Override
public @NonNull Optional<SocketAddress> localAddress() {
return Optional.of(ch.localAddress());
}

@Override
public @NonNull Optional<SocketAddress> remoteAddress() {
return Optional.of(ch.remoteAddress());
}
}

@Internal
record QuicChannelMetadata(Channel ch) implements ConnectionMetadata {
@Override
public @NonNull Optional<SocketAddress> localAddress() {
return Optional.ofNullable(((QuicChannel) ch).localSocketAddress());
}

@Override
public @NonNull Optional<SocketAddress> remoteAddress() {
return Optional.ofNullable(((QuicChannel) ch).remoteSocketAddress());
}
}

@Internal
public static final class Empty implements ConnectionMetadata {
static final ConnectionMetadata INSTANCE = new Empty();

@Override
public @NonNull Optional<SocketAddress> localAddress() {
return Optional.empty();
}

@Override
public @NonNull Optional<SocketAddress> remoteAddress() {
return Optional.empty();
}
}
}
Loading

0 comments on commit 4e4a84f

Please sign in to comment.