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