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 }