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/query.d, sel/client/query.d)
28  */
29 module sel.client.query;
30 
31 import std.bitmanip : nativeToBigEndian, peek;
32 import std.conv : to;
33 import std.datetime : dur;
34 import std.datetime.stopwatch : StopWatch;
35 import std.socket;
36 import std.string : indexOf, lastIndexOf, strip, split;
37 import std.system : Endian;
38 
39 import sel.client.util : Server;
40 
41 enum QueryType { basic,	full }
42 
43 const(Query) query(Address address, QueryType type=QueryType.full) {
44 	Socket socket = new UdpSocket(address.addressFamily);
45 	socket.setOption(SocketOptionLevel.SOCKET, SocketOption.REUSEADDR, true);
46 	socket.setOption(SocketOptionLevel.SOCKET, SocketOption.RCVTIMEO, dur!"seconds"(4));
47 	ubyte[] buffer = new ubyte[4096];
48 	ptrdiff_t recv;
49 	socket.sendTo(cast(ubyte[])[254, 253, 9, 0, 0, 0, 0], address);
50 	if((recv = socket.receiveFrom(buffer, address)) >= 7 && buffer[0..5] == [9, 0, 0, 0, 0] && buffer[recv-1] == 0) {
51 		StopWatch timer;
52 		timer.start();
53 		socket.sendTo(cast(ubyte[])[254, 253, 0, 0, 0, 0, 0] ~ nativeToBigEndian(to!int(cast(string)buffer[5..recv-1])) ~ new ubyte[type==QueryType.full?4:0], address);
54 		if((recv = socket.receiveFrom(buffer, address)) > 5 && buffer[0..5] == [0, 0, 0, 0, 0]) {
55 			uint latency;
56 			timer.stop();
57 			timer.peek.split!"msecs"(latency);
58 			size_t index = 5;
59 			string readString() {
60 				size_t next = index;
61 				while(next < buffer.length && buffer[next] != 0) next++;
62 				string ret = cast(string)buffer[index..next++].dup;
63 				index = next;
64 				return ret;
65 			}
66 			if(type == QueryType.basic) {
67 				string motd = readString();
68 				string gametype = readString();
69 				string map = readString();
70 				string online = readString();
71 				string max = readString();
72 				ushort port = peek!(ushort, Endian.littleEndian)(buffer, &index);
73 				string ip = readString();
74 				return Query(Server(motd, 0, to!int(online), to!int(max), latency), ip, port, gametype, map);
75 			} else {
76 				string[string] data;
77 				string next;
78 				while((next = readString()).length) {
79 					data[next] = readString();
80 				}
81 				auto motd = "hostname" in data;
82 				auto gametype = "gametype" in data;
83 				auto map = "map" in data;
84 				auto online = "numplayers" in data;
85 				auto max = "maxplayers" in data;
86 				auto port = "hostport" in data;
87 				auto ip = "hostip" in data;
88 				if(motd && online && max && port && ip) {
89 					Query ret = Query(Server(*motd, 0, to!int(*online), to!int(*max), latency), *ip, to!ushort(*port), gametype ? *gametype : "SMP", map ? *map : "");
90 					auto plugins = "plugins" in data;
91 					if(plugins) {
92 						ptrdiff_t i = indexOf(*plugins, ":");
93 						if(i == -1) {
94 							ret.software = strip(*plugins);
95 						} else {
96 							ret.software = strip((*plugins)[0..i]);
97 							foreach(plugin ; split((*plugins)[i+1..$], ";")) {
98 								i = plugin.lastIndexOf(" ");
99 								if(i == -1) {
100 									ret.plugins ~= Plugin(plugin.strip);
101 								} else {
102 									ret.plugins ~= Plugin(strip(plugin[0..i]), strip(plugin[i+1..$]));
103 								}
104 							}
105 						}
106 					}
107 					if(index + 10 < recv && buffer[index..index+10] == "\u0001player_\0\0") {
108 						index += 10;
109 						while((next = readString()).length) {
110 							ret.players ~= next;
111 						}
112 					}
113 					return ret;
114 				}
115 			}
116 		}
117 	}
118 	return Query.init;
119 }
120 
121 /// ditto
122 const(Query) query(string ip, ushort port, QueryType type=QueryType.full) {
123 	return query(new InternetAddress(ip, port), type);
124 }
125 
126 /// ditto
127 const(Query) javaQuery(string ip, QueryType type=QueryType.full) {
128 	return query(ip, ushort(25565), type);
129 }
130 
131 /// ditto
132 const(Query) pocketQuery(string ip, QueryType type=QueryType.full) {
133 	return query(ip, ushort(19132), type);
134 }
135 
136 struct Plugin {
137 
138 	string name, version_;
139 
140 }
141 
142 struct Query {
143 
144 	Server server;
145 
146 	string ip;
147 	ushort port;
148 
149 	string gametype;
150 	string map;
151 
152 	string software;
153 	Plugin[] plugins;
154 
155 	string[] players;
156 
157 	public inout string toString() {
158 		if(this.server.valid) {
159 			return "Query(" ~ this.server.toString()[7..$-1] ~ ", ip: " ~ this.ip ~ ", port: " ~ to!string(this.port) ~ ", gametype: " ~ this.gametype ~ ", map: " ~ this.map ~ ")";
160 		} else {
161 			return "Query()";
162 		}
163 	}
164 
165 	alias server this;
166 
167 }