minecraft server
spigot服務器代碼中,net.minecraft.server.v1_8_R3.PacketPlayOutPlayerInfo用來序列化玩家配置文件GameProfile給客戶端的。
某次由于服務器返回的格式不符合要求,導致在使用了某種道具后導致了某個服務器崩潰。
com.mojiang.authlib
服務器和客戶端共用代碼,用來實現(xiàn)yggdrasil用戶登錄驗證和用戶Profile的獲取。
其中com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java定義了所有使用到的url和資源域名白名單列表,直接更改此代碼再重新打包成jar可改變客戶端和服務器端的行為。
spigot服務器
入口
spigot-1.7.x-1.8.1.jar!\org\bukkit\craftbukkit\Main
->
net.minecraft.server.v1_7_R4.MinecraftServer.main(options1);
其中MinecraftServer是純凈版服務器反編譯的代碼,而org\bukkit\craftbukkit\v1_7_R4\CraftServer是自已在反編譯代碼上封裝的一層服務器接口。
net.minecraft.server.v1_7_R4.LoginListener
public void a(PacketLoginInEncryptionBegin packetlogininencryptionbegin) {
函數(shù)用來處理登錄請求,在里開啟線程向服務器驗證登錄(盜版服的情況下,直接在線程里fireLoginEvents聲明登錄成功)。
1.7版本的spigot的實現(xiàn)是開啟ThreadPlayerLookupUUID
線程類來驗證登錄。
1.8.8版本在此函數(shù)中直接開啟匿名線程類,但里面的流程還是大致相同的,都是通過調用LoginListener.this.server.aD().hasJoinedServer(...)
來驗證登錄,這個aD()
返回的即是上面提到的YggdrasilMinecraftSessionService
。
在hasJoinedServer里轉調net.minecraft.util.com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService.makeRequest
并指定鏈接來獲取一個HasJoinedMinecraftServerResponse
格式的對象,這個對象的json原形在在mc的登錄驗證接口文檔中有,就不再多說了。拿到Response后,hasJoinedServer使用Response構造出一個GameProfile并且返回,LoginListener將返回的GameProfile保存在成員變量i里。
這里連接成功了就開始觸發(fā)連接后處理流程,其中有一個工作流程是騎過LoginListener調用PlayerList然后通過上面提到的PacketPlayOutPlayerInfo構造一個包,并通過net.minecraft.server.v1_7_R4.PlayerConnection.SendPacket()加入到發(fā)送隊列,最后發(fā)送出去。在網絡發(fā)送時,調用PacketPlayOutPlayerInfo.b將這個packet序列化成二進制。
但是在Spigot 1.7的,在packetdataserializer.version >= 20 這個分支才完整的輸出了皮膚和披風等信息;在另外的分支里,只輸出了name。通過http://wiki.vg/Protocol_version_numbers中得到20版本號是介于1.7.10(version=5)和1.8版本(version=47)之間的某測試版本的版本號,所以對于老版本mc客戶端應該是不會直接返回帶Propertys的GameProfile的包。
造成這種代碼區(qū)別的原因是因為,在1.7.10也就是version為5的協(xié)議中用戶列表中只有一種消息,只有三個字段Player name、Online、Ping。
而在在47版本的協(xié)議中區(qū)分了更多的類型,里面添加了action并且提供對一組用戶的通知。action為0(add player)的消息中附帶有GameProfile中的Property的屬性。
在1.7.10之前版本應該是只能通過Mojang API#UUID -> Profile + Skin/Cape來請求皮膚和披風。
public void PacketPlayOutPlayerInfo.b(PacketDataSerializer packetdataserializer) throws IOException {
if(packetdataserializer.version >= 20) {
...
case 0:
packetdataserializer.a(this.player.getName());
PropertyMap properties = this.player.getProperties();
packetdataserializer.b(properties.size());
Iterator i$ = properties.values().iterator();
while(i$.hasNext()) {
Property property = (Property)i$.next();
packetdataserializer.a(property.getName());
packetdataserializer.a(property.getValue());
packetdataserializer.writeBoolean(property.hasSignature());
if(property.hasSignature()) {
packetdataserializer.a(property.getSignature());
}
}
...
} else {
packetdataserializer.a(this.username);
packetdataserializer.writeBoolean(this.action != 4);
packetdataserializer.writeShort(this.ping);
}
}
BungeeCord
支持的客戶端版本列表
在net.md_5.bungee.protocol.ProtocolConstants.java
里定義了SUPPORTED_VERSION_IDS,如:
public static final List<String> SUPPORTED_VERSIONS = Arrays.asList(
"1.8.x",
"1.9.x",
"1.10.x",
"1.11.x"
);
public static final List<Integer> SUPPORTED_VERSION_IDS = Arrays.asList( ProtocolConstants.MINECRAFT_1_8,
ProtocolConstants.MINECRAFT_1_9,
ProtocolConstants.MINECRAFT_1_9_1,
ProtocolConstants.MINECRAFT_1_9_2,
ProtocolConstants.MINECRAFT_1_9_4,
ProtocolConstants.MINECRAFT_1_10,
ProtocolConstants.MINECRAFT_1_11
);
正版登錄驗證
在net.md_5.bungee.connection.InitialHandler
的public void handle(EncryptionResponse encryptResponse)
方法中,調用
精簡版本代碼:
HttpClient.get("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" + xxx,new Callback(){
if (success){
...
} else {
InitialHandler.this.disconnect("給客戶端的提示錯誤信息")
}
});
客戶端
authlib
同上面服務器,只不過客戶端的authlib是在.minecraft中的.minecraft\libraries\com\mojang\authlib
目錄中,替換和原客戶端相同的版本即可。
皮膚和披風獲取
服務器訪問MojangAPi驗證客戶端登錄后就有了皮膚和披風數(shù)據(jù),然后加入緩存。
1.8版本在登錄成功后,服務器就會返回給客戶端的Player_List_Item消息中就加入皮膚和披風數(shù)據(jù),所以客戶端可以直接展示自己及別人的皮膚。
1.7以前版本的客戶端,1.7版本通過Spawn Player通知某個玩家周圍可見用戶的皮膚數(shù)據(jù)。但自己的皮膚需要單獨在YggdrasilMinecraftSessionService類的protected GameProfile fillGameProfile(GameProfile gameprofile, boolean flag) 方法中訪問MojangApi來獲取自己的皮膚數(shù)據(jù),返回的結果跟服務器訪問MojangAPi得到的結果差不多。
{
"timestamp": 1501839740,
"profileId": "08d699bb6400355e981b678c9441fa75",
"profileName": "k1988",
"signatureRequired": false,
"textures": {
"CAPE": {
"url": "http://icon.mc.kuai8.com/cape/douyu.png"
},
"SKIN": {
"url": "http://icon.mc.kuai8.com/imshop/201708/20170803110431142.png"
}
}
}
白名單
為了安全起見,皮膚和披風的鏈接都需要在YggdrasilMinecraftSessionService.isWhitelistedDomain中判斷是否預定義的幾個白名單網址。
forge版本
無敵模式
在編譯spigot時反編譯了net.minecraft.server.Entity的代碼中,有一個函數(shù)
public boolean damageEntity(DamageSource damagesource, float f) {
if (this.isInvulnerable(damagesource)) {
return false;
} else {
this.ac();
return false;
}
}
在forge版本的net.minecraft.entity.player.EntityPlayerMp
的代碼中,同樣有一段類似但更復雜的函數(shù),如果hook掉此函數(shù)的功能直接return false,即可實現(xiàn)無敵模式。
public boolean func_70097_a(DamageSource source, float amount) {
if(this.func_180431_b(source)) {
return false;
} else {
boolean flag = this.field_71133_b.func_71262_S() && this.func_175400_cq() && "fall".equals(source.field_76373_n);
if(!flag && this.field_147101_bU > 0 && source != DamageSource.field_76380_i) {
return false;
} else {
if(source instanceof EntityDamageSource) {
Entity entity = source.func_76346_g();
if(entity instanceof EntityPlayer && !this.func_96122_a((EntityPlayer)entity)) {
return false;
}
if(entity instanceof EntityArrow) {
EntityArrow entityarrow = (EntityArrow)entity;
if(entityarrow.field_70250_c instanceof EntityPlayer && !this.func_96122_a((EntityPlayer)entityarrow.field_70250_c)) {
return false;
}
}
}
return super.func_70097_a(source, amount);
}
}
}
皮膚性別選擇
游戲中默認皮膚是Steve還是Alex的選擇方式。
ref:http://wiki.vg/Mojang_API#UUID_-.3E_Profile_.2B_Skin.2FCape
/*
* uuid的hashCode如果是奇數(shù)就是Alex,為偶數(shù)就是Steve
*/
private static void printType(String uuid) {
UUID uid = UUID.fromString(uuid);
if ((uid.hashCode() & 1) != 0) {
System.out.println(uid.toString() + " = Alex");
} else {
System.out.println(uid.toString() + " = Steve");
}
}