1 /* 2 * Copyright (c) 2017-2020 sel-project 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in all 12 * copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 * SOFTWARE. 21 * 22 */ 23 /** 24 * Copyright: 2017-2020 sel-project 25 * License: MIT 26 * Authors: Kripth 27 * Source: $(HTTP github.com/sel-project/sel-client/sel/client/java.d, sel/client/java.d) 28 */ 29 module sel.client.java; 30 31 import std.conv : to; 32 import std.datetime : Duration, dur; 33 import std.datetime.stopwatch : StopWatch; 34 import std.json : JSONValue, JSON_TYPE, parseJSON; 35 import std.net.curl : HTTP, post; 36 import std.random : uniform; 37 import std.socket : Socket, TcpSocket, SocketOptionLevel, SocketOption, Address; 38 import std.uuid : UUID, parseUUID; 39 import std.zlib : Compress, UnCompress; 40 41 import sel.client.client : isSupported, Client; 42 import sel.client.util : Server, IHandler; 43 import sel.net : Stream, TcpStream, ModifierStream, LengthPrefixedStream, CompressedStream; 44 45 import sul.utils.var : varuint; 46 47 debug import std.stdio : writeln; 48 49 class JavaClient(uint __protocol) : Client if(isSupported!("java", __protocol)) { 50 51 public static string randomUsername() { 52 enum char[] pool = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_".dup; 53 char[] ret = new char[uniform!"[]"(3, 16)]; 54 foreach(ref c ; ret) { 55 c = pool[uniform(0, $)]; 56 } 57 return ret.idup; 58 } 59 60 mixin("import Status = sul.protocol.java" ~ to!string(__protocol) ~ ".status;"); 61 mixin("import Login = sul.protocol.java" ~ to!string(__protocol) ~ ".login;"); 62 63 mixin("public import Clientbound = sul.protocol.java" ~ to!string(__protocol) ~ ".clientbound;"); 64 mixin("public import Serverbound = sul.protocol.java" ~ to!string(__protocol) ~ ".serverbound;"); 65 66 private string _lasterror; 67 68 private string accessToken; 69 private UUID uuid; 70 71 public this(string name) { 72 super(name); 73 } 74 75 public this(string email, string password) { 76 // authenticate user 77 JSONValue[string] payload; 78 payload["agent"] = ["name": JSONValue("Minecraft"), "version": JSONValue(1)]; 79 payload["username"] = email; 80 payload["password"] = password; 81 auto response = postJSON("https://authserver.mojang.com/authenticate", JSONValue(payload)); 82 if(response.type == JSON_TYPE.OBJECT) { 83 auto at = "accessToken" in response; 84 auto sp = "selectedProfile" in response; 85 if(at && at.type == JSON_TYPE.STRING && sp && sp.type == JSON_TYPE.OBJECT) { 86 this.accessToken = at.str; 87 auto profile = (*sp).object; 88 email = profile["name"].str; 89 this.uuid = parseUUID(profile["id"].str); 90 } 91 } 92 this(email); 93 } 94 95 public this() { 96 this(randomUsername()); 97 } 98 99 public override pure nothrow @property @safe @nogc ushort defaultPort() { 100 return ushort(25565); 101 } 102 103 public final pure nothrow @property @safe @nogc string lastError() { 104 return this._lasterror; 105 } 106 107 protected override Server pingImpl(Address address, string ip, ushort port, Duration timeout) { 108 Socket socket = new TcpSocket(address.addressFamily); 109 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 110 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout); 111 socket.connect(address); 112 socket.blocking = true; 113 auto stream = new LengthPrefixedStream!varuint(new TcpStream(socket)); 114 // require status 115 stream.send(new Status.Handshake(__protocol, ip, port, Status.Handshake.STATUS).encode()); 116 stream.send(new Status.Request().encode()); 117 ubyte[] packet = stream.receive(); 118 if(packet.length && packet[0] == Status.Response.ID) { 119 auto json = parseJSON(Status.Response.fromBuffer(packet).json); 120 if(json.type == JSON_TYPE.OBJECT) { 121 Server server; 122 auto description = "description" in json; 123 auto version_ = "version" in json; 124 auto players = "players" in json; 125 auto favicon = "favicon" in json; 126 if(description) { 127 server = Server(chatToString(*description), 0, 0, 0, 0); 128 } 129 if(players && players.type == JSON_TYPE.OBJECT) { 130 auto online = "online" in *players; 131 auto max = "max" in *players; 132 if(online && online.type == JSON_TYPE.INTEGER && max && max.type == JSON_TYPE.INTEGER) { 133 server.online = cast(uint)online.integer; 134 server.max = cast(uint)max.integer; 135 } 136 } 137 if(version_ && version_.type == JSON_TYPE.OBJECT) { 138 auto protocol = "protocol" in *version_; 139 if(protocol && protocol.type == JSON_TYPE.INTEGER) { 140 server.protocol = cast(uint)protocol.integer; 141 } 142 } 143 if(favicon && favicon.type == JSON_TYPE.STRING) { 144 server.favicon = favicon.str; 145 } 146 // ping 147 StopWatch timer; 148 timer.start(); 149 stream.send(new Status.Latency(0).encode()); 150 packet = stream.receive(); 151 if(packet.length == 9 && packet[0] == Status.Latency.ID) { 152 timer.stop(); 153 timer.peek.split!"msecs"(server.ping); 154 socket.close(); 155 return server; 156 } 157 } 158 } 159 socket.close(); 160 return Server.init; 161 } 162 163 protected override string rawPingImpl(Address address, string ip, ushort port, Duration timeout) { 164 Socket socket = new TcpSocket(address.addressFamily); 165 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 166 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout); 167 socket.connect(address); 168 socket.blocking = true; 169 auto stream = new LengthPrefixedStream!varuint(new TcpStream(socket)); 170 // require status 171 stream.send(new Status.Handshake(__protocol, ip, port, Status.Handshake.STATUS).encode()); 172 stream.send(new Status.Request().encode()); 173 ubyte[] packet = stream.receive(); 174 socket.close(); 175 if(packet.length && packet[0] == Status.Response.ID) { 176 return Status.Response.fromBuffer(packet).json; 177 } else { 178 return ""; 179 } 180 } 181 182 protected override Stream connectImpl(Address address, string ip, ushort port, Duration timeout, IHandler handler) { 183 Socket socket = new TcpSocket(address.addressFamily); 184 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true); 185 socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, timeout); 186 socket.connect(address); 187 socket.blocking = true; 188 Stream stream = new LengthPrefixedStream!varuint(new TcpStream(socket)); 189 // handshake 190 stream.send(new Status.Handshake(__protocol, ip, port, Status.Handshake.LOGIN).encode()); 191 stream.send(new Login.LoginStart(this.name).encode()); 192 // 193 bool delegate(ubyte[])[ubyte] expected = [ 194 Login.Disconnect.ID: (ubyte[] buffer){ 195 this._lasterror = "Disconnected: " ~ chatToString(parseJSON(Login.Disconnect.fromBuffer!false(buffer).reason)); 196 return false; 197 }, 198 Login.EncryptionRequest.ID: (ubyte[] buffer){ 199 this._lasterror = "Authentication required"; 200 return false; 201 }, 202 Login.SetCompression.ID: (ubyte[] buffer){ 203 stream = new CompressedStream!varuint(stream, Login.SetCompression.fromBuffer!false(buffer).thresold); 204 return true; 205 } 206 ]; 207 ubyte[] buffer; 208 do { 209 buffer = stream.receive(); 210 if(buffer.length) { 211 auto del = buffer[0] in expected; 212 if(del) { 213 (*del)(buffer[1..$]); 214 expected.remove(buffer[0]); 215 } else { 216 break; 217 } 218 } else { 219 this._lasterror = "Unexpected empty packet"; 220 } 221 } while(buffer.length); 222 if(buffer[0] == Login.LoginSuccess.ID) { 223 auto ls = Login.LoginSuccess.fromBuffer(buffer); 224 if(ls.username == this.name) { 225 this.uuid = parseUUID(ls.uuid); //TODO may throw an exception 226 import std.concurrency; 227 spawn(&startGameLoop, cast(shared)stream, cast(shared)handler); 228 return stream; 229 } else { 230 this._lasterror = "Username mismatch (" ~ this.name ~ " != " ~ ls.username ~ ")"; 231 } 232 } else { 233 this._lasterror = "Unexpected packet " ~ to!string(buffer[0]) ~ " when expecting " ~ to!string(expected.keys)[1..$-1]; 234 } 235 socket.close(); 236 return null; 237 } 238 239 private static void startGameLoop(shared Stream _stream, shared IHandler _handler) { 240 auto stream = cast()_stream; 241 auto handler = cast()_handler; 242 stream.socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"msecs"(0)); // connection is closed when socket is 243 while(true) { 244 auto packet = stream.receive(); 245 if(packet.length) { 246 if(packet[0] == Clientbound.KeepAlive.ID) { 247 stream.send(new Serverbound.KeepAlive(Clientbound.KeepAlive.fromBuffer(packet).id).encode()); 248 } else { 249 handler.handle(packet); 250 if(packet[0] == Clientbound.Disconnect.ID) { 251 break; 252 } 253 } 254 } else { 255 // socket closed or packet with length 0 256 break; 257 } 258 } 259 } 260 261 } 262 263 private JSONValue postJSON(string url, JSONValue json) { 264 HTTP http = HTTP(); 265 http.addRequestHeader("Content-Type", "application/json"); 266 return parseJSON(post(url, json.toString(), http).idup); 267 } 268 269 private string chatToString(JSONValue json) { 270 if(json.type == JSON_TYPE.OBJECT) { 271 string ret; 272 auto text = "text" in json; 273 if(text && text.type == JSON_TYPE.STRING) { 274 ret ~= text.str; 275 } 276 auto extra = "extra" in json; 277 if(extra && extra.type == JSON_TYPE.ARRAY) { 278 foreach(element ; extra.array) { 279 if(element.type == JSON_TYPE.OBJECT) { 280 ret ~= chatToString(element); 281 } 282 } 283 } 284 return ret; 285 } else if(json.type == JSON_TYPE.STRING) { 286 return json.str; 287 } 288 return ""; 289 }