1 /*
2  * Copyright (c) 2017-2018 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 module config;
24 
25 import std.ascii : newline;
26 import std.conv : to, ConvException;
27 import std.file : exists, read, write, remove, tempDir, mkdirRecurse;
28 import std.json : JSONValue;
29 import std.path : dirSeparator, buildNormalizedPath;
30 import std.socket : Address;
31 import std..string : replace, split, join, toLower, toUpper, startsWith, endsWith;
32 import std.traits : isArray, isAssociativeArray, isIntegral, isFloatingPoint;
33 import std.typetuple : TypeTuple;
34 import std.uuid : UUID, parseUUID;
35 import std.zip : ZipArchive;
36 
37 import selery.about;
38 import selery.config : Config, Files;
39 import selery.lang : LanguageManager;
40 import selery.plugin : Plugin;
41 
42 import toml;
43 import toml.json;
44 
45 enum bool portable = __traits(compiles, import("portable.zip"));
46 
47 enum ConfigType : string {
48 
49 	default_ = "default",
50 	hub = "hub",
51 	node = "node"
52 
53 }
54 
55 mixin({
56 	string[] commands;
57 	foreach(member ; __traits(allMembers, Config.Node)) {
58 		static if(member.endsWith("Command")) commands ~= (`"` ~ member[0..$-7] ~ `"`);
59 	}
60 	return "alias Commands = TypeTuple!(" ~ commands.join(",") ~ ");";
61 }());
62 
63 auto loadConfig(ConfigType type, ref string[] args) {
64 
65 	immutable filename = (){
66 		final switch(type) with(ConfigType) {
67 			case default_: return "selery.toml";
68 			case hub: return "selery.hub.toml";
69 			case node: return "selery.node.toml";
70 		}
71 	}();
72 	
73 	immutable isHub = type == ConfigType.default_ || type == ConfigType.hub;
74 	immutable isNode = type == ConfigType.default_ || type == ConfigType.node;
75 	
76 	bool hasArg(string a) {
77 		foreach(i, arg; args) {
78 			if(arg == a) {
79 				args = args[0..i] ~ args[i+1..$];
80 				return true;
81 			}
82 		}
83 		return false;
84 	}
85 
86 	auto config = new class Config {
87 	
88 		private string language;
89 	
90 		public override void load() {
91 		
92 			version(Windows) {
93 				import std.utf : toUTF8;
94 				import std..string : fromStringz;
95 				import core.sys.windows.winnls;
96 				wchar[] lang = new wchar[3];
97 				wchar[] country = new wchar[3];
98 				GetLocaleInfo(GetUserDefaultUILanguage(), LOCALE_SISO639LANGNAME, lang.ptr, 3);
99 				GetLocaleInfo(GetUserDefaultUILanguage(), LOCALE_SISO3166CTRYNAME, country.ptr, 3);
100 				this.language = fromStringz(toUTF8(lang).ptr) ~ "_" ~ fromStringz(toUTF8(country).ptr);
101 			} else {
102 				import std.process : environment;
103 				this.language = environment.get("LANGUAGE", environment.get("LANG", "en_US"));
104 			}
105 		
106 			this.reload();
107 		
108 			immutable temp = buildNormalizedPath(tempDir() ~ dirSeparator ~ "selery" ~ dirSeparator ~ this.uuid.toString().toUpper());
109 			mkdirRecurse(temp);
110 			
111 			static if(portable) {
112 				
113 				this.files = new CompressedFiles(new ZipArchive(cast(void[])import("portable.zip")), temp);
114 				
115 			} else {
116 	
117 				this.files = new Files("assets", temp);
118 				
119 			}
120 			
121 			this.lang = new LanguageManager(this.files, this.language);
122 			this.lang.load();
123 		
124 		}
125 	
126 		public override void reload() {
127 		
128 			TOMLDocument document;
129 	
130 			if(exists(filename)) document = parseTOML(cast(string)read(filename));
131 				
132 			T get(T)(TOMLValue target) {
133 				static if(is(T == string)) {
134 					return target.str;
135 				} else static if(isArray!T) {
136 					T ret;
137 					foreach(value ; target.array) {
138 						ret ~= get!(typeof(ret[0]))(value);
139 					}
140 					return ret;
141 				} else static if(isAssociativeArray!T) {
142 					T ret;
143 					foreach(key, value; target.table) {
144 						ret[key] = get!(typeof(ret[""]))(value);
145 					}
146 					return ret;
147 				} else static if(is(T == bool)) {
148 					return target.type == TOML_TYPE.TRUE;
149 				} else static if(isFloatingPoint!T) {
150 					return cast(T)target.floating;
151 				} else static if(isIntegral!T) {
152 					return cast(T)target.integer;
153 				} else static if(is(T == UUID)) {
154 					return parseUUID(get!string(target));
155 				} else static if(is(T == JSONValue)) {
156 					return toJSON(target); //TODO handle conversion errors
157 				} else static if(is(T == Config.Hub.Address)) {
158 					return convertAddress(target.str);
159 				} else {
160 					static assert(0);
161 				}
162 			}
163 			
164 			TOMLValue getValue(TOMLValue[string] table, const(string)[] keys) {
165 				auto value = keys[0] in table;
166 				if(value) {
167 					if(keys.length == 1) return *value;
168 					else return getValue((*value).table, keys[1..$]); // throws exception if not a table
169 				} else {
170 					throw new TOMLException(keys[0] ~ " not in table");
171 				}
172 			}
173 			
174 			void set(T)(ref T value, const(string)[] keys...) {
175 				static if(is(T == string) || isIntegral!T || isFloatingPoint!T || is(T == bool) || isArray!T) {
176 					// override using --key=value
177 					immutable option = "--" ~ keys.join("-");
178 					foreach(i, arg; args) {
179 						if(arg.startsWith(option ~ "=")) {
180 							args = args[0..i] ~ args[i+1..$];
181 							try {
182 								immutable data = arg[option.length+1..$];
183 								static if(isArray!T && !is(T == string)) {
184 									T _value;
185 									alias A = typeof(_value[0]);
186 									foreach(s_data ; split(data, ",")) {
187 										static if(is(A == Config.Hub.Address)) _value ~= convertAddress(s_data);
188 										else _value ~= to!A(s_data);
189 									}
190 									value = _value;
191 								} else {
192 									value = to!T(data);
193 								}
194 							} catch(ConvException) {}
195 							return;
196 						}
197 					}
198 				}
199 				try {
200 					value = get!T(getValue(document.table, keys));
201 				} catch(TOMLException) {}
202 			}
203 			
204 			void setProtocols(ref uint[] value, uint[] all, uint[] latest, const(string)[] keys...) {
205 				string s;
206 				set(s, keys);
207 				if(s == "all" || s == "*") value = all;
208 				else if(s == "latest") value = latest;
209 				else set(value, keys);
210 			}
211 
212 			set(this.uuid, "uuid");
213 			set(this.language, "language");
214 			
215 			if(isHub) with(this.hub = new Config.Hub()) {
216 			
217 				set(displayName, "display-name");
218 				set(edu, "edu");
219 				set(bedrock.enabled, "bedrock", "enabled");
220 				set(bedrock.motd, "bedrock", "motd");
221 				set(bedrock.addresses, "bedrock", "addresses");
222 				setProtocols(bedrock.protocols, supportedBedrockProtocols, latestBedrockProtocols, "bedrock", "accepted-protocols");
223 				set(allowVanillaPlayers, "bedrock", "allow-vanilla-players");
224 				set(java.enabled, "java", "enabled");
225 				set(java.motd, "java", "motd");
226 				set(java.addresses, "java", "addresses");
227 				setProtocols(java.protocols, supportedJavaProtocols, latestJavaProtocols, "java", "accepted-protocols");
228 				set(query, "query-enabled");
229 				set(serverIp, "server-ip");
230 				set(favicon, "favicon");
231 				set(acceptedNodes, "hncom", "accepted-addresses");
232 				set(hncomPassword, "hncom", "password");
233 				set(maxNodes, "hncom", "node-limit");
234 				set(hncomPort, "hncom", "port");
235 				set(social, "social");
236 				
237 				// unlimited nodes
238 				string unlimited;
239 				set(unlimited, "hncom", "node-limit");
240 				if(unlimited.toLower() == "unlimited") maxNodes = 0;
241 				
242 			}
243 			
244 			if(isNode) with(this.node = new Config.Node()) {
245 			
246 				// override default
247 				transferCommand = type != ConfigType.default_;
248 			
249 				set(name, "hub", "name");
250 				set(password, "hub", "password");
251 				set(ip, "hub", "ip");
252 				set(port, "hub", "port");
253 				set(main, "hub", "main");
254 				set(bedrock.enabled, "bedrock", "enabled");
255 				setProtocols(bedrock.protocols, supportedBedrockProtocols, latestBedrockProtocols, "bedrock", "accepted-protocols");
256 				set(java.enabled, "java", "enabled");
257 				setProtocols(java.protocols, supportedJavaProtocols, latestJavaProtocols, "java", "accepted-protocols");
258 				set(maxPlayers, "max-players");
259 				set(gamemode, "world", "gamemode");
260 				set(difficulty, "world", "difficulty");
261 				set(depleteHunger, "world", "deplete-hunger");
262 				set(doDaylightCycle, "world", "do-daylight-cycle");
263 				set(doEntityDrops, "world", "do-entity-drops");
264 				set(doFireTick, "world", "do-fire-tick");
265 				set(doScheduledTicks, "world", "do-scheduled-ticks");
266 				set(doWeatherCycle, "world", "do-weather-cycle");
267 				set(naturalRegeneration, "natural-regeneration");
268 				set(pvp, "world", "pvp");
269 				set(randomTickSpeed, "world", "random-tick-speed");
270 				set(viewDistance, "view-distance");
271 				
272 				// commands
273 				foreach(command ; Commands) {
274 					set(mixin(command ~ "Command"), "command", command);
275 				}
276 				
277 				// unlimited players
278 				string unlimited;
279 				set(unlimited, "max-players");
280 				if(unlimited.toLower() == "unlimited") maxPlayers = 0;
281 				
282 			}
283 			
284 			if(!exists(filename)) this.save();
285 		
286 		}
287 		
288 		public override void save() {
289 		
290 			string serializeProtocols(uint[] protocols, uint[] all, uint[] latest) {
291 				if(protocols == latest) return `"latest"`;
292 				else if(protocols == all) return `"all"`;
293 				else return to!string(protocols);
294 			}
295 		
296 			// is this needed?
297 			if(this.hub is null) this.hub = new Config.Hub();
298 			if(this.node is null) this.node = new Config.Node();
299 		
300 			string file = "# " ~ Software.name ~ " " ~ Software.fullVersion ~ " configuration file" ~ newline ~ newline;
301 			
302 			file ~= "uuid = \"" ~ this.uuid.toString().toUpper() ~ "\"" ~ newline;
303 			if(isHub) file ~= "display-name = \"" ~ this.hub.displayName ~ "\"" ~ newline;
304 			if(isNode) file ~= "max-players = " ~ (this.node.maxPlayers == 0 ? "\"unlimited\"" : to!string(this.node.maxPlayers)) ~ newline;
305 			file ~= "language = \"" ~ this.language ~ "\"" ~ newline;
306 			if(isHub) file ~= "server-ip = \"" ~ this.hub.serverIp ~ "\"" ~ newline;
307 			if(isHub) file ~= "query-enabled = " ~ to!string(this.hub.query) ~ newline;
308 			if(isHub && !this.hub.edu) file ~= "favicon = \"" ~ this.hub.favicon ~ "\"" ~ newline;
309 			if(isHub) file ~= "social = {}" ~ newline; //TODO
310 			if(isHub) with(this.hub.bedrock) {
311 				file ~= newline ~ "[bedrock]" ~ newline;
312 				file ~= "enabled = " ~ to!string(enabled) ~ newline;
313 				file ~= "motd = \"" ~ motd ~ "\"" ~ newline;
314 				file ~= "online-mode = false" ~ newline;
315 				file ~= "addresses = " ~ addressString(addresses) ~ newline;
316 				file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedBedrockProtocols, latestBedrockProtocols) ~ newline;
317 				if(this.hub.edu) file ~= newline ~ "allow-vanilla-players = " ~ to!string(this.hub.allowVanillaPlayers);
318 			}
319 			if(isHub && !this.hub.edu) with(this.hub.java) {
320 				file ~= newline ~ "[java]" ~ newline;
321 				file ~= "enabled = " ~ to!string(enabled) ~ newline;
322 				file ~= "motd = \"" ~ motd ~ "\"" ~ newline;
323 				file ~= "online-mode = false" ~ newline;
324 				file ~= "addresses = " ~ addressString(addresses) ~ newline;
325 				file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedJavaProtocols, latestJavaProtocols) ~ newline;
326 			}
327 			if(type == ConfigType.node) with(this.node) {
328 				file ~= newline ~ "[hub]" ~ newline;
329 				file ~= "name = \"" ~ name ~ "\"" ~ newline;
330 				file ~= "password = \"" ~ password ~ "\"" ~ newline;
331 				file ~= "ip = \"" ~ ip ~ "\"" ~ newline;
332 				file ~= "port = " ~ to!string(port) ~ newline;
333 				file ~= "main = " ~ to!string(main) ~ newline;
334 			}
335 			if(type == ConfigType.node) with(this.node.bedrock) {
336 				file ~= newline ~ "[bedrock]" ~ newline;
337 				file ~= "enabled = " ~ to!string(enabled) ~ newline;
338 				file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedBedrockProtocols, latestBedrockProtocols) ~ newline;
339 			}
340 			if(type == ConfigType.node) with(this.node.java) {
341 				file ~= newline ~ "[java]" ~ newline;
342 				file ~= "enabled = " ~ to!string(enabled) ~ newline;
343 				file ~= "accepted-protocols = " ~ serializeProtocols(protocols, supportedJavaProtocols, latestJavaProtocols) ~ newline;
344 			}
345 			if(isNode) with(this.node) {
346 				file ~= newline ~ "[world]" ~ newline;
347 				file ~= "gamemode = " ~ to!string(gamemode) ~ newline;
348 				file ~= "difficulty = " ~ to!string(difficulty) ~ newline;
349 				file ~= "deplete-hunger = " ~ to!string(depleteHunger) ~ newline;
350 				file ~= "do-daylight-cycle = " ~ to!string(doDaylightCycle) ~ newline;
351 				file ~= "do-entity-drops = " ~ to!string(doEntityDrops) ~ newline;
352 				file ~= "do-fire-tick = " ~ to!string(doFireTick) ~ newline;
353 				file ~= "do-scheduled-ticks = " ~ to!string(doScheduledTicks) ~ newline;
354 				file ~= "do-weather-cycle = " ~ to!string(doWeatherCycle) ~ newline;
355 				file ~= "natural-regeneration = " ~ to!string(naturalRegeneration) ~ newline;
356 				file ~= "pvp = " ~ to!string(pvp) ~ newline;
357 				file ~= "random-tick-speed = " ~ to!string(randomTickSpeed) ~ newline;
358 				file ~= "view-distance = " ~ to!string(viewDistance) ~ newline;
359 			}
360 			if(isNode) with(this.node) {
361 				file ~= newline ~ "[command]" ~ newline;
362 				foreach(command ; Commands) {
363 					file ~= command ~ " = " ~ to!string(mixin(command ~ "Command")) ~ newline;
364 				}
365 			}
366 			if(type == ConfigType.hub) with(this.hub) {
367 				file ~= newline ~ "[hncom]" ~ newline;
368 				file ~= "accepted-addresses = " ~ to!string(acceptedNodes) ~ newline;
369 				file ~= "password = \"" ~ hncomPassword ~ "\"" ~ newline;
370 				file ~= "node-limit = " ~ (maxNodes == 0 ? "\"unlimited\"" : to!string(maxNodes)) ~ newline;
371 				file ~= "port = " ~ to!string(hncomPort);
372 				file ~= newline;
373 			}
374 			
375 			write(filename, file);
376 		
377 		}
378 		
379 	};
380 	
381 	if(hasArg("--reset") || hasArg("-r")) remove(filename);
382 	
383 	config.load();
384 	
385 	if(hasArg("--update-config") || hasArg("-uc")) config.save();
386 	
387 	return config;
388 
389 }
390 
391 class ReleaseFiles : Files {
392 
393 	public this(string assets, string temp) {
394 		super(assets, temp);
395 	}
396 	
397 	public override inout bool hasPluginAsset(Plugin plugin, string file) {
398 		return exists(this.assets ~ "plugins" ~ dirSeparator ~ plugin.name ~ dirSeparator ~ file);
399 	}
400 	
401 	public override inout void[] readPluginAsset(Plugin plugin, string file) {
402 		return read(this.assets ~ "plugins" ~ dirSeparator ~ plugin.name ~ dirSeparator ~ file);
403 	}
404 
405 }
406 
407 class CompressedFiles : Files {
408 	
409 	private ZipArchive archive;
410 	
411 	public this(ZipArchive archive, string temp) {
412 		super("", temp);
413 		this.archive = archive;
414 	}
415 	
416 	public override inout bool hasAsset(string file) {
417 		return !!(convert(file) in (cast()this.archive).directory);
418 	}
419 	
420 	public override inout void[] readAsset(string file) {
421 		auto member = (cast()this.archive).directory[convert(file)];
422 		if(member.expandedData.length != member.expandedSize) (cast()this.archive).expand(member);
423 		return cast(void[])member.expandedData;
424 	}
425 	
426 	public override inout bool hasPluginAsset(Plugin plugin, string file) {
427 		return this.hasAsset("plugins/" ~ plugin.name ~ "/" ~ file);
428 	}
429 	
430 	public override inout void[] readPluginAsset(Plugin plugin, string file) {
431 		return this.readAsset("plugins/" ~ plugin.name ~ "/" ~ file);
432 	}
433 	
434 	private static string convert(string file) {
435 		version(Windows) file = file.replace("\\", "/");
436 		while(file[$-1] == '/') file = file[0..$-1];
437 		return file;
438 	}
439 	
440 }
441 
442 /**
443  * Throws: ConvException
444  */
445 Config.Hub.Address convertAddress(string str) {
446 	Config.Hub.Address address;
447 	auto s = str.split(":");
448 	if(s.length >= 2) {
449 		address.port = to!ushort(s[$-1]);
450 		address.ip = s[0..$-1].join(":");
451 		if(address.ip.startsWith("[")) address.ip = address.ip[1..$];
452 		if(address.ip.endsWith("]")) address.ip = address.ip[0..$-1];
453 	}
454 	return address;
455 }
456 
457 string addressString(Config.Hub.Address[] addresses) {
458 	string[] ret;
459 	foreach(address ; addresses) {
460 		ret ~= address.toString();
461 	}
462 	return to!string(ret);
463 }