13
0
geforkt von Mirrors/Velocity

Online-mode and encryption support

Dieser Commit ist enthalten in:
Andrew Steinborn 2018-07-27 00:10:09 -04:00
Ursprung 359d1ea17c
Commit fc5b0d3577
17 geänderte Dateien mit 443 neuen und 37 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,10 @@
package com.velocitypowered.proxy;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.http.NettyHttpClient;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils;
import com.velocitypowered.proxy.connection.client.HandshakeSessionHandler;
@ -11,16 +15,22 @@ import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import net.kyori.text.Component;
import net.kyori.text.serializer.GsonComponentSerializer;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
public class VelocityServer {
public static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.create();
private static VelocityServer server;
private EventLoopGroup bossGroup;
private EventLoopGroup childGroup;
private NettyHttpClient httpClient;
private KeyPair serverKeyPair;
public VelocityServer() {
@ -46,12 +56,15 @@ public class VelocityServer {
}
// Start the listener
bossGroup = new NioEventLoopGroup();
childGroup = new NioEventLoopGroup();
bossGroup = new NioEventLoopGroup(0, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Netty Boss Thread").build());
childGroup = new NioEventLoopGroup(0, new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Netty I/O Thread #%d").build());
httpClient = new NettyHttpClient(this);
server = this;
new ServerBootstrap()
.channel(NioServerSocketChannel.class)
.group(bossGroup, childGroup)
.childOption(ChannelOption.TCP_NODELAY, true)
.childOption(ChannelOption.IP_TOS, 0x18)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
@ -82,4 +95,8 @@ public class VelocityServer {
.channel(NioSocketChannel.class)
.group(childGroup);
}
public NettyHttpClient getHttpClient() {
return httpClient;
}
}

Datei anzeigen

@ -1,9 +1,12 @@
package com.velocitypowered.proxy.connection;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.Velocity;
import com.velocitypowered.proxy.protocol.PacketWrapper;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.compression.JavaVelocityCompressor;
import com.velocitypowered.proxy.protocol.encryption.JavaVelocityCipher;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import com.velocitypowered.proxy.protocol.netty.*;
import com.velocitypowered.proxy.protocol.packets.SetCompression;
import io.netty.channel.Channel;
@ -12,8 +15,12 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.ReferenceCountUtil;
import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.MINECRAFT_DECODER;
import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.MINECRAFT_ENCODER;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.GeneralSecurityException;
import static com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils.*;
/**
* A utility class to make working with the pipeline a little less painful and transparently handles certain Minecraft
@ -156,4 +163,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
channel.pipeline().addBefore(MINECRAFT_DECODER, "compress-decoder", decoder);
channel.pipeline().addBefore(MINECRAFT_ENCODER, "compress-encoder", encoder);
}
public void enableEncryption(byte[] secret) throws GeneralSecurityException {
SecretKey key = new SecretKeySpec(secret, "AES");
VelocityCipher decryptionCipher = new JavaVelocityCipher(false, key);
VelocityCipher encryptionCipher = new JavaVelocityCipher(true, key);
channel.pipeline().addBefore(FRAME_DECODER, "cipher-decoder", new MinecraftCipherDecoder(decryptionCipher));
channel.pipeline().addBefore(FRAME_ENCODER, "cipher-encoder", new MinecraftCipherEncoder(encryptionCipher));
}
}

Datei anzeigen

@ -4,7 +4,7 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
import io.netty.buffer.ByteBuf;
public interface MinecraftSessionHandler {
void handle(MinecraftPacket packet);
void handle(MinecraftPacket packet) throws Exception;
default void handleUnknown(ByteBuf buf) {
// No-op: we'll release the buffer later.

Datei anzeigen

@ -8,7 +8,6 @@ import com.velocitypowered.proxy.data.ServerInfo;
import com.velocitypowered.proxy.protocol.netty.MinecraftPipelineUtils;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
import com.velocitypowered.proxy.util.UuidUtils;
import io.netty.channel.*;
public class ServerConnection {
@ -56,12 +55,10 @@ public class ServerConnection {
// BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
// separated by \0 (the null byte). In order, you send the original host, the player's IP, their
// UUID (undashed), and if you are in online-mode, their login properties (retrieved from Mojang).
//
// Velocity doesn't yet support online-mode, unfortunately. That will come soon.
return serverInfo.getAddress().getHostString() + "\0" +
proxyPlayer.getRemoteAddress().getHostString() + "\0" +
UuidUtils.toUndashed(proxyPlayer.getUniqueId()) + "\0" +
"[]";
proxyPlayer.getProfile().getId() + "\0" +
VelocityServer.GSON.toJson(proxyPlayer.getProfile().getProperties());
}
private void startHandshake() {

Datei anzeigen

@ -1,5 +1,6 @@
package com.velocitypowered.proxy.connection.client;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.packets.Chat;
import com.velocitypowered.proxy.connection.MinecraftConnection;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
@ -14,23 +15,25 @@ import java.net.InetSocketAddress;
import java.util.UUID;
public class ConnectedPlayer {
private final String username;
private final UUID uniqueId;
private final GameProfile profile;
private final MinecraftConnection connection;
private ServerConnection connectedServer;
public ConnectedPlayer(String username, UUID uniqueId, MinecraftConnection connection) {
this.username = username;
this.uniqueId = uniqueId;
public ConnectedPlayer(GameProfile profile, MinecraftConnection connection) {
this.profile = profile;
this.connection = connection;
}
public String getUsername() {
return username;
return profile.getName();
}
public UUID getUniqueId() {
return uniqueId;
return profile.idAsUuid();
}
public GameProfile getProfile() {
return profile;
}
public MinecraftConnection getConnection() {

Datei anzeigen

@ -1,9 +1,11 @@
package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.data.GameProfile;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.StateRegistry;
import com.velocitypowered.proxy.protocol.packets.EncryptionRequest;
import com.velocitypowered.proxy.protocol.packets.EncryptionResponse;
import com.velocitypowered.proxy.protocol.packets.ServerLogin;
import com.velocitypowered.proxy.protocol.packets.ServerLoginSuccess;
import com.velocitypowered.proxy.connection.MinecraftConnection;
@ -11,25 +13,72 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.connection.backend.ServerConnection;
import com.velocitypowered.proxy.data.ServerInfo;
import com.velocitypowered.proxy.util.EncryptionUtils;
import com.velocitypowered.proxy.util.UuidUtils;
import java.net.InetSocketAddress;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.concurrent.ThreadLocalRandom;
public class LoginSessionHandler implements MinecraftSessionHandler {
private static final String MOJANG_SERVER_AUTH_URL =
"https://sessionserver.mojang.com/session/minecraft/hasJoined?username=%s&serverId=%s&ip=%s";
private final MinecraftConnection inbound;
private ServerLogin login;
private byte[] verify;
public LoginSessionHandler(MinecraftConnection inbound) {
this.inbound = Preconditions.checkNotNull(inbound, "inbound");
}
@Override
public void handle(MinecraftPacket packet) {
public void handle(MinecraftPacket packet) throws Exception {
if (packet instanceof ServerLogin) {
this.login = (ServerLogin) packet;
// Request encryption.
EncryptionRequest request = generateRequest();
this.verify = Arrays.copyOf(request.getVerifyToken(), 4);
inbound.write(request);
// TODO: Online-mode checks
handleSuccessfulLogin();
//handleSuccessfulLogin();
}
if (packet instanceof EncryptionResponse) {
KeyPair serverKeyPair = VelocityServer.getServer().getServerKeyPair();
EncryptionResponse response = (EncryptionResponse) packet;
byte[] decryptedVerifyToken = EncryptionUtils.decryptRsa(serverKeyPair, response.getVerifyToken());
if (!Arrays.equals(verify, decryptedVerifyToken)) {
throw new IllegalStateException("Unable to successfully decrypt the verification token.");
}
byte[] decryptedSharedSecret = EncryptionUtils.decryptRsa(serverKeyPair, response.getSharedSecret());
String serverId = EncryptionUtils.generateServerId(decryptedSharedSecret, serverKeyPair.getPublic());
String playerIp = ((InetSocketAddress) inbound.getChannel().remoteAddress()).getHostString();
VelocityServer.getServer().getHttpClient()
.get(new URL(String.format(MOJANG_SERVER_AUTH_URL, login.getUsername(), serverId, playerIp)))
.thenAccept(profileResponse -> {
try {
inbound.enableEncryption(decryptedSharedSecret);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
GameProfile profile = VelocityServer.GSON.fromJson(profileResponse, GameProfile.class);
handleSuccessfulLogin(profile);
})
.exceptionally(exception -> {
System.out.println("Can't enable encryption");
exception.printStackTrace();
inbound.close();
return null;
});
}
}
@ -43,17 +92,16 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
return request;
}
private void handleSuccessfulLogin() {
private void handleSuccessfulLogin(GameProfile profile) {
inbound.setCompressionThreshold(256);
String username = login.getUsername();
ServerLoginSuccess success = new ServerLoginSuccess();
success.setUsername(username);
success.setUuid(UuidUtils.generateOfflinePlayerUuid(username));
success.setUsername(profile.getName());
success.setUuid(profile.idAsUuid());
inbound.write(success);
// Initiate a regular connection and move over to it.
ConnectedPlayer player = new ConnectedPlayer(success.getUsername(), success.getUuid(), inbound);
ConnectedPlayer player = new ConnectedPlayer(profile, inbound);
ServerInfo info = new ServerInfo("test", new InetSocketAddress("localhost", 25565));
ServerConnection connection = new ServerConnection(info, player, VelocityServer.getServer());

Datei anzeigen

@ -3,6 +3,7 @@ package com.velocitypowered.proxy.connection.client;
import com.google.common.base.Preconditions;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.velocitypowered.proxy.VelocityServer;
import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.packets.Ping;
import com.velocitypowered.proxy.protocol.packets.StatusRequest;
@ -15,9 +16,6 @@ import net.kyori.text.TextComponent;
import net.kyori.text.serializer.GsonComponentSerializer;
public class StatusSessionHandler implements MinecraftSessionHandler {
private static final Gson GSON = new GsonBuilder()
.registerTypeHierarchyAdapter(Component.class, new GsonComponentSerializer())
.create();
private final MinecraftConnection connection;
public StatusSessionHandler(MinecraftConnection connection) {
@ -43,7 +41,7 @@ public class StatusSessionHandler implements MinecraftSessionHandler {
null
);
StatusResponse response = new StatusResponse();
response.setStatus(GSON.toJson(ping));
response.setStatus(VelocityServer.GSON.toJson(ping));
connection.write(response);
}
}

Datei anzeigen

@ -0,0 +1,63 @@
package com.velocitypowered.proxy.connection.http;
import com.velocitypowered.proxy.VelocityServer;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import javax.net.ssl.SSLEngine;
import java.net.URL;
import java.util.concurrent.CompletableFuture;
public class NettyHttpClient {
private final VelocityServer server;
public NettyHttpClient(VelocityServer server) {
this.server = server;
}
public CompletableFuture<String> get(URL url) {
String host = url.getHost();
int port = url.getPort();
boolean ssl = url.getProtocol().equals("https");
if (port == -1) {
port = ssl ? 443 : 80;
}
CompletableFuture<String> reply = new CompletableFuture<>();
server.initializeGenericBootstrap()
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
if (ssl) {
SslContext context = SslContextBuilder.forClient().build();
SSLEngine engine = context.newEngine(ch.alloc());
ch.pipeline().addLast(new SslHandler(engine));
}
ch.pipeline().addLast(new HttpClientCodec());
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, url.getPath() + "?" + url.getQuery());
request.headers().add(HttpHeaderNames.HOST, url.getHost());
request.headers().add(HttpHeaderNames.USER_AGENT, "Velocity");
ctx.writeAndFlush(request);
}
});
ch.pipeline().addLast(new SimpleHttpResponseCollector(reply));
}
})
.connect(host, port)
.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
reply.completeExceptionally(future.cause());
}
}
});
return reply;
}
}

Datei anzeigen

@ -0,0 +1,41 @@
package com.velocitypowered.proxy.connection.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
class SimpleHttpResponseCollector extends ChannelInboundHandlerAdapter {
private final StringBuilder buffer = new StringBuilder(1024);
private final CompletableFuture<String> reply;
SimpleHttpResponseCollector(CompletableFuture<String> reply) {
this.reply = reply;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof HttpResponse) {
HttpResponseStatus status = ((HttpResponse) msg).status();
if (status != HttpResponseStatus.OK) {
ctx.close();
reply.completeExceptionally(new RuntimeException("Unexpected status code " + status.code()));
}
}
if (msg instanceof HttpContent) {
buffer.append(((HttpContent) msg).content().toString(StandardCharsets.UTF_8));
((HttpContent) msg).release();
if (msg instanceof LastHttpContent) {
ctx.close();
reply.complete(buffer.toString());
}
}
}
}

Datei anzeigen

@ -0,0 +1,84 @@
package com.velocitypowered.proxy.data;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.proxy.util.UuidUtils;
import java.util.List;
import java.util.UUID;
public class GameProfile {
private final String id;
private final String name;
private final List<Property> properties;
public GameProfile(String id, String name, List<Property> properties) {
this.id = id;
this.name = name;
this.properties = ImmutableList.copyOf(properties);
}
public String getId() {
return id;
}
public UUID idAsUuid() {
return UuidUtils.fromUndashed(id);
}
public String getName() {
return name;
}
public List<Property> getProperties() {
return ImmutableList.copyOf(properties);
}
public static GameProfile forOfflinePlayer(String username) {
Preconditions.checkNotNull(username, "username");
String id = UuidUtils.toUndashed(UuidUtils.generateOfflinePlayerUuid(username));
return new GameProfile(id, username, ImmutableList.of());
}
@Override
public String toString() {
return "GameProfile{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", properties=" + properties +
'}';
}
public class Property {
private final String name;
private final String value;
private final String signature;
public Property(String name, String value, String signature) {
this.name = name;
this.value = value;
this.signature = signature;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
public String getSignature() {
return signature;
}
@Override
public String toString() {
return "Property{" +
"name='" + name + '\'' +
", value='" + value + '\'' +
", signature='" + signature + '\'' +
'}';
}
}
}

Datei anzeigen

@ -0,0 +1,43 @@
package com.velocitypowered.proxy.protocol.encryption;
import com.google.common.base.Preconditions;
import io.netty.buffer.ByteBuf;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import java.security.GeneralSecurityException;
public class JavaVelocityCipher implements VelocityCipher {
private final Cipher cipher;
private boolean disposed = false;
public JavaVelocityCipher(boolean encrypt, SecretKey key) throws GeneralSecurityException {
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
this.cipher.init(encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(key.getEncoded()));
}
@Override
public void process(ByteBuf source, ByteBuf destination) throws ShortBufferException {
ensureNotDisposed();
byte[] sourceAsBytes = new byte[source.readableBytes()];
source.readBytes(sourceAsBytes);
int outputSize = cipher.getOutputSize(sourceAsBytes.length);
byte[] destinationBytes = new byte[outputSize];
cipher.update(sourceAsBytes, 0, sourceAsBytes.length, destinationBytes);
destination.writeBytes(destinationBytes);
}
@Override
public void dispose() {
ensureNotDisposed();
disposed = true;
}
private void ensureNotDisposed() {
Preconditions.checkState(!disposed, "Object already disposed");
}
}

Datei anzeigen

@ -0,0 +1,10 @@
package com.velocitypowered.proxy.protocol.encryption;
import com.velocitypowered.proxy.util.Disposable;
import io.netty.buffer.ByteBuf;
import javax.crypto.ShortBufferException;
public interface VelocityCipher extends Disposable {
void process(ByteBuf source, ByteBuf destination) throws ShortBufferException;
}

Datei anzeigen

@ -1,8 +0,0 @@
package com.velocitypowered.proxy.protocol.encryption;
import com.velocitypowered.proxy.util.Disposable;
import io.netty.buffer.ByteBuf;
public interface VelocityEncryptor extends Disposable {
void process(ByteBuf source, ByteBuf destination);
}

Datei anzeigen

@ -0,0 +1,29 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
public class MinecraftCipherDecoder extends ByteToMessageDecoder {
private final VelocityCipher cipher;
public MinecraftCipherDecoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
ByteBuf decrypted = ctx.alloc().buffer();
try {
cipher.process(in, decrypted);
out.add(decrypted);
} catch (Exception e) {
decrypted.release();
throw e;
}
}
}

Datei anzeigen

@ -0,0 +1,20 @@
package com.velocitypowered.proxy.protocol.netty;
import com.google.common.base.Preconditions;
import com.velocitypowered.proxy.protocol.encryption.VelocityCipher;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MinecraftCipherEncoder extends MessageToByteEncoder<ByteBuf> {
private final VelocityCipher cipher;
public MinecraftCipherEncoder(VelocityCipher cipher) {
this.cipher = Preconditions.checkNotNull(cipher, "cipher");
}
@Override
protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception {
cipher.process(msg, out);
}
}

Datei anzeigen

@ -5,14 +5,40 @@ import com.velocitypowered.proxy.protocol.ProtocolConstants;
import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf;
import java.util.Arrays;
public class EncryptionResponse implements MinecraftPacket {
private byte[] sharedSecret;
private byte[] verifyToken;
public byte[] getSharedSecret() {
return sharedSecret;
}
public void setSharedSecret(byte[] sharedSecret) {
this.sharedSecret = sharedSecret;
}
public byte[] getVerifyToken() {
return verifyToken;
}
public void setVerifyToken(byte[] verifyToken) {
this.verifyToken = verifyToken;
}
@Override
public String toString() {
return "EncryptionResponse{" +
"sharedSecret=" + Arrays.toString(sharedSecret) +
", verifyToken=" + Arrays.toString(verifyToken) +
'}';
}
@Override
public void decode(ByteBuf buf, ProtocolConstants.Direction direction, int protocolVersion) {
this.sharedSecret = ProtocolUtils.readByteArray(buf, 256);
this.verifyToken = ProtocolUtils.readByteArray(buf, 4);
this.verifyToken = ProtocolUtils.readByteArray(buf, 128);
}
@Override

Datei anzeigen

@ -1,9 +1,28 @@
package com.velocitypowered.proxy.util;
import javax.crypto.Cipher;
import java.math.BigInteger;
import java.security.*;
public enum EncryptionUtils { ;
public static String twosComplementSha1Digest(byte[] digest) {
return new BigInteger(digest).toString(16);
}
public static byte[] decryptRsa(KeyPair keyPair, byte[] bytes) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
return cipher.doFinal(bytes);
}
public static String generateServerId(byte[] sharedSecret, PublicKey key) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.update(sharedSecret);
digest.update(key.getEncoded());
return twosComplementSha1Digest(digest.digest());
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}