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 }