diff --git a/server.js b/server.js index 28e03bd..5604386 100644 --- a/server.js +++ b/server.js @@ -1 +1,224 @@ -Y29uc3QgZXhwcmVzcyA9IHJlcXVpcmUoJ2V4cHJlc3MnKTsKY29uc3QgZmV0Y2ggPSByZXF1aXJlKCdub2RlLWZldGNoJyk7CmNvbnN0IHBhdGggPSByZXF1aXJlKCdwYXRoJyk7CmNvbnN0IGh0dHAgPSByZXF1aXJlKCdodHRwJyk7CmNvbnN0IGNyeXB0byA9IHJlcXVpcmUoJ2NyeXB0bycpOwpjb25zdCB7IFdlYlNvY2tldFNlcnZlciwgV2ViU29ja2V0IH0gPSByZXF1aXJlKCd3cycpOwpyZXF1aXJlKCdkb3RlbnYnKS5jb25maWcoKTsKCmNvbnN0IFZFUlNJT04gPSAnMS40LjAnOwpjb25zdCBhcHAgPSBleHByZXNzKCk7CmNvbnN0IHNlcnZlciA9IGh0dHAuY3JlYXRlU2VydmVyKGFwcCk7CmNvbnN0IFBPUlQgPSBwcm9jZXNzLmVudi5QT1JUIHx8IDMwMDA7CmNvbnN0IElNTUlDSF9VUkwgPSAocHJvY2Vzcy5lbnYuSU1NSUNIX1VSTCB8fCAnaHR0cDovL2xvY2FsaG9zdDoyMjgzJykucmVwbGFjZSgvXC8rJC8sICcnKTsKY29uc3QgQVBJX0tFWSA9IHByb2Nlc3MuZW52LklNTUlDSF9BUElfS0VZIHx8ICcnOwpjb25zdCBTTElERVNIT1dfSU5URVJWQUwgPSBwYXJzZUludChwcm9jZXNzLmVudi5TTElERVNIT1dfSU5URVJWQUwsIDEwKSB8fCAzMDsKY29uc3QgVFJBTlNJVElPTl9EVVJBVElPTiA9IHBhcnNlSW50KHByb2Nlc3MuZW52LlRSQU5TSVRJT05fRFVSQVRJT04sIDEwKSB8fCAyOwpjb25zdCBTSE9XX0NMT0NLID0gcHJvY2Vzcy5lbnYuU0hPV19DTE9DSyAhPT0gJ2ZhbHNlJzsKY29uc3QgU0hPV19EQVRFID0gcHJvY2Vzcy5lbnYuU0hPV19EQVRFICE9PSAnZmFsc2UnOwpjb25zdCBTSE9XX0VYSUYgPSBwcm9jZXNzLmVudi5TSE9XX0VYSUYgIT09ICdmYWxzZSc7CmNvbnN0IFNIT1dfUFJPR1JFU1MgPSBwcm9jZXNzLmVudi5TSE9XX1BST0dSRVNTICE9PSAnZmFsc2UnOwpjb25zdCBJTUFHRV9GSVQgPSBwcm9jZXNzLmVudi5JTUFHRV9GSVQgfHwgJ2NvbnRhaW4nOwpjb25zdCBCQUNLR1JPVU5EX0JMVVIgPSBwcm9jZXNzLmVudi5CQUNLR1JPVU5EX0JMVVIgIT09ICdmYWxzZSc7CmNvbnN0IFNIVUZGTEUgPSBwcm9jZXNzLmVudi5TSFVGRkxFICE9PSAnZmFsc2UnOwpjb25zdCBBTEJVTV9JRCA9IHByb2Nlc3MuZW52LkFMQlVNX0lEIHx8ICcnOwpjb25zdCBTSE9XX0ZBVk9SSVRFU19PTkxZID0gcHJvY2Vzcy5lbnYuU0hPV19GQVZPUklURVNfT05MWSA9PT0gJ3RydWUnOwpjb25zdCBSRUZSRVNIX0lOVEVSVkFMID0gcGFyc2VJbnQocHJvY2Vzcy5lbnYuUkVGUkVTSF9JTlRFUlZBTCwgMTApIHx8IDMwMDsKY29uc3QgSU5DTFVERV9WSURFT1MgPSBwcm9jZXNzLmVudi5JTkNMVURFX1ZJREVPUyAhPT0gJ2ZhbHNlJzsKCi8vIC0tLSBBdXRoIGNvbmZpZ3VyYXRpb24gLS0tCmNvbnN0IEFETUlOX1VTRVJOQU1FID0gcHJvY2Vzcy5lbnYuQURNSU5fVVNFUk5BTUUgfHwgJ2FkbWluJzsKY29uc3QgQURNSU5fUEFTU1dPUkQgPSBwcm9jZXNzLmVudi5BRE1JTl9QQVNTV09SRCB8fCAnJzsKY29uc3QgRlJBTUJFX0FQSV9UT0tFTiA9IHByb2Nlc3MuZW52LkZSQU1CRV9BUElfVE9LRU4gfHwgJyc7CmNvbnN0IEFVVEhfRU5BQkxFRCA9ICEhQURNSU5fUEFTU1dPUkQ7CgovLyBTZXNzaW9uIHN0b3JlOiB0b2tlbiAtPiB7IHVzZXJuYW1lLCBjcmVhdGVkQXQsIGV4cGlyZXNBdCB9CmNvbnN0IHNlc3Npb25zID0gbmV3IE1hcCgpOwpjb25zdCBTRVNTSU9OX1RUTCA9IDI0ICogNjAgKiA2MCAqIDEwMDA7IC8vIDI0IGhvdXJzCgpmdW5jdGlvbiBjcmVhdGVTZXNzaW9uKHVzZXJuYW1lKSB7CiAgY29uc3QgdG9rZW4gPSBjcnlwdG8ucmFuZG9tQnl0ZXMoMzIpLnRvU3RyaW5nKCdoZXgnKTsKICBjb25zdCBub3cgPSBEYXRlLm5vdygpOwogIHNlc3Npb25zLnNldCh0b2tlbiwgeyB1c2VybmFtZSwgY3JlYXRlZEF0OiBub3csIGV4cGlyZXNBdDogbm93ICsgU0VTU0lPTl9UVEwgfSk7CiAgcmV0dXJuIHRva2VuOwp9CgpmdW5jdGlvbiB2YWxpZGF0ZVNlc3Npb24odG9rZW4pIHsKICBpZiAoIXRva2VuKSByZXR1cm4gZmFsc2U7CiAgY29uc3Qgc2Vzc2lvbiA9IHNlc3Npb25zLmdldCh0b2tlbik7CiAgaWYgKCFzZXNzaW9uKSByZXR1cm4gZmFsc2U7CiAgaWYgKERhdGUubm93KCkgPiBzZXNzaW9uLmV4cGlyZXNBdCkgeyBzZXNzaW9ucy5kZWxldGUodG9rZW4pOyByZXR1cm4gZmFsc2U7IH0KICByZXR1cm4gdHJ1ZTsKfQoKZnVuY3Rpb24gY2xlYW51cFNlc3Npb25zKCkgeyBjb25zdCBub3cgPSBEYXRlLm5vdygpOyBzZXNzaW9ucy5mb3JFYWNoKChzLCB0KSA9PiB7IGlmIChub3cgPiBzLmV4cGlyZXNBdCkgc2Vzc2lvbnMuZGVsZXRlKHQpOyB9KTsgfQpzZXRJbnRlcnZhbChjbGVhbnVwU2Vzc2lvbnMsIDYwICogNjAgKiAxMDAwKTsgLy8gY2xlYW51cCBldmVyeSBob3VyCgovLyAtLS0gQWRtaW4gYXV0aCBtaWRkbGV3YXJlIChjb29raWUtYmFzZWQgZm9yIGJyb3dzZXIpIC0tLQpmdW5jdGlvbiByZXF1aXJlQWRtaW5BdXRoKHJlcSwgcmVzLCBuZXh0KSB7CiAgaWYgKCFBVVRIX0VOQUJMRUQpIHJldHVybiBuZXh0KCk7CiAgY29uc3QgY29va2llID0gcmVxLmhlYWRlcnMuY29va2llIHx8ICcnOwogIGNvbnN0IG1hdGNoID0gY29va2llLm1hdGNoKC9mcmFtYmVfc2Vzc2lvbj0oW2EtZjAtOV0rKS8pOwogIGNvbnN0IHRva2VuID0gbWF0Y2ggPyBtYXRjaFsxXSA6IG51bGw7CiAgaWYgKHZhbGlkYXRlU2Vzc2lvbih0b2tlbikpIHJldHVybiBuZXh0KCk7CiAgLy8gTm90IGF1dGhlbnRpY2F0ZWQg4oCUIGlmIHJlcXVlc3RpbmcgSFRNTCwgcmVkaXJlY3QgdG8gbG9naW47IG90aGVyd2lzZSA0MDEKICBpZiAocmVxLmFjY2VwdHMoJ2h0bWwnKSkgcmV0dXJuIHJlcy5yZWRpcmVjdCgnL2FkbWluL2xvZ2luJyk7CiAgcmV0dXJuIHJlcy5zdGF0dXMoNDAxKS5qc29uKHsgZXJyb3I6ICdVbmF1dGhvcml6ZWQnLCBtZXNzYWdlOiAnQWRtaW4gbG9naW4gcmVxdWlyZWQnIH0pOwp9CgovLyAtLS0gQVBJIHRva2VuIG1pZGRsZXdhcmUgKGZvciBleHRlcm5hbCBjYWxsZXJzIGxpa2UgSG9tZSBBc3Npc3RhbnQpIC0tLQpmdW5jdGlvbiByZXF1aXJlQXBpVG9rZW4ocmVxLCByZXMsIG5leHQpIHsKICAvLyBBY2NlcHQgZWl0aGVyOiBCZWFyZXIgdG9rZW4gaW4gQXV0aG9yaXphdGlvbiBoZWFkZXIsIG9yIHgtYXBpLXRva2VuIGhlYWRlciwgb3IgP3Rva2VuPSBxdWVyeSBwYXJhbQogIGNvbnN0IGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5hdXRob3JpemF0aW9uIHx8ICcnOwogIGNvbnN0IGJlYXJlclRva2VuID0gYXV0aEhlYWRlci5zdGFydHNXaXRoKCdCZWFyZXIgJykgPyBhdXRoSGVhZGVyLnNsaWNlKDcpIDogJyc7CiAgY29uc3QgaGVhZGVyVG9rZW4gPSByZXEuaGVhZGVyc1sneC1hcGktdG9rZW4nXSB8fCAnJzsKICBjb25zdCBxdWVyeVRva2VuID0gcmVxLnF1ZXJ5LnRva2VuIHx8ICcnOwogIGNvbnN0IHByb3ZpZGVkID0gYmVhcmVyVG9rZW4gfHwgaGVhZGVyVG9rZW4gfHwgcXVlcnlUb2tlbjsKCiAgLy8gSWYgQVBJIHRva2VuIGlzIGNvbmZpZ3VyZWQsIHJlcXVpcmUgaXQKICBpZiAoRlJBTUJFX0FQSV9UT0tFTikgewogICAgaWYgKHByb3ZpZGVkID09PSBGUkFNQkVfQVBJX1RPS0VOKSByZXR1cm4gbmV4dCgpOwogIH0KCiAgLy8gQWxzbyBhY2NlcHQgdmFsaWQgYWRtaW4gc2Vzc2lvbiBjb29raWUKICBpZiAoQVVUSF9FTkFCTEVEKSB7CiAgICBjb25zdCBjb29raWUgPSByZXEuaGVhZGVycy5jb29raWUgfHwgJyc7CiAgICBjb25zdCBtYXRjaCA9IGNvb2tpZS5tYXRjaCgvZnJhbWJlX3Nlc3Npb249KFthLWYwLTldKykvKTsKICAgIGlmIChtYXRjaCAmJiB2YWxpZGF0ZVNlc3Npb24obWF0Y2hbMV0pKSByZXR1cm4gbmV4dCgpOwogIH0KCiAgLy8gSWYgbmVpdGhlciBhdXRoIG1ldGhvZCBpcyBjb25maWd1cmVkLCBhbGxvdyBvcGVuIGFjY2VzcwogIGlmICghRlJBTUJFX0FQSV9UT0tFTiAmJiAhQVVUSF9FTkFCTEVEKSByZXR1cm4gbmV4dCgpOwoKICByZXR1cm4gcmVzLnN0YXR1cyg0MDEpLmpzb24oeyBlcnJvcjogJ1VuYXV0aG9yaXplZCcsIG1lc3NhZ2U6ICdWYWxpZCBBUEkgdG9rZW4gb3IgYWRtaW4gc2Vzc2lvbiByZXF1aXJlZCcgfSk7Cn0KCmZ1bmN0aW9uIGltbWljaEhlYWRlcnMoKSB7IHJldHVybiB7ICd4LWFwaS1rZXknOiBBUElfS0VZLCAnQWNjZXB0JzogJ2FwcGxpY2F0aW9uL2pzb24nLCAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH07IH0KZnVuY3Rpb24gbG9nKG1zZykgeyBjb25zb2xlLmxvZygnW0ZyYW1iZV0gJyArIG1zZyk7IH0KZnVuY3Rpb24gbG9nRXJyKG1zZykgeyBjb25zb2xlLmVycm9yKCdbRnJhbWJlXSBFUlJPUjogJyArIG1zZyk7IH0KCmNvbnN0IGNsaWVudHMgPSBuZXcgTWFwKCk7CmxldCBjbGllbnROYW1lU3RvcmUgPSB7fTsKZnVuY3Rpb24gZ2V0Q2xpZW50SXAocmVxKSB7IHJldHVybiByZXEuaGVhZGVyc1sneC1mb3J3YXJkZWQtZm9yJ10/LnNwbGl0KCcsJylbMF0udHJpbSgpIHx8IHJlcS5zb2NrZXQucmVtb3RlQWRkcmVzcyB8fCAndW5rbm93bic7IH0KZnVuY3Rpb24gZ2VuZXJhdGVDbGllbnRJZChpcCkgeyByZXR1cm4gaXAucmVwbGFjZSgvWy46XS9nLCAnXycpOyB9CmZ1bmN0aW9uIGJyb2FkY2FzdFRvQWRtaW5zKG1zZykgeyBjb25zdCBkID0gSlNPTi5zdHJpbmdpZnkobXNnKTsgY2xpZW50cy5mb3JFYWNoKGMgPT4geyBpZiAoYy5yb2xlID09PSAnYWRtaW4nICYmIGMud3MucmVhZHlTdGF0ZSA9PT0gV2ViU29ja2V0Lk9QRU4pIGMud3Muc2VuZChkKTsgfSk7IH0KZnVuY3Rpb24gZ2V0Q2xpZW50TGlzdCgpIHsgY29uc3QgbGlzdCA9IFtdOyBjbGllbnRzLmZvckVhY2goKGMsIGlkKSA9PiB7IGlmIChjLnJvbGUgPT09ICdmcmFtZScpIGxpc3QucHVzaCh7IGlkLCBpcDogYy5pcCwgbmFtZTogYy5uYW1lIHx8IGNsaWVudE5hbWVTdG9yZVtjLmlwXSB8fCAnJywgc3RhdHVzOiBjLnN0YXR1cyB8fCAndW5rbm93bicsIGNvbm5lY3RlZEF0OiBjLmNvbm5lY3RlZEF0LCBsYXN0U2VlbjogYy5sYXN0U2VlbiwgY29uZmlnOiBjLmNvbmZpZyB8fCB7fSB9KTsgfSk7IHJldHVybiBsaXN0OyB9Cgpjb25zdCB3c3MgPSBuZXcgV2ViU29ja2V0U2VydmVyKHsgc2VydmVyLCBwYXRoOiAnL3dzJyB9KTsKd3NzLm9uKCdjb25uZWN0aW9uJywgKHdzLCByZXEpID0+IHsKICBjb25zdCBpcCA9IGdldENsaWVudElwKHJlcSk7CiAgY29uc3QgY2xpZW50SWQgPSBnZW5lcmF0ZUNsaWVudElkKGlwKSArICdfJyArIERhdGUubm93KCk7CiAgbG9nKCdXZWJTb2NrZXQgY29ubmVjdGVkOiAnICsgaXAgKyAnICgnICsgY2xpZW50SWQgKyAnKScpOwogIGNvbnN0IGluZm8gPSB7IHdzLCBpcCwgcm9sZTogJ2ZyYW1lJywgbmFtZTogY2xpZW50TmFtZVN0b3JlW2lwXSB8fCAnJywgc3RhdHVzOiAnY29ubmVjdGVkJywgY29ubmVjdGVkQXQ6IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSwgbGFzdFNlZW46IG5ldyBEYXRlKCkudG9JU09TdHJpbmcoKSwgY29uZmlnOiB7fSB9OwogIGNsaWVudHMuc2V0KGNsaWVudElkLCBpbmZvKTsKICB3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHsgdHlwZTogJ3dlbGNvbWUnLCBjbGllbnRJZCwgbmFtZTogaW5mby5uYW1lIH0pKTsKICB3cy5vbignbWVzc2FnZScsIHJhdyA9PiB7CiAgICB0cnkgewogICAgICBjb25zdCBtc2cgPSBKU09OLnBhcnNlKHJhdyk7IGluZm8ubGFzdFNlZW4gPSBuZXcgRGF0ZSgpLnRvSVNPU3RyaW5nKCk7CiAgICAgIHN3aXRjaCAobXNnLnR5cGUpIHsKICAgICAgICBjYXNlICdyZWdpc3Rlcic6CiAgICAgICAgICBpbmZvLnJvbGUgPSBtc2cucm9sZSB8fCAnZnJhbWUnOwogICAgICAgICAgaWYgKG1zZy5yb2xlID09PSAnYWRtaW4nKSB7IGxvZygnQWRtaW4gY29ubmVjdGVkIGZyb20gJyArIGlwKTsgd3Muc2VuZChKU09OLnN0cmluZ2lmeSh7IHR5cGU6ICdjbGllbnRMaXN0JywgY2xpZW50czogZ2V0Q2xpZW50TGlzdCgpIH0pKTsgfQogICAgICAgICAgZWxzZSB7IGxvZygnRnJhbWUgcmVnaXN0ZXJlZDogJyArIGlwKTsgaW5mby5zdGF0dXMgPSBtc2cuc3RhdHVzIHx8ICdpZGxlJzsgaW5mby5jb25maWcgPSBtc2cuY29uZmlnIHx8IHt9OyBicm9hZGNhc3RUb0FkbWlucyh7IHR5cGU6ICdjbGllbnRMaXN0JywgY2xpZW50czogZ2V0Q2xpZW50TGlzdCgpIH0pOyB9CiAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICdzdGF0dXMnOgogICAgICAgICAgaW5mby5zdGF0dXMgPSBtc2cuc3RhdHVzIHx8IGluZm8uc3RhdHVzOyBpZiAobXNnLmNvbmZpZykgaW5mby5jb25maWcgPSBtc2cuY29uZmlnOwogICAgICAgICAgYnJvYWRjYXN0VG9BZG1pbnMoeyB0eXBlOiAnY2xpZW50VXBkYXRlJywgY2xpZW50SWQsIGNsaWVudDogeyBpZDogY2xpZW50SWQsIGlwOiBpbmZvLmlwLCBuYW1lOiBpbmZvLm5hbWUsIHN0YXR1czogaW5mby5zdGF0dXMsIGxhc3RTZWVuOiBpbmZvLmxhc3RTZWVuLCBjb25maWc6IGluZm8uY29uZmlnIH0gfSk7CiAgICAgICAgICBicmVhazsKICAgICAgICBjYXNlICdhZG1pbkNvbW1hbmQnOgogICAgICAgICAgY29uc3QgdGFyZ2V0ID0gY2xpZW50cy5nZXQobXNnLnRhcmdldElkKTsKICAgICAgICAgIGlmICh0YXJnZXQgJiYgdGFyZ2V0LndzLnJlYWR5U3RhdGUgPT09IFdlYlNvY2tldC5PUEVOKSB7IHRhcmdldC53cy5zZW5kKEpTT04uc3RyaW5naWZ5KHsgdHlwZTogJ2NvbW1hbmQnLCBhY3Rpb246IG1zZy5hY3Rpb24sIHBheWxvYWQ6IG1zZy5wYXlsb2FkIH0pKTsgbG9nKCdDb21tYW5kICcgKyBtc2cuYWN0aW9uICsgJyAtPiAnICsgbXNnLnRhcmdldElkKTsgfQogICAgICAgICAgZWxzZSB3cy5zZW5kKEpTT04uc3RyaW5naWZ5KHsgdHlwZTogJ2Vycm9yJywgbWVzc2FnZTogJ0NsaWVudCBub3QgZm91bmQnIH0pKTsKICAgICAgICAgIGJyZWFrOwogICAgICAgIGNhc2UgJ3JlbmFtZUNsaWVudCc6CiAgICAgICAgICBjb25zdCBydCA9IGNsaWVudHMuZ2V0KG1zZy50YXJnZXRJZCk7CiAgICAgICAgICBpZiAocnQpIHsgcnQubmFtZSA9IG1zZy5uYW1lOyBjbGllbnROYW1lU3RvcmVbcnQuaXBdID0gbXNnLm5hbWU7IGxvZygnUmVuYW1lZCAnICsgbXNnLnRhcmdldElkICsgJyAtPiAiJyArIG1zZy5uYW1lICsgJyInKTsgYnJvYWRjYXN0VG9BZG1pbnMoeyB0eXBlOiAnY2xpZW50TGlzdCcsIGNsaWVudHM6IGdldENsaWVudExpc3QoKSB9KTsgfQogICAgICAgICAgYnJlYWs7CiAgICAgIH0KICAgIH0gY2F0Y2ggKGUpIHsgbG9nRXJyKCdXUyBwYXJzZSBlcnJvcjogJyArIGUubWVzc2FnZSk7IH0KICB9KTsKICB3cy5vbignY2xvc2UnLCAoKSA9PiB7IGxvZygnV2ViU29ja2V0IGRpc2Nvbm5lY3RlZDogJyArIGlwKTsgY2xpZW50cy5kZWxldGUoY2xpZW50SWQpOyBicm9hZGNhc3RUb0FkbWlucyh7IHR5cGU6ICdjbGllbnRMaXN0JywgY2xpZW50czogZ2V0Q2xpZW50TGlzdCgpIH0pOyB9KTsKfSk7CgphcHAudXNlKCcvYXBpJywgKHJlcSwgX3JlcywgbmV4dCkgPT4geyBsb2coJ0FQSSAnICsgcmVxLm1ldGhvZCArICcgJyArIHJlcS5vcmlnaW5hbFVybCk7IG5leHQoKTsgfSk7CmFwcC51c2UoZXhwcmVzcy5qc29uKCkpOwoKLy8gLS0tIEF1dGggZW5kcG9pbnRzIC0tLQphcHAuZ2V0KCcvYXBpL2F1dGgvc3RhdHVzJywgKF9yZXEsIHJlcykgPT4gewogIHJlcy5qc29uKHsgYXV0aEVuYWJsZWQ6IEFVVEhfRU5BQkxFRCwgYXBpVG9rZW5FbmFibGVkOiAhIUZSQU1CRV9BUElfVE9LRU4gfSk7Cn0pOwoKYXBwLnBvc3QoJy9hcGkvYXV0aC9sb2dpbicsIChyZXEsIHJlcykgPT4gewogIGlmICghQVVUSF9FTkFCTEVEKSByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSwgbWVzc2FnZTogJ0F1dGggbm90IGVuYWJsZWQnIH0pOwogIGNvbnN0IHsgdXNlcm5hbWUsIHBhc3N3b3JkIH0gPSByZXEuYm9keSB8fCB7fTsKICBpZiAodXNlcm5hbWUgPT09IEFETUlOX1VTRVJOQU1FICYmIHBhc3N3b3JkID09PSBBRE1JTl9QQVNTV09SRCkgewogICAgY29uc3QgdG9rZW4gPSBjcmVhdGVTZXNzaW9uKHVzZXJuYW1lKTsKICAgIHJlcy5zZXRIZWFkZXIoJ1NldC1Db29raWUnLCBgZnJhbWJlX3Nlc3Npb249JHt0b2tlbn07IFBhdGg9LzsgSHR0cE9ubHk7IFNhbWVTaXRlPVN0cmljdDsgTWF4LUFnZT0ke1NFU1NJT05fVFRMIC8gMTAwMH1gKTsKICAgIGxvZygnQWRtaW4gbG9naW46ICcgKyB1c2VybmFtZSk7CiAgICByZXR1cm4gcmVzLmpzb24oeyBvazogdHJ1ZSB9KTsKICB9CiAgbG9nKCdGYWlsZWQgbG9naW4gYXR0ZW1wdDogJyArICh1c2VybmFtZSB8fCAnKGVtcHR5KScpKTsKICByZXR1cm4gcmVzLnN0YXR1cyg0MDEpLmpzb24oeyBvazogZmFsc2UsIGVycm9yOiAnSW52YWxpZCBjcmVkZW50aWFscycgfSk7Cn0pOwoKYXBwLnBvc3QoJy9hcGkvYXV0aC9sb2dvdXQnLCAocmVxLCByZXMpID0+IHsKICBjb25zdCBjb29raWUgPSByZXEuaGVhZGVycy5jb29raWUgfHwgJyc7CiAgY29uc3QgbWF0Y2ggPSBjb29raWUubWF0Y2goL2ZyYW1iZV9zZXNzaW9uPShbYS1mMC05XSspLyk7CiAgaWYgKG1hdGNoKSBzZXNzaW9ucy5kZWxldGUobWF0Y2hbMV0pOwogIHJlcy5zZXRIZWFkZXIoJ1NldC1Db29raWUnLCAnZnJhbWJlX3Nlc3Npb249OyBQYXRoPS87IEh0dHBPbmx5OyBNYXgtQWdlPTAnKTsKICByZXMuanNvbih7IG9rOiB0cnVlIH0pOwp9KTsKCi8vIC0tLSBMb2dpbiBwYWdlIChzZXJ2ZWQgd2l0aG91dCBhdXRoKSAtLS0KYXBwLmdldCgnL2FkbWluL2xvZ2luJywgKF9yZXEsIHJlcykgPT4gewogIGlmICghQVVUSF9FTkFCTEVEKSByZXR1cm4gcmVzLnJlZGlyZWN0KCcvYWRtaW4nKTsKICByZXMuc2VuZEZpbGUocGF0aC5qb2luKF9fZGlybmFtZSwgJ3B1YmxpYycsICdhZG1pbicsICdsb2dpbi5odG1sJykpOwp9KTsKCi8vIC0tLSBTdGF0aWMgZmlsZXMgKG5vbi1hZG1pbiBwYWdlcyBkb24ndCByZXF1aXJlIGF1dGgpIC0tLQphcHAudXNlKGV4cHJlc3Muc3RhdGljKHBhdGguam9pbihfX2Rpcm5hbWUsICdwdWJsaWMnKSwgeyBzZXRIZWFkZXJzOiAocmVzLCBmcCkgPT4geyBpZiAoZnAuZW5kc1dpdGgoJy5odG1sJykgfHwgZnAuZW5kc1dpdGgoJy5qcycpIHx8IGZwLmVuZHNXaXRoKCcuY3NzJykpIHsgcmVzLnNldEhlYWRlcignQ2FjaGUtQ29udHJvbCcsICduby1jYWNoZSwgbm8tc3RvcmUsIG11c3QtcmV2YWxpZGF0ZScpOyByZXMuc2V0SGVhZGVyKCdQcmFnbWEnLCAnbm8tY2FjaGUnKTsgcmVzLnNldEhlYWRlcignRXhwaXJlcycsICcwJyk7IH0gfSB9KSk7CgpmdW5jdGlvbiBtYXBBc3NldChhKSB7IHJldHVybiB7IGlkOiBhLmlkLCB0eXBlOiBhLnR5cGUsIG9yaWdpbmFsRmlsZU5hbWU6IGEub3JpZ2luYWxGaWxlTmFtZSwgZmlsZUNyZWF0ZWRBdDogYS5maWxlQ3JlYXRlZEF0LCBpc0Zhdm9yaXRlOiBhLmlzRmF2b3JpdGUsIGV4aWZJbmZvOiBhLmV4aWZJbmZvID8geyBtYWtlOiBhLmV4aWZJbmZvLm1ha2UsIG1vZGVsOiBhLmV4aWZJbmZvLm1vZGVsLCBjaXR5OiBhLmV4aWZJbmZvLmNpdHksIHN0YXRlOiBhLmV4aWZJbmZvLnN0YXRlLCBjb3VudHJ5OiBhLmV4aWZJbmZvLmNvdW50cnksIGRlc2NyaXB0aW9uOiBhLmV4aWZJbmZvLmRlc2NyaXB0aW9uLCBkYXRlVGltZU9yaWdpbmFsOiBhLmV4aWZJbmZvLmRhdGVUaW1lT3JpZ2luYWwgfSA6IG51bGwgfTsgfQpmdW5jdGlvbiBmaWx0ZXJBc3NldHMoYXNzZXRzKSB7IHJldHVybiBJTkNMVURFX1ZJREVPUyA/IGFzc2V0cy5maWx0ZXIoYSA9PiBhLnR5cGUgPT09ICdJTUFHRScgfHwgYS50eXBlID09PSAnVklERU8nKSA6IGFzc2V0cy5maWx0ZXIoYSA9PiBhLnR5cGUgPT09ICdJTUFHRScpOyB9CgphcHAuZ2V0KCcvYXBpL2NvbmZpZycsIChfcmVxLCByZXMpID0+IHsgcmVzLmpzb24oeyB2ZXJzaW9uOiBWRVJTSU9OLCBzbGlkZXNob3dJbnRlcnZhbDogU0xJREVTSE9XX0lOVEVSVkFMLCB0cmFuc2l0aW9uRHVyYXRpb246IFRSQU5TSVRJT05fRFVSQVRJT04sIHNob3dDbG9jazogU0hPV19DTE9DSywgc2hvd0RhdGU6IFNIT1dfREFURSwgc2hvd0V4aWY6IFNIT1dfRVhJRiwgc2hvd1Byb2dyZXNzOiBTSE9XX1BST0dSRVNTLCBpbWFnZUZpdDogSU1BR0VfRklULCBiYWNrZ3JvdW5kQmx1cjogQkFDS0dST1VORF9CTFVSLCBzaHVmZmxlOiBTSFVGRkxFLCBhbGJ1bUlkOiBBTEJVTV9JRCwgc2hvd0Zhdm9yaXRlc09ubHk6IFNIT1dfRkFWT1JJVEVTX09OTFksIHJlZnJlc2hJbnRlcnZhbDogUkVGUkVTSF9JTlRFUlZBTCwgaW5jbHVkZVZpZGVvczogSU5DTFVERV9WSURFT1MsIGNvbm5lY3RlZDogISFBUElfS0VZLCBhdXRoRW5hYmxlZDogQVVUSF9FTkFCTEVEIH0pOyB9KTsKYXBwLmdldCgnL2FwaS9zZXJ2ZXItaW5mbycsIGFzeW5jIChfcmVxLCByZXMpID0+IHsgdHJ5IHsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9zZXJ2ZXIvdmVyc2lvbmAsIHsgaGVhZGVyczogaW1taWNoSGVhZGVycygpIH0pOyBpZiAoIXIub2spIHRocm93IG5ldyBFcnJvcihgSW1taWNoIHJldHVybmVkICR7ci5zdGF0dXN9YCk7IGNvbnN0IHYgPSBhd2FpdCByLmpzb24oKTsgbG9nKCdJbW1pY2ggT0sgdicgKyB2Lm1ham9yICsgJy4nICsgdi5taW5vciArICcuJyArIHYucGF0Y2gpOyByZXMuanNvbih7IG9rOiB0cnVlLCB2ZXJzaW9uOiB2IH0pOyB9IGNhdGNoIChlKSB7IGxvZ0VycignSW1taWNoIGZhaWxlZDogJyArIGUubWVzc2FnZSk7IHJlcy5zdGF0dXMoNTAyKS5qc29uKHsgb2s6IGZhbHNlLCBlcnJvcjogZS5tZXNzYWdlIH0pOyB9IH0pOwphcHAuZ2V0KCcvYXBpL2FsYnVtcycsIGFzeW5jIChfcmVxLCByZXMpID0+IHsgdHJ5IHsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9hbGJ1bXNgLCB7IGhlYWRlcnM6IGltbWljaEhlYWRlcnMoKSB9KTsgaWYgKCFyLm9rKSB0aHJvdyBuZXcgRXJyb3IoYCR7ci5zdGF0dXN9YCk7IGNvbnN0IGEgPSBhd2FpdCByLmpzb24oKTsgbG9nKCdMaXN0ZWQgJyArIGEubGVuZ3RoICsgJyBhbGJ1bXMnKTsgcmVzLmpzb24oYS5tYXAoeCA9PiAoeyBpZDogeC5pZCwgYWxidW1OYW1lOiB4LmFsYnVtTmFtZSwgYXNzZXRDb3VudDogeC5hc3NldENvdW50LCBhbGJ1bVRodW1ibmFpbEFzc2V0SWQ6IHguYWxidW1UaHVtYm5haWxBc3NldElkLCB1cGRhdGVkQXQ6IHgudXBkYXRlZEF0IH0pKSk7IH0gY2F0Y2ggKGUpIHsgbG9nRXJyKCdBbGJ1bXM6ICcgKyBlLm1lc3NhZ2UpOyByZXMuc3RhdHVzKDUwMikuanNvbih7IGVycm9yOiBlLm1lc3NhZ2UgfSk7IH0gfSk7CmFwcC5nZXQoJy9hcGkvYWxidW1zLzppZCcsIGFzeW5jIChyZXEsIHJlcykgPT4geyB0cnkgeyBjb25zdCByID0gYXdhaXQgZmV0Y2goYCR7SU1NSUNIX1VSTH0vYXBpL2FsYnVtcy8ke3JlcS5wYXJhbXMuaWR9YCwgeyBoZWFkZXJzOiBpbW1pY2hIZWFkZXJzKCkgfSk7IGlmICghci5vaykgdGhyb3cgbmV3IEVycm9yKGAke3Iuc3RhdHVzfWApOyBjb25zdCBhbCA9IGF3YWl0IHIuanNvbigpOyBjb25zdCBhID0gZmlsdGVyQXNzZXRzKGFsLmFzc2V0cyB8fCBbXSkubWFwKG1hcEFzc2V0KTsgbG9nKCdBbGJ1bSAiJyArIGFsLmFsYnVtTmFtZSArICciOiAnICsgYS5sZW5ndGggKyAnIGFzc2V0cycpOyByZXMuanNvbih7IGlkOiBhbC5pZCwgYWxidW1OYW1lOiBhbC5hbGJ1bU5hbWUsIGFzc2V0Q291bnQ6IGEubGVuZ3RoLCBhc3NldHM6IGEgfSk7IH0gY2F0Y2ggKGUpIHsgbG9nRXJyKCdBbGJ1bTogJyArIGUubWVzc2FnZSk7IHJlcy5zdGF0dXMoNTAyKS5qc29uKHsgZXJyb3I6IGUubWVzc2FnZSB9KTsgfSB9KTsKYXBwLmdldCgnL2FwaS9wZW9wbGUnLCBhc3luYyAoX3JlcSwgcmVzKSA9PiB7IHRyeSB7IGNvbnN0IHIgPSBhd2FpdCBmZXRjaChgJHtJTU1JQ0hfVVJMfS9hcGkvcGVvcGxlYCwgeyBoZWFkZXJzOiBpbW1pY2hIZWFkZXJzKCkgfSk7IGlmICghci5vaykgdGhyb3cgbmV3IEVycm9yKGAke3Iuc3RhdHVzfWApOyBjb25zdCBkID0gYXdhaXQgci5qc29uKCk7IHJlcy5qc29uKChkLnBlb3BsZSB8fCBkIHx8IFtdKS5tYXAocCA9PiAoeyBpZDogcC5pZCwgbmFtZTogcC5uYW1lLCB0aHVtYm5haWxQYXRoOiBwLnRodW1ibmFpbFBhdGggfSkpKTsgfSBjYXRjaCAoZSkgeyByZXMuc3RhdHVzKDUwMikuanNvbih7IGVycm9yOiBlLm1lc3NhZ2UgfSk7IH0gfSk7CmFwcC5nZXQoJy9hcGkvcGVvcGxlLzppZCcsIGFzeW5jIChyZXEsIHJlcykgPT4geyB0cnkgeyBjb25zdCByID0gYXdhaXQgZmV0Y2goYCR7SU1NSUNIX1VSTH0vYXBpL3Blb3BsZS8ke3JlcS5wYXJhbXMuaWR9L2Fzc2V0c2AsIHsgaGVhZGVyczogaW1taWNoSGVhZGVycygpIH0pOyBpZiAoIXIub2spIHRocm93IG5ldyBFcnJvcihgJHtyLnN0YXR1c31gKTsgY29uc3QgcmF3ID0gYXdhaXQgci5qc29uKCk7IHJlcy5qc29uKGZpbHRlckFzc2V0cyhBcnJheS5pc0FycmF5KHJhdykgPyByYXcgOiBbXSkubWFwKG1hcEFzc2V0KSk7IH0gY2F0Y2ggKGUpIHsgcmVzLnN0YXR1cyg1MDIpLmpzb24oeyBlcnJvcjogZS5tZXNzYWdlIH0pOyB9IH0pOwphcHAuZ2V0KCcvYXBpL3Blb3BsZS86aWQvdGh1bWJuYWlsJywgYXN5bmMgKHJlcSwgcmVzKSA9PiB7IHRyeSB7IGNvbnN0IHIgPSBhd2FpdCBmZXRjaChgJHtJTU1JQ0hfVVJMfS9hcGkvcGVvcGxlLyR7cmVxLnBhcmFtcy5pZH0vdGh1bWJuYWlsYCwgeyBoZWFkZXJzOiB7ICd4LWFwaS1rZXknOiBBUElfS0VZIH0gfSk7IGlmICghci5vaykgdGhyb3cgbmV3IEVycm9yKGAke3Iuc3RhdHVzfWApOyByZXMuc2V0KCdDb250ZW50LVR5cGUnLCByLmhlYWRlcnMuZ2V0KCdjb250ZW50LXR5cGUnKSB8fCAnaW1hZ2UvanBlZycpOyByZXMuc2V0KCdDYWNoZS1Db250cm9sJywgJ3B1YmxpYywgbWF4LWFnZT04NjQwMCcpOyByLmJvZHkucGlwZShyZXMpOyB9IGNhdGNoIChlKSB7IHJlcy5zdGF0dXMoNTAyKS5qc29uKHsgZXJyb3I6IGUubWVzc2FnZSB9KTsgfSB9KTsKYXBwLmdldCgnL2FwaS9hc3NldHMvcmFuZG9tJywgYXN5bmMgKHJlcSwgcmVzKSA9PiB7IHRyeSB7IGNvbnN0IGMgPSBNYXRoLm1pbihwYXJzZUludChyZXEucXVlcnkuY291bnQsIDEwKSB8fCA1MCwgMjUwKTsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9hc3NldHMvcmFuZG9tP2NvdW50PSR7Y31gLCB7IGhlYWRlcnM6IGltbWljaEhlYWRlcnMoKSB9KTsgaWYgKCFyLm9rKSB0aHJvdyBuZXcgRXJyb3IoYCR7ci5zdGF0dXN9YCk7IHJlcy5qc29uKGZpbHRlckFzc2V0cyhhd2FpdCByLmpzb24oKSkubWFwKG1hcEFzc2V0KSk7IH0gY2F0Y2ggKGUpIHsgcmVzLnN0YXR1cyg1MDIpLmpzb24oeyBlcnJvcjogZS5tZXNzYWdlIH0pOyB9IH0pOwphcHAuZ2V0KCcvYXBpL2Fzc2V0cy9mYXZvcml0ZXMnLCBhc3luYyAoX3JlcSwgcmVzKSA9PiB7IHRyeSB7IGNvbnN0IHIgPSBhd2FpdCBmZXRjaChgJHtJTU1JQ0hfVVJMfS9hcGkvc2VhcmNoL21ldGFkYXRhYCwgeyBtZXRob2Q6ICdQT1NUJywgaGVhZGVyczogaW1taWNoSGVhZGVycygpLCBib2R5OiBKU09OLnN0cmluZ2lmeSh7IGlzRmF2b3JpdGU6IHRydWUsIHNpemU6IDI1MCwgcGFnZTogMSB9KSB9KTsgaWYgKCFyLm9rKSB0aHJvdyBuZXcgRXJyb3IoYCR7ci5zdGF0dXN9YCk7IGNvbnN0IGQgPSBhd2FpdCByLmpzb24oKTsgcmVzLmpzb24oZmlsdGVyQXNzZXRzKGQuYXNzZXRzPy5pdGVtcyB8fCBbXSkubWFwKGEgPT4gKHsgLi4ubWFwQXNzZXQoYSksIGlzRmF2b3JpdGU6IHRydWUgfSkpKTsgfSBjYXRjaCAoZSkgeyByZXMuc3RhdHVzKDUwMikuanNvbih7IGVycm9yOiBlLm1lc3NhZ2UgfSk7IH0gfSk7CmFwcC5nZXQoJy9hcGkvYXNzZXRzLzppZC90aHVtYm5haWwnLCBhc3luYyAocmVxLCByZXMpID0+IHsgdHJ5IHsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9hc3NldHMvJHtyZXEucGFyYW1zLmlkfS90aHVtYm5haWw/c2l6ZT0ke3JlcS5xdWVyeS5zaXplIHx8ICdwcmV2aWV3J31gLCB7IGhlYWRlcnM6IHsgJ3gtYXBpLWtleSc6IEFQSV9LRVkgfSB9KTsgaWYgKCFyLm9rKSB0aHJvdyBuZXcgRXJyb3IoYCR7ci5zdGF0dXN9YCk7IHJlcy5zZXQoJ0NvbnRlbnQtVHlwZScsIHIuaGVhZGVycy5nZXQoJ2NvbnRlbnQtdHlwZScpIHx8ICdpbWFnZS9qcGVnJyk7IHJlcy5zZXQoJ0NhY2hlLUNvbnRyb2wnLCAncHVibGljLCBtYXgtYWdlPTg2NDAwJyk7IHIuYm9keS5waXBlKHJlcyk7IH0gY2F0Y2ggKGUpIHsgcmVzLnN0YXR1cyg1MDIpLmpzb24oeyBlcnJvcjogZS5tZXNzYWdlIH0pOyB9IH0pOwphcHAuZ2V0KCcvYXBpL2Fzc2V0cy86aWQvdmlkZW8nLCBhc3luYyAocmVxLCByZXMpID0+IHsgdHJ5IHsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9hc3NldHMvJHtyZXEucGFyYW1zLmlkfS92aWRlby9wbGF5YmFja2AsIHsgaGVhZGVyczogeyAneC1hcGkta2V5JzogQVBJX0tFWSB9IH0pOyBpZiAoIXIub2spIHRocm93IG5ldyBFcnJvcihgJHtyLnN0YXR1c31gKTsgcmVzLnNldCgnQ29udGVudC1UeXBlJywgci5oZWFkZXJzLmdldCgnY29udGVudC10eXBlJykgfHwgJ3ZpZGVvL21wNCcpOyByZXMuc2V0KCdDYWNoZS1Db250cm9sJywgJ3B1YmxpYywgbWF4LWFnZT04NjQwMCcpOyBjb25zdCBjbCA9IHIuaGVhZGVycy5nZXQoJ2NvbnRlbnQtbGVuZ3RoJyk7IGlmIChjbCkgcmVzLnNldCgnQ29udGVudC1MZW5ndGgnLCBjbCk7IHIuYm9keS5waXBlKHJlcyk7IH0gY2F0Y2ggKGUpIHsgcmVzLnN0YXR1cyg1MDIpLmpzb24oeyBlcnJvcjogZS5tZXNzYWdlIH0pOyB9IH0pOwphcHAuZ2V0KCcvYXBpL2Fzc2V0cy86aWQvb3JpZ2luYWwnLCBhc3luYyAocmVxLCByZXMpID0+IHsgdHJ5IHsgY29uc3QgciA9IGF3YWl0IGZldGNoKGAke0lNTUlDSF9VUkx9L2FwaS9hc3NldHMvJHtyZXEucGFyYW1zLmlkfS9vcmlnaW5hbGAsIHsgaGVhZGVyczogeyAneC1hcGkta2V5JzogQVBJX0tFWSB9IH0pOyBpZiAoIXIub2spIHRocm93IG5ldyBFcnJvcihgJHtyLnN0YXR1c31gKTsgcmVzLnNldCgnQ29udGVudC1UeXBlJywgci5oZWFkZXJzLmdldCgnY29udGVudC10eXBlJykgfHwgJ2ltYWdlL2pwZWcnKTsgcmVzLnNldCgnQ2FjaGUtQ29udHJvbCcsICdwdWJsaWMsIG1heC1hZ2U9ODY0MDAnKTsgci5ib2R5LnBpcGUocmVzKTsgfSBjYXRjaCAoZSkgeyByZXMuc3RhdHVzKDUwMikuanNvbih7IGVycm9yOiBlLm1lc3NhZ2UgfSk7IH0gfSk7CgovLyAtLS0gUkVTVCBBUEk6IENsaWVudCBtYW5hZ2VtZW50ICh0b2tlbi1hdXRoZW50aWNhdGVkIGZvciBIb21lIEFzc2lzdGFudCBldGMuKSAtLS0KYXBwLmdldCgnL2FwaS9jbGllbnRzJywgcmVxdWlyZUFwaVRva2VuLCAoX3JlcSwgcmVzKSA9PiB7CiAgcmVzLmpzb24oeyBvazogdHJ1ZSwgY2xpZW50czogZ2V0Q2xpZW50TGlzdCgpIH0pOwp9KTsKCmFwcC5wb3N0KCcvYXBpL2NsaWVudHMvOmlkL2NvbW1hbmQnLCByZXF1aXJlQXBpVG9rZW4sIChyZXEsIHJlcykgPT4gewogIGNvbnN0IHsgaWQgfSA9IHJlcS5wYXJhbXM7CiAgY29uc3QgeyBhY3Rpb24sIHBheWxvYWQgfSA9IHJlcS5ib2R5IHx8IHt9OwoKICBpZiAoIWFjdGlvbikgcmV0dXJuIHJlcy5zdGF0dXMoNDAwKS5qc29uKHsgb2s6IGZhbHNlLCBlcnJvcjogJ01pc3NpbmcgYWN0aW9uJyB9KTsKCiAgY29uc3QgdGFyZ2V0ID0gY2xpZW50cy5nZXQoaWQpOwogIGlmICghdGFyZ2V0KSByZXR1cm4gcmVzLnN0YXR1cyg0MDQpLmpzb24oeyBvazogZmFsc2UsIGVycm9yOiAnQ2xpZW50IG5vdCBmb3VuZCcgfSk7CiAgaWYgKHRhcmdldC5yb2xlICE9PSAnZnJhbWUnKSByZXR1cm4gcmVzLnN0YXR1cyg0MDApLmpzb24oeyBvazogZmFsc2UsIGVycm9yOiAnVGFyZ2V0IGlzIG5vdCBhIGZyYW1lIGNsaWVudCcgfSk7CiAgaWYgKHRhcmdldC53cy5yZWFkeVN0YXRlICE9PSBXZWJTb2NrZXQuT1BFTikgcmV0dXJuIHJlcy5zdGF0dXMoNDEwKS5qc29uKHsgb2s6IGZhbHNlLCBlcnJvcjogJ0NsaWVudCBXZWJTb2NrZXQgbm90IGNvbm5lY3RlZCcgfSk7CgogIHRhcmdldC53cy5zZW5kKEpTT04uc3RyaW5naWZ5KHsgdHlwZTogJ2NvbW1hbmQnLCBhY3Rpb24sIHBheWxvYWQ6IHBheWxvYWQgfHwge30gfSkpOwogIGxvZygnUkVTVCBjb21tYW5kICcgKyBhY3Rpb24gKyAnIC0+ICcgKyBpZCk7CiAgcmVzLmpzb24oeyBvazogdHJ1ZSwgYWN0aW9uLCB0YXJnZXRJZDogaWQgfSk7Cn0pOwoKLy8gLS0tIEFkbWluIGRhc2hib2FyZCAoYXV0aC1wcm90ZWN0ZWQpIC0tLQphcHAuZ2V0KCcvYWRtaW4nLCByZXF1aXJlQWRtaW5BdXRoLCAoX3JlcSwgcmVzKSA9PiB7IHJlcy5zZW5kRmlsZShwYXRoLmpvaW4oX19kaXJuYW1lLCAncHVibGljJywgJ2FkbWluJywgJ2luZGV4Lmh0bWwnKSk7IH0pOwoKLy8gLS0tIENhdGNoLWFsbCBmb3IgZnJhbWUgU1BBIC0tLQphcHAuZ2V0KCcqJywgKF9yZXEsIHJlcykgPT4geyByZXMuc2VuZEZpbGUocGF0aC5qb2luKF9fZGlybmFtZSwgJ3B1YmxpYycsICdpbmRleC5odG1sJykpOyB9KTsKCnNlcnZlci5saXN0ZW4oUE9SVCwgJzAuMC4wLjAnLCAoKSA9PiB7CiAgbG9nKCctLS0gRnJhbWJlIHYnICsgVkVSU0lPTiArICcgLS0tJyk7CiAgbG9nKCdTZXJ2ZXIgbGlzdGVuaW5nIG9uIHBvcnQgJyArIFBPUlQpOwogIGxvZygnQWRtaW4gZGFzaGJvYXJkOiBodHRwOi8vMC4wLjAuMDonICsgUE9SVCArICcvYWRtaW4nKTsKICBsb2coJ1dlYlNvY2tldDogd3M6Ly8wLjAuMC4wOicgKyBQT1JUICsgJy93cycpOwogIGxvZygnSW1taWNoIFVSTDogJyArIElNTUlDSF9VUkwpOwogIGxvZygnQVBJIGtleTogJyArIChBUElfS0VZID8gJ2NvbmZpZ3VyZWQgKCcgKyBBUElfS0VZLnN1YnN0cmluZygwLCA4KSArICcuLi4pJyA6ICdOT1QgU0VUJykpOwogIGxvZygnQWRtaW4gYXV0aDogJyArIChBVVRIX0VOQUJMRUQgPyAnRU5BQkxFRCAodXNlcjogJyArIEFETUlOX1VTRVJOQU1FICsgJyknIDogJ0RJU0FCTEVEIChubyBBRE1JTl9QQVNTV09SRCBzZXQpJykpOwogIGxvZygnQVBJIHRva2VuOiAnICsgKEZSQU1CRV9BUElfVE9LRU4gPyAnY29uZmlndXJlZCAoJyArIEZSQU1CRV9BUElfVE9LRU4uc3Vic3RyaW5nKDAsIDgpICsgJy4uLiknIDogJ05PVCBTRVQgKFJFU1QgQVBJIG9wZW4pJykpOwogIGxvZygnU2xpZGVzaG93OiAnICsgU0xJREVTSE9XX0lOVEVSVkFMICsgJ3MgaW50ZXJ2YWwsIHJlZnJlc2ggZXZlcnkgJyArIFJFRlJFU0hfSU5URVJWQUwgKyAncycpOwogIGxvZygnVmlkZW9zOiAnICsgKElOQ0xVREVfVklERU9TID8gJ2VuYWJsZWQnIDogJ2Rpc2FibGVkJykpOwogIGxvZygnV2FpdGluZyBmb3IgY29ubmVjdGlvbnMuLi4nKTsKfSk7Cg== \ No newline at end of file +const express = require('express'); +const fetch = require('node-fetch'); +const path = require('path'); +const http = require('http'); +const crypto = require('crypto'); +const { WebSocketServer, WebSocket } = require('ws'); +require('dotenv').config(); + +const VERSION = '1.4.0'; +const app = express(); +const server = http.createServer(app); +const PORT = process.env.PORT || 3000; +const IMMICH_URL = (process.env.IMMICH_URL || 'http://localhost:2283').replace(/\/+$/, ''); +const API_KEY = process.env.IMMICH_API_KEY || ''; +const SLIDESHOW_INTERVAL = parseInt(process.env.SLIDESHOW_INTERVAL, 10) || 30; +const TRANSITION_DURATION = parseInt(process.env.TRANSITION_DURATION, 10) || 2; +const SHOW_CLOCK = process.env.SHOW_CLOCK !== 'false'; +const SHOW_DATE = process.env.SHOW_DATE !== 'false'; +const SHOW_EXIF = process.env.SHOW_EXIF !== 'false'; +const SHOW_PROGRESS = process.env.SHOW_PROGRESS !== 'false'; +const IMAGE_FIT = process.env.IMAGE_FIT || 'contain'; +const BACKGROUND_BLUR = process.env.BACKGROUND_BLUR !== 'false'; +const SHUFFLE = process.env.SHUFFLE !== 'false'; +const ALBUM_ID = process.env.ALBUM_ID || ''; +const SHOW_FAVORITES_ONLY = process.env.SHOW_FAVORITES_ONLY === 'true'; +const REFRESH_INTERVAL = parseInt(process.env.REFRESH_INTERVAL, 10) || 300; +const INCLUDE_VIDEOS = process.env.INCLUDE_VIDEOS !== 'false'; + +// --- Auth configuration --- +const ADMIN_USERNAME = process.env.ADMIN_USERNAME || 'admin'; +const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || ''; +const FRAMBE_API_TOKEN = process.env.FRAMBE_API_TOKEN || ''; +const AUTH_ENABLED = !!ADMIN_PASSWORD; + +// Session store: token -> { username, createdAt, expiresAt } +const sessions = new Map(); +const SESSION_TTL = 24 * 60 * 60 * 1000; // 24 hours + +function createSession(username) { + const token = crypto.randomBytes(32).toString('hex'); + const now = Date.now(); + sessions.set(token, { username, createdAt: now, expiresAt: now + SESSION_TTL }); + return token; +} + +function validateSession(token) { + if (!token) return false; + const session = sessions.get(token); + if (!session) return false; + if (Date.now() > session.expiresAt) { sessions.delete(token); return false; } + return true; +} + +function cleanupSessions() { const now = Date.now(); sessions.forEach((s, t) => { if (now > s.expiresAt) sessions.delete(t); }); } +setInterval(cleanupSessions, 60 * 60 * 1000); // cleanup every hour + +// --- Admin auth middleware (cookie-based for browser) --- +function requireAdminAuth(req, res, next) { + if (!AUTH_ENABLED) return next(); + const cookie = req.headers.cookie || ''; + const match = cookie.match(/frambe_session=([a-f0-9]+)/); + const token = match ? match[1] : null; + if (validateSession(token)) return next(); + if (req.accepts('html')) return res.redirect('/admin/login'); + return res.status(401).json({ error: 'Unauthorized', message: 'Admin login required' }); +} + +// --- API token middleware (for external callers like Home Assistant) --- +function requireApiToken(req, res, next) { + const authHeader = req.headers.authorization || ''; + const bearerToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : ''; + const headerToken = req.headers['x-api-token'] || ''; + const queryToken = req.query.token || ''; + const provided = bearerToken || headerToken || queryToken; + if (FRAMBE_API_TOKEN) { + if (provided === FRAMBE_API_TOKEN) return next(); + } + if (AUTH_ENABLED) { + const cookie = req.headers.cookie || ''; + const match = cookie.match(/frambe_session=([a-f0-9]+)/); + if (match && validateSession(match[1])) return next(); + } + if (!FRAMBE_API_TOKEN && !AUTH_ENABLED) return next(); + return res.status(401).json({ error: 'Unauthorized', message: 'Valid API token or admin session required' }); +} + +function immichHeaders() { return { 'x-api-key': API_KEY, 'Accept': 'application/json', 'Content-Type': 'application/json' }; } +function log(msg) { console.log('[Frambe] ' + msg); } +function logErr(msg) { console.error('[Frambe] ERROR: ' + msg); } + +const clients = new Map(); +let clientNameStore = {}; +function getClientIp(req) { return req.headers['x-forwarded-for']?.split(',')[0].trim() || req.socket.remoteAddress || 'unknown'; } +function generateClientId(ip) { return ip.replace(/[.:]/g, '_'); } +function broadcastToAdmins(msg) { const d = JSON.stringify(msg); clients.forEach(c => { if (c.role === 'admin' && c.ws.readyState === WebSocket.OPEN) c.ws.send(d); }); } +function getClientList() { const list = []; clients.forEach((c, id) => { if (c.role === 'frame') list.push({ id, ip: c.ip, name: c.name || clientNameStore[c.ip] || '', status: c.status || 'unknown', connectedAt: c.connectedAt, lastSeen: c.lastSeen, config: c.config || {} }); }); return list; } + +const wss = new WebSocketServer({ server, path: '/ws' }); +wss.on('connection', (ws, req) => { + const ip = getClientIp(req); + const clientId = generateClientId(ip) + '_' + Date.now(); + log('WebSocket connected: ' + ip + ' (' + clientId + ')'); + const info = { ws, ip, role: 'frame', name: clientNameStore[ip] || '', status: 'connected', connectedAt: new Date().toISOString(), lastSeen: new Date().toISOString(), config: {} }; + clients.set(clientId, info); + ws.send(JSON.stringify({ type: 'welcome', clientId, name: info.name })); + ws.on('message', raw => { + try { + const msg = JSON.parse(raw); info.lastSeen = new Date().toISOString(); + switch (msg.type) { + case 'register': + info.role = msg.role || 'frame'; + if (msg.role === 'admin') { log('Admin connected from ' + ip); ws.send(JSON.stringify({ type: 'clientList', clients: getClientList() })); } + else { log('Frame registered: ' + ip); info.status = msg.status || 'idle'; info.config = msg.config || {}; broadcastToAdmins({ type: 'clientList', clients: getClientList() }); } + break; + case 'status': + info.status = msg.status || info.status; if (msg.config) info.config = msg.config; + broadcastToAdmins({ type: 'clientUpdate', clientId, client: { id: clientId, ip: info.ip, name: info.name, status: info.status, lastSeen: info.lastSeen, config: info.config } }); + break; + case 'adminCommand': + const target = clients.get(msg.targetId); + if (target && target.ws.readyState === WebSocket.OPEN) { target.ws.send(JSON.stringify({ type: 'command', action: msg.action, payload: msg.payload })); log('Command ' + msg.action + ' -> ' + msg.targetId); } + else ws.send(JSON.stringify({ type: 'error', message: 'Client not found' })); + break; + case 'renameClient': + const rt = clients.get(msg.targetId); + if (rt) { rt.name = msg.name; clientNameStore[rt.ip] = msg.name; log('Renamed ' + msg.targetId + ' -> "' + msg.name + '"'); broadcastToAdmins({ type: 'clientList', clients: getClientList() }); } + break; + } + } catch (e) { logErr('WS parse error: ' + e.message); } + }); + ws.on('close', () => { log('WebSocket disconnected: ' + ip); clients.delete(clientId); broadcastToAdmins({ type: 'clientList', clients: getClientList() }); }); +}); + +app.use('/api', (req, _res, next) => { log('API ' + req.method + ' ' + req.originalUrl); next(); }); +app.use(express.json()); + +// --- Auth endpoints --- +app.get('/api/auth/status', (_req, res) => { + res.json({ authEnabled: AUTH_ENABLED, apiTokenEnabled: !!FRAMBE_API_TOKEN }); +}); + +app.post('/api/auth/login', (req, res) => { + if (!AUTH_ENABLED) return res.json({ ok: true, message: 'Auth not enabled' }); + const { username, password } = req.body || {}; + if (username === ADMIN_USERNAME && password === ADMIN_PASSWORD) { + const token = createSession(username); + res.setHeader('Set-Cookie', `frambe_session=${token}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${SESSION_TTL / 1000}`); + log('Admin login: ' + username); + return res.json({ ok: true }); + } + log('Failed login attempt: ' + (username || '(empty)')); + return res.status(401).json({ ok: false, error: 'Invalid credentials' }); +}); + +app.post('/api/auth/logout', (req, res) => { + const cookie = req.headers.cookie || ''; + const match = cookie.match(/frambe_session=([a-f0-9]+)/); + if (match) sessions.delete(match[1]); + res.setHeader('Set-Cookie', 'frambe_session=; Path=/; HttpOnly; Max-Age=0'); + res.json({ ok: true }); +}); + +// --- Login page (served without auth) --- +app.get('/admin/login', (_req, res) => { + if (!AUTH_ENABLED) return res.redirect('/admin'); + res.sendFile(path.join(__dirname, 'public', 'admin', 'login.html')); +}); + +// --- Static files (non-admin pages don't require auth) --- +app.use(express.static(path.join(__dirname, 'public'), { setHeaders: (res, fp) => { if (fp.endsWith('.html') || fp.endsWith('.js') || fp.endsWith('.css')) { res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } } })); + +function mapAsset(a) { return { id: a.id, type: a.type, originalFileName: a.originalFileName, fileCreatedAt: a.fileCreatedAt, isFavorite: a.isFavorite, exifInfo: a.exifInfo ? { make: a.exifInfo.make, model: a.exifInfo.model, city: a.exifInfo.city, state: a.exifInfo.state, country: a.exifInfo.country, description: a.exifInfo.description, dateTimeOriginal: a.exifInfo.dateTimeOriginal } : null }; } +function filterAssets(assets) { return INCLUDE_VIDEOS ? assets.filter(a => a.type === 'IMAGE' || a.type === 'VIDEO') : assets.filter(a => a.type === 'IMAGE'); } + +app.get('/api/config', (_req, res) => { res.json({ version: VERSION, slideshowInterval: SLIDESHOW_INTERVAL, transitionDuration: TRANSITION_DURATION, showClock: SHOW_CLOCK, showDate: SHOW_DATE, showExif: SHOW_EXIF, showProgress: SHOW_PROGRESS, imageFit: IMAGE_FIT, backgroundBlur: BACKGROUND_BLUR, shuffle: SHUFFLE, albumId: ALBUM_ID, showFavoritesOnly: SHOW_FAVORITES_ONLY, refreshInterval: REFRESH_INTERVAL, includeVideos: INCLUDE_VIDEOS, connected: !!API_KEY, authEnabled: AUTH_ENABLED }); }); +app.get('/api/server-info', async (_req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/server/version`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`Immich returned ${r.status}`); const v = await r.json(); log('Immich OK v' + v.major + '.' + v.minor + '.' + v.patch); res.json({ ok: true, version: v }); } catch (e) { logErr('Immich failed: ' + e.message); res.status(502).json({ ok: false, error: e.message }); } }); +app.get('/api/albums', async (_req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/albums`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`${r.status}`); const a = await r.json(); log('Listed ' + a.length + ' albums'); res.json(a.map(x => ({ id: x.id, albumName: x.albumName, assetCount: x.assetCount, albumThumbnailAssetId: x.albumThumbnailAssetId, updatedAt: x.updatedAt }))); } catch (e) { logErr('Albums: ' + e.message); res.status(502).json({ error: e.message }); } }); +app.get('/api/albums/:id', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/albums/${req.params.id}`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`${r.status}`); const al = await r.json(); const a = filterAssets(al.assets || []).map(mapAsset); log('Album "' + al.albumName + '": ' + a.length + ' assets'); res.json({ id: al.id, albumName: al.albumName, assetCount: a.length, assets: a }); } catch (e) { logErr('Album: ' + e.message); res.status(502).json({ error: e.message }); } }); +app.get('/api/people', async (_req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/people`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`${r.status}`); const d = await r.json(); res.json((d.people || d || []).map(p => ({ id: p.id, name: p.name, thumbnailPath: p.thumbnailPath }))); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/people/:id', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/people/${req.params.id}/assets`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`${r.status}`); const raw = await r.json(); res.json(filterAssets(Array.isArray(raw) ? raw : []).map(mapAsset)); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/people/:id/thumbnail', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/people/${req.params.id}/thumbnail`, { headers: { 'x-api-key': API_KEY } }); if (!r.ok) throw new Error(`${r.status}`); res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); res.set('Cache-Control', 'public, max-age=86400'); r.body.pipe(res); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/assets/random', async (req, res) => { try { const c = Math.min(parseInt(req.query.count, 10) || 50, 250); const r = await fetch(`${IMMICH_URL}/api/assets/random?count=${c}`, { headers: immichHeaders() }); if (!r.ok) throw new Error(`${r.status}`); res.json(filterAssets(await r.json()).map(mapAsset)); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/assets/favorites', async (_req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/search/metadata`, { method: 'POST', headers: immichHeaders(), body: JSON.stringify({ isFavorite: true, size: 250, page: 1 }) }); if (!r.ok) throw new Error(`${r.status}`); const d = await r.json(); res.json(filterAssets(d.assets?.items || []).map(a => ({ ...mapAsset(a), isFavorite: true }))); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/assets/:id/thumbnail', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/thumbnail?size=${req.query.size || 'preview'}`, { headers: { 'x-api-key': API_KEY } }); if (!r.ok) throw new Error(`${r.status}`); res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); res.set('Cache-Control', 'public, max-age=86400'); r.body.pipe(res); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/assets/:id/video', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/video/playback`, { headers: { 'x-api-key': API_KEY } }); if (!r.ok) throw new Error(`${r.status}`); res.set('Content-Type', r.headers.get('content-type') || 'video/mp4'); res.set('Cache-Control', 'public, max-age=86400'); const cl = r.headers.get('content-length'); if (cl) res.set('Content-Length', cl); r.body.pipe(res); } catch (e) { res.status(502).json({ error: e.message }); } }); +app.get('/api/assets/:id/original', async (req, res) => { try { const r = await fetch(`${IMMICH_URL}/api/assets/${req.params.id}/original`, { headers: { 'x-api-key': API_KEY } }); if (!r.ok) throw new Error(`${r.status}`); res.set('Content-Type', r.headers.get('content-type') || 'image/jpeg'); res.set('Cache-Control', 'public, max-age=86400'); r.body.pipe(res); } catch (e) { res.status(502).json({ error: e.message }); } }); + +// --- REST API: Client management (token-authenticated for Home Assistant etc.) --- +app.get('/api/clients', requireApiToken, (_req, res) => { + res.json({ ok: true, clients: getClientList() }); +}); + +app.post('/api/clients/:id/command', requireApiToken, (req, res) => { + const { id } = req.params; + const { action, payload } = req.body || {}; + if (!action) return res.status(400).json({ ok: false, error: 'Missing action' }); + const target = clients.get(id); + if (!target) return res.status(404).json({ ok: false, error: 'Client not found' }); + if (target.role !== 'frame') return res.status(400).json({ ok: false, error: 'Target is not a frame client' }); + if (target.ws.readyState !== WebSocket.OPEN) return res.status(410).json({ ok: false, error: 'Client WebSocket not connected' }); + target.ws.send(JSON.stringify({ type: 'command', action, payload: payload || {} })); + log('REST command ' + action + ' -> ' + id); + res.json({ ok: true, action, targetId: id }); +}); + +// --- Admin dashboard (auth-protected) --- +app.get('/admin', requireAdminAuth, (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'admin', 'index.html')); }); + +// --- Catch-all for frame SPA --- +app.get('*', (_req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); + +server.listen(PORT, '0.0.0.0', () => { + log('--- Frambe v' + VERSION + ' ---'); + log('Server listening on port ' + PORT); + log('Admin dashboard: http://0.0.0.0:' + PORT + '/admin'); + log('WebSocket: ws://0.0.0.0:' + PORT + '/ws'); + log('Immich URL: ' + IMMICH_URL); + log('API key: ' + (API_KEY ? 'configured (' + API_KEY.substring(0, 8) + '...)' : 'NOT SET')); + log('Admin auth: ' + (AUTH_ENABLED ? 'ENABLED (user: ' + ADMIN_USERNAME + ')' : 'DISABLED (no ADMIN_PASSWORD set)')); + log('API token: ' + (FRAMBE_API_TOKEN ? 'configured (' + FRAMBE_API_TOKEN.substring(0, 8) + '...)' : 'NOT SET (REST API open)')); + log('Slideshow: ' + SLIDESHOW_INTERVAL + 's interval, refresh every ' + REFRESH_INTERVAL + 's'); + log('Videos: ' + (INCLUDE_VIDEOS ? 'enabled' : 'disabled')); + log('Waiting for connections...'); +});