You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There are many issues with how RCON connections are handled on Minecraft servers, so special hacks are required to make them work robustly.
I took some time to review the decompiled Java source code and created a minimal implementation for Deno:
exportinterfaceRconConnectOptionsextendsDeno.ConnectOptions{port: number;password: string;host?: string;}exportclassRconClient{staticasyncconnect(options: RconConnectOptions): Promise<RconClient>{constconn=awaitDeno.connect(options);conn.setNoDelay(true);constclient=newRconClient(conn);awaitclient.auth(options.password);returnclient;}
#writer: WritableStreamDefaultWriter<Uint8Array>;
#reader: ReadableStreamBYOBReader;
#lockPromise: Promise<void>=Promise.resolve();
#authed =false;
#reqId =0;
#closed =false;
#buf =newUint8Array(14+3*4096);// 14 byte header + worst case utf-8 length of 4096 utf-16 code units
#skipRead =false;
#pos =0;constructor(conn: Deno.Conn){this.#writer =conn.writable.getWriter();this.#reader =conn.readable.getReader({mode: "byob"});}asyncauth(password: string){if(this.#authed)thrownewError("Already authenticated");constrelease=awaitthis.#lock();try{constreqId=this.#nextReqId();awaitthis.#send(reqId,3,password);constres=awaitthis.#recv();if(!res)thrownewError("Connection closed");if(res.id==-1)thrownewError("Authentication failed");if(res.id!=reqId)thrownewError(`Invalid response id (expected ${reqId}, got ${res.id})`);if(res.type!=2)thrownewError(`Unexpected response type (expected 2, got ${res.type})`);this.#authed =true;}catch(e){awaitthis.close();throwe;}finally{release();}}asynccmd(cmd: string): Promise<string|null>{if(!this.#authed)thrownewError("Not authenticated");constrelease=awaitthis.#lock();try{constreqId=this.#nextReqId();awaitthis.#send(reqId,2,cmd);// By reading immediately after each write, we avoid packets being combined by the TCP stack,// thus avoiding an implementation bug in the Minecraft serverconstres=awaitthis.#recv();if(!res)returnnull;if(res.id!=reqId)thrownewError(`Invalid response id (expected ${reqId}, got ${res.id})`);if(res.type!=0)thrownewError(`Unexpected response type (expected 0, got ${res.type})`);if(res.message.length<4096){// message is guaranteed to be not fragmented// note: minecraft java counts the length as utf-16 code units (like we do here)this.#reqId +=1;returnres.message;}constdummyReqId=this.#nextReqId();// send dummy request which is guaranteed to be not fragmentedawaitthis.#send(dummyReqId,-1,"");letoutput=res.message;while(true){constres=awaitthis.#recv();if(!res)returnnull;// message is complete when dummy response is receivedif(res.id==dummyReqId)break;if(res.id!=reqId)thrownewError(`Invalid response id (expected ${reqId}, got ${res.id})`);if(res.type!=0)thrownewError(`Unexpected response type (expected 0, got ${res.type})`);output+=res.message;}returnoutput;}catch(e){if(!this.#closed)awaitthis.close();throwe;}finally{release();}}asyncclose(){this.#closed =true;awaitthis.#writer.close();}async #send(id: number,type: number,message: string){if(this.#closed)thrownewError("Connection closed");constpayload=newTextEncoder().encode(message);constbuf=newUint8Array(14+payload.length);constview=newDataView(buf.buffer);view.setUint32(0,10+payload.length,true);view.setInt32(4,id,true);view.setInt32(8,type,true);buf.set(payload,12);// this may still result in a fragmented tcp packet if used over external network interfaces,// therefore using RCON over anything other than localhost is not recommendedif(buf.length>1460)thrownewError("Message too long");awaitthis.#writer.write(buf);}async #recv(): Promise<{id: number;type: number;message: string}|null>{while(!this.#closed){if(!this.#skipRead){constresult=awaitthis.#reader.read(this.#buf.subarray(this.#pos));if(result.done){this.#closed =true;returnnull;}this.#buf =newUint8Array(result.value.buffer);this.#pos +=result.value.length;}this.#skipRead =false;if(this.#pos <4)continue;constview=newDataView(this.#buf.buffer,this.#buf.byteOffset,this.#pos);constlen=view.getUint32(0,true);if(this.#pos <4+len)continue;constid=view.getInt32(4,true);consttype=view.getInt32(8,true);constpayload=this.#buf.subarray(12,len+2);constmessage=newTextDecoder().decode(payload);this.#buf.copyWithin(0,len+4);this.#pos -=len+4;this.#skipRead =true;return{ id, type, message };}returnnull;}
#lock(): Promise<()=>void>{returnnewPromise((resolve)=>{this.#lockPromise =this.#lockPromise.then(()=>{returnnewPromise((release)=>resolve(release));});});}
#nextReqId(){constid=this.#reqId;this.#reqId =(this.#reqId +1)%0x8000_0000;returnid;}}
I may use it as reference for improving/rewriting this library. Not sure how or if to handle non-Minecraft RCON implementations.
The text was updated successfully, but these errors were encountered:
janispritzkau
changed the title
Fix connection issues
Fix connection issues and fragmentation
Jan 4, 2024
Also, notice how Minecraft uses UTF-16 code units when splitting responses into multiple packets, but Source RCON Protocol specifies bytes. And unlike the Source Dedicated Server, Minecraft doesn't echo unknown request types, so the multi-packet response handling method doesn't work for Minecraft.
It's unfortunate, how broken the protocol is. And also on my part, for not specifying exactly what this library was made for.
There are many issues with how RCON connections are handled on Minecraft servers, so special hacks are required to make them work robustly.
I took some time to review the decompiled Java source code and created a minimal implementation for Deno:
I may use it as reference for improving/rewriting this library. Not sure how or if to handle non-Minecraft RCON implementations.
The text was updated successfully, but these errors were encountered: