1 /*
2  * Copyright (c) 2017-2018 SEL
3  * 
4  * This program is free software: you can redistribute it and/or modify
5  * it under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12  * See the GNU Lesser General Public License for more details.
13  * 
14  */
15 /**
16  * Copyright: 2017-2020 sel-project
17  * License: LGPL-3.0
18  * Authors: Kripth
19  * Source: $(HTTP github.com/sel-project/sel-server/sel/server/query.d, sel/server/query.d)
20  */
21 module sel.server.query;
22 
23 import std.array : Appender;
24 import std.bitmanip : nativeToLittleEndian;
25 import std.conv : to;
26 import std.datetime.stopwatch : StopWatch;
27 import std.string : join;
28 
29 import sel.server.server;
30 
31 class Query {
32 
33 	private shared ServerInfo info;
34 
35 	public shared string software = "sel-server";
36 	public shared string[] plugins;
37 
38 	public shared string[] players;
39 
40 	public shared this(shared ServerInfo info) {
41 		this.info = info;
42 	}
43 
44 	public class Handler {
45 
46 		private immutable string game;
47 		private immutable string ip;
48 		private immutable ushort port;
49 
50 		private int challengeToken = 1;
51 		private string[int] sessions;
52 		private StopWatch sessionTimer;
53 
54 		private immutable(ubyte)[] basicQuery, fullQuery;
55 		private StopWatch basicTimer, fullTimer;
56 		private long lastBasic = long.min, lastFull = long.min;
57 
58 		public this(string game, string ip, ushort port) {
59 			this.game = game;
60 			this.ip = ip;
61 			this.port = port;
62 			this.sessionTimer.start();
63 			this.basicTimer.start();
64 			this.fullTimer.start();
65 		}
66 		
67 		public ubyte[] handle(ubyte[] payload) {
68 			if(payload.length >= 5) {
69 				if(payload[0] == 0) {
70 					// query request
71 					if(payload.length == 13) {
72 						mixin(createHandler("full", 5_000));
73 					} else {
74 						mixin(createHandler("basic", 1_000));
75 					}
76 				} else if(payload[0] == 9) {
77 					// login
78 					if(this.sessionTimer.peek().split!"msecs"().msecs > 30_000) {
79 						this.sessions.clear();
80 						this.sessionTimer.reset();
81 					}
82 					this.sessions[this.challengeToken] = ""; //TODO store the address
83 					return ubyte(9) ~ payload[1..5] ~ cast(ubyte[])to!string(this.challengeToken++) ~ ubyte(0);
84 				}
85 
86 			}
87 			return [];
88 		}
89 
90 		private void regenerateBasicQuery() {
91 			auto appender = createAppender();
92 			appender.put(info.motd.raw);
93 			appender.put(info.gametype);
94 			appender.put(info.map);
95 			appender.put(to!string(info.online));
96 			appender.put(to!string(info.max));
97 			appender.put(cast(ubyte[])nativeToLittleEndian(this.port));
98 			appender.put(this.ip);
99 			this.basicQuery = appender.data.idup;
100 		}
101 
102 		private void regenerateFullQuery() {
103 			auto appender = createAppender();
104 			appender.put("splitnum", "\x80");
105 			appender.put("hostname", info.motd.raw);
106 			appender.put("gametype", info.gametype);
107 			appender.put("game_id", this.game);
108 			appender.put("version", "?");
109 			appender.put("plugins", software ~ (plugins.length ? ": " ~ plugins.join("; ") : ""));
110 			appender.put("map", info.map);
111 			appender.put("numplayers", to!string(info.online));
112 			appender.put("maxplayers", to!string(info.max));
113 			appender.put("hostport", to!string(this.port));
114 			appender.put("hostip", this.ip);
115 			appender.put("\0\1player_\0");
116 			foreach(player ; players) {
117 				appender.put(player);
118 			}
119 			appender.appender.put(ubyte(0));
120 			this.fullQuery = appender.data.idup;
121 		}
122 
123 	}
124 
125 }
126 
127 private auto createAppender() {
128 
129 	struct _ {
130 
131 		public Appender!(ubyte[]) appender;
132 
133 		void put(ubyte[] bytes) {
134 			this.appender.put(bytes);
135 		}
136 
137 		void put(string value) {
138 			this.appender.put(cast(ubyte[])value);
139 			this.appender.put(ubyte(0));
140 		}
141 
142 		void put(string key, string value) {
143 			this.put(key);
144 			this.put(value);
145 		}
146 
147 		alias appender this;
148 
149 	}
150 
151 	return _();
152 
153 }
154 
155 private string createHandler(string type, uint time) {
156 	import std.string : capitalize;
157 	return "
158 		immutable peek = this." ~ type ~ "Timer.peek().split!`msecs`().msecs;
159 		if(peek > this.last" ~ capitalize(type) ~ " + " ~ to!string(time) ~ ") {
160 			this.regenerate" ~ capitalize(type) ~ "Query();
161 			this.last" ~ capitalize(type) ~ " = peek;
162 			this." ~ type ~ "Timer.reset();
163 		}
164 		return this." ~ type ~ "Query.dup;
165 	";
166 }