package net.minecraft.server.jsonrpc; import com.google.common.collect.Sets; import com.google.common.net.HostAndPort; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.mojang.logging.LogUtils; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.websocketx.WebSocketFrameAggregator; import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslContext; import java.net.InetSocketAddress; import java.util.Objects; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import net.minecraft.server.jsonrpc.internalapi.MinecraftApi; import net.minecraft.server.jsonrpc.security.AuthenticationHandler; import net.minecraft.server.jsonrpc.websocket.JsonToWebSocketEncoder; import net.minecraft.server.jsonrpc.websocket.WebSocketToJsonCodec; import net.minecraft.server.notifications.NotificationManager; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; public class ManagementServer { private static final Logger LOGGER = LogUtils.getLogger(); private final HostAndPort hostAndPort; private final AuthenticationHandler authenticationHandler; @Nullable private Channel serverChannel; private final NioEventLoopGroup nioEventLoopGroup; @Nullable private ScheduledFuture heartbeat; private final Set connections = Sets.newIdentityHashSet(); public ManagementServer(final HostAndPort hostAndPort, final AuthenticationHandler authenticationHandler) { this.hostAndPort = hostAndPort; this.authenticationHandler = authenticationHandler; this.nioEventLoopGroup = new NioEventLoopGroup(0, new ThreadFactoryBuilder().setNameFormat("Management server IO #%d").setDaemon(true).build()); } public ManagementServer(final HostAndPort hostAndPort, final AuthenticationHandler authenticationHandler, final NioEventLoopGroup nioEventLoopGroup) { this.hostAndPort = hostAndPort; this.authenticationHandler = authenticationHandler; this.nioEventLoopGroup = nioEventLoopGroup; } public boolean scheduleHeartbeat(final NotificationManager notificationManager, final long period) { if (this.heartbeat != null && !this.heartbeat.cancel(true)) { LOGGER.warn("The existing heartbeat was not canceled and the new heartbeat of {} seconds has not been applied.", period); return false; } else { if (period > 0L) { this.heartbeat = this.nioEventLoopGroup.scheduleAtFixedRate(notificationManager::statusHeartbeat, period, period, TimeUnit.SECONDS); } return true; } } public void onConnected(final Connection connection) { synchronized (this.connections) { this.connections.add(connection); } } public void onDisconnected(final Connection connection) { synchronized (this.connections) { this.connections.remove(connection); } } public void startWithoutTls(final MinecraftApi minecraftApi) { this.start(minecraftApi, null); } public void startWithTls(final MinecraftApi minecraftApi, final SslContext sslContext) { this.start(minecraftApi, sslContext); } private void start(final MinecraftApi minecraftApi, @Nullable final SslContext sslContext) { final JsonRpcLogger jsonrpcLogger = new JsonRpcLogger(); ChannelFuture channel = new ServerBootstrap() .handler(new LoggingHandler(LogLevel.DEBUG)) .channel(NioServerSocketChannel.class) .childHandler( new ChannelInitializer() { { Objects.requireNonNull(ManagementServer.this); } @Override protected void initChannel(final Channel channel) { try { channel.config().setOption(ChannelOption.TCP_NODELAY, true); } catch (ChannelException var3) { } ChannelPipeline pipeline = channel.pipeline(); if (sslContext != null) { pipeline.addLast(sslContext.newHandler(channel.alloc())); } pipeline.addLast(new HttpServerCodec()) .addLast(new HttpObjectAggregator(65536)) .addLast(ManagementServer.this.authenticationHandler) .addLast(new WebSocketServerProtocolHandler("/")) .addLast(new WebSocketFrameAggregator(65536)) .addLast(new WebSocketToJsonCodec()) .addLast(new JsonToWebSocketEncoder()) .addLast(new Connection(channel, ManagementServer.this, minecraftApi, jsonrpcLogger)); } } ) .group(this.nioEventLoopGroup) .localAddress(this.hostAndPort.getHost(), this.hostAndPort.getPort()) .bind(); this.serverChannel = channel.channel(); channel.syncUninterruptibly(); LOGGER.info("Json-RPC Management connection listening on {}:{}", this.hostAndPort.getHost(), this.getPort()); } public void stop(final boolean closeNioEventLoopGroup) throws InterruptedException { if (this.serverChannel != null) { this.serverChannel.close().sync(); this.serverChannel = null; } this.connections.clear(); if (closeNioEventLoopGroup) { this.nioEventLoopGroup.shutdownGracefully().sync(); } } public void tick() { this.forEachConnection(Connection::tick); } public int getPort() { return this.serverChannel != null ? ((InetSocketAddress)this.serverChannel.localAddress()).getPort() : this.hostAndPort.getPort(); } void forEachConnection(final Consumer action) { synchronized (this.connections) { this.connections.forEach(action); } } }