diff --git a/Assets/Mirror/Hosting.meta b/Assets/Mirror/Hosting.meta new file mode 100644 index 000000000..43de0c569 --- /dev/null +++ b/Assets/Mirror/Hosting.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b13bce90dfb604c2d9170e3640f59ad9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap.meta b/Assets/Mirror/Hosting/Edgegap.meta new file mode 100755 index 000000000..c4f5fd2c1 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b7c51dc3e45095f4a8a960150837fe7b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md b/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md new file mode 100644 index 000000000..f158d0a2f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md @@ -0,0 +1,3 @@ +# Edgegap Servers Plugin Changelog + +Please refer to our [Github repository](https://github.com/edgegap/edgegap-unity-plugin/releases) for up-to-date changelog. diff --git a/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md.meta b/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md.meta new file mode 100644 index 000000000..9ea264276 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/CHANGELOG.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: e3f5a876d3822ce4e884e3c8d026fbcc +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Dependencies.meta b/Assets/Mirror/Hosting/Edgegap/Dependencies.meta new file mode 100644 index 000000000..1a9b91853 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dependencies.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 14024ce6d2e64d5ba58ab20409ac648f +timeCreated: 1701785018 \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs new file mode 100644 index 000000000..6c4b15d4f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs @@ -0,0 +1,704 @@ +// MIRROR CHANGE: drop in Codice.Utils HttpUtility subset to not depend on Unity's plastic scm package +// SOURCE: Unity Plastic SCM package + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text; + +namespace Edgegap.Codice.Utils // MIRROR CHANGE: namespace Edgegap.* to not collide if anyone has Plastic SCM installed already +{ + public class HttpEncoder + { + private static char[] hexChars = "0123456789abcdef".ToCharArray(); + private static object entitiesLock = new object(); + private static SortedDictionary entities; + private static HttpEncoder defaultEncoder = new HttpEncoder(); + private static HttpEncoder currentEncoder = HttpEncoder.defaultEncoder; + + private static IDictionary Entities + { + get + { + lock (HttpEncoder.entitiesLock) + { + if (HttpEncoder.entities == null) + HttpEncoder.InitEntities(); + return (IDictionary) HttpEncoder.entities; + } + } + } + + public static HttpEncoder Current + { + get => HttpEncoder.currentEncoder; + set => HttpEncoder.currentEncoder = value != null ? value : throw new ArgumentNullException(nameof (value)); + } + + public static HttpEncoder Default => HttpEncoder.defaultEncoder; + + protected internal virtual void HeaderNameValueEncode( + string headerName, + string headerValue, + out string encodedHeaderName, + out string encodedHeaderValue) + { + encodedHeaderName = !string.IsNullOrEmpty(headerName) ? HttpEncoder.EncodeHeaderString(headerName) : headerName; + if (string.IsNullOrEmpty(headerValue)) + encodedHeaderValue = headerValue; + else + encodedHeaderValue = HttpEncoder.EncodeHeaderString(headerValue); + } + + private static void StringBuilderAppend(string s, ref StringBuilder sb) + { + if (sb == null) + sb = new StringBuilder(s); + else + sb.Append(s); + } + + private static string EncodeHeaderString(string input) + { + StringBuilder sb = (StringBuilder) null; + for (int index = 0; index < input.Length; ++index) + { + char ch = input[index]; + if (ch < ' ' && ch != '\t' || ch == '\u007F') + HttpEncoder.StringBuilderAppend(string.Format("%{0:x2}", (object) (int) ch), ref sb); + } + return sb != null ? sb.ToString() : input; + } + + protected internal virtual void HtmlAttributeEncode(string value, TextWriter output) + { + if (output == null) + throw new ArgumentNullException(nameof (output)); + if (string.IsNullOrEmpty(value)) + return; + output.Write(HttpEncoder.HtmlAttributeEncode(value)); + } + + protected internal virtual void HtmlDecode(string value, TextWriter output) + { + if (output == null) + throw new ArgumentNullException(nameof (output)); + output.Write(HttpEncoder.HtmlDecode(value)); + } + + protected internal virtual void HtmlEncode(string value, TextWriter output) + { + if (output == null) + throw new ArgumentNullException(nameof (output)); + output.Write(HttpEncoder.HtmlEncode(value)); + } + + protected internal virtual byte[] UrlEncode(byte[] bytes, int offset, int count) => HttpEncoder.UrlEncodeToBytes(bytes, offset, count); + + protected internal virtual string UrlPathEncode(string value) + { + if (string.IsNullOrEmpty(value)) + return value; + MemoryStream result = new MemoryStream(); + int length = value.Length; + for (int index = 0; index < length; ++index) + HttpEncoder.UrlPathEncodeChar(value[index], (Stream) result); + return Encoding.ASCII.GetString(result.ToArray()); + } + + internal static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) + { + int num1 = bytes != null ? bytes.Length : throw new ArgumentNullException(nameof (bytes)); + if (num1 == 0) + return new byte[0]; + if (offset < 0 || offset >= num1) + throw new ArgumentOutOfRangeException(nameof (offset)); + if (count < 0 || count > num1 - offset) + throw new ArgumentOutOfRangeException(nameof (count)); + MemoryStream result = new MemoryStream(count); + int num2 = offset + count; + for (int index = offset; index < num2; ++index) + HttpEncoder.UrlEncodeChar((char) bytes[index], (Stream) result, false); + return result.ToArray(); + } + + internal static string HtmlEncode(string s) + { + switch (s) + { + case "": + return string.Empty; + case null: + return (string) null; + default: + bool flag = false; + for (int index = 0; index < s.Length; ++index) + { + char ch = s[index]; + if (ch == '&' || ch == '"' || ch == '<' || ch == '>' || ch > '\u009F' || ch == '\'') + { + flag = true; + break; + } + } + if (!flag) + return s; + StringBuilder stringBuilder = new StringBuilder(); + int length = s.Length; + for (int index = 0; index < length; ++index) + { + char ch = s[index]; + switch (ch) + { + case '"': + stringBuilder.Append("""); + break; + case '&': + stringBuilder.Append("&"); + break; + case '\'': + stringBuilder.Append("'"); + break; + case '<': + stringBuilder.Append("<"); + break; + case '>': + stringBuilder.Append(">"); + break; + case '<': + stringBuilder.Append("<"); + break; + case '>': + stringBuilder.Append(">"); + break; + default: + if (ch > '\u009F' && ch < 'Ā') + { + stringBuilder.Append("&#"); + stringBuilder.Append(((int) ch).ToString((IFormatProvider) CultureInfo.InvariantCulture)); + stringBuilder.Append(";"); + break; + } + stringBuilder.Append(ch); + break; + } + } + return stringBuilder.ToString(); + } + } + + internal static string HtmlAttributeEncode(string s) + { + if (string.IsNullOrEmpty(s)) + return string.Empty; + bool flag = false; + for (int index = 0; index < s.Length; ++index) + { + char ch = s[index]; + int num; + switch (ch) + { + case '"': + case '&': + case '<': + num = 0; + break; + default: + num = ch != '\'' ? 1 : 0; + break; + } + if (num == 0) + { + flag = true; + break; + } + } + if (!flag) + return s; + StringBuilder stringBuilder = new StringBuilder(); + int length = s.Length; + for (int index = 0; index < length; ++index) + { + char ch = s[index]; + switch (ch) + { + case '"': + stringBuilder.Append("""); + break; + case '&': + stringBuilder.Append("&"); + break; + case '\'': + stringBuilder.Append("'"); + break; + case '<': + stringBuilder.Append("<"); + break; + default: + stringBuilder.Append(ch); + break; + } + } + return stringBuilder.ToString(); + } + + internal static string HtmlDecode(string s) + { + switch (s) + { + case "": + return string.Empty; + case null: + return (string) null; + default: + if (s.IndexOf('&') == -1) + return s; + StringBuilder stringBuilder1 = new StringBuilder(); + StringBuilder stringBuilder2 = new StringBuilder(); + StringBuilder stringBuilder3 = new StringBuilder(); + int length = s.Length; + int num1 = 0; + int num2 = 0; + bool flag1 = false; + bool flag2 = false; + for (int index = 0; index < length; ++index) + { + char ch = s[index]; + if (num1 == 0) + { + if (ch == '&') + { + stringBuilder2.Append(ch); + stringBuilder1.Append(ch); + num1 = 1; + } + else + stringBuilder3.Append(ch); + } + else if (ch == '&') + { + num1 = 1; + if (flag2) + { + stringBuilder2.Append(num2.ToString((IFormatProvider) CultureInfo.InvariantCulture)); + flag2 = false; + } + stringBuilder3.Append(stringBuilder2.ToString()); + stringBuilder2.Length = 0; + stringBuilder2.Append('&'); + } + else + { + switch (num1) + { + case 1: + if (ch == ';') + { + num1 = 0; + stringBuilder3.Append(stringBuilder2.ToString()); + stringBuilder3.Append(ch); + stringBuilder2.Length = 0; + break; + } + num2 = 0; + flag1 = false; + num1 = ch == '#' ? 3 : 2; + stringBuilder2.Append(ch); + stringBuilder1.Append(ch); + break; + case 2: + stringBuilder2.Append(ch); + if (ch == ';') + { + string str = stringBuilder2.ToString(); + if (str.Length > 1 && HttpEncoder.Entities.ContainsKey(str.Substring(1, str.Length - 2))) + str = HttpEncoder.Entities[str.Substring(1, str.Length - 2)].ToString(); + stringBuilder3.Append(str); + num1 = 0; + stringBuilder2.Length = 0; + stringBuilder1.Length = 0; + break; + } + break; + case 3: + if (ch == ';') + { + if (num2 == 0) + stringBuilder3.Append(stringBuilder1.ToString() + ";"); + else if (num2 > (int) ushort.MaxValue) + { + stringBuilder3.Append("&#"); + stringBuilder3.Append(num2.ToString((IFormatProvider) CultureInfo.InvariantCulture)); + stringBuilder3.Append(";"); + } + else + stringBuilder3.Append((char) num2); + num1 = 0; + stringBuilder2.Length = 0; + stringBuilder1.Length = 0; + flag2 = false; + } + else if (flag1 && Uri.IsHexDigit(ch)) + { + num2 = num2 * 16 + Uri.FromHex(ch); + flag2 = true; + stringBuilder1.Append(ch); + } + else if (char.IsDigit(ch)) + { + num2 = num2 * 10 + ((int) ch - 48); + flag2 = true; + stringBuilder1.Append(ch); + } + else if (num2 == 0 && (ch == 'x' || ch == 'X')) + { + flag1 = true; + stringBuilder1.Append(ch); + } + else + { + num1 = 2; + if (flag2) + { + stringBuilder2.Append(num2.ToString((IFormatProvider) CultureInfo.InvariantCulture)); + flag2 = false; + } + stringBuilder2.Append(ch); + } + break; + } + } + } + if (stringBuilder2.Length > 0) + stringBuilder3.Append(stringBuilder2.ToString()); + else if (flag2) + stringBuilder3.Append(num2.ToString((IFormatProvider) CultureInfo.InvariantCulture)); + return stringBuilder3.ToString(); + } + } + + internal static bool NotEncoded(char c) => c == '!' || c == '(' || c == ')' || c == '*' || c == '-' || c == '.' || c == '_'; + + internal static void UrlEncodeChar(char c, Stream result, bool isUnicode) + { + if (c > 'ÿ') + { + int num = (int) c; + result.WriteByte((byte) 37); + result.WriteByte((byte) 117); + int index1 = num >> 12; + result.WriteByte((byte) HttpEncoder.hexChars[index1]); + int index2 = num >> 8 & 15; + result.WriteByte((byte) HttpEncoder.hexChars[index2]); + int index3 = num >> 4 & 15; + result.WriteByte((byte) HttpEncoder.hexChars[index3]); + int index4 = num & 15; + result.WriteByte((byte) HttpEncoder.hexChars[index4]); + } + else if (c > ' ' && HttpEncoder.NotEncoded(c)) + result.WriteByte((byte) c); + else if (c == ' ') + result.WriteByte((byte) 43); + else if (c < '0' || c < 'A' && c > '9' || c > 'Z' && c < 'a' || c > 'z') + { + if (isUnicode && c > '\u007F') + { + result.WriteByte((byte) 37); + result.WriteByte((byte) 117); + result.WriteByte((byte) 48); + result.WriteByte((byte) 48); + } + else + result.WriteByte((byte) 37); + int index5 = (int) c >> 4; + result.WriteByte((byte) HttpEncoder.hexChars[index5]); + int index6 = (int) c & 15; + result.WriteByte((byte) HttpEncoder.hexChars[index6]); + } + else + result.WriteByte((byte) c); + } + + internal static void UrlPathEncodeChar(char c, Stream result) + { + if (c < '!' || c > '~') + { + byte[] bytes = Encoding.UTF8.GetBytes(c.ToString()); + for (int index1 = 0; index1 < bytes.Length; ++index1) + { + result.WriteByte((byte) 37); + int index2 = (int) bytes[index1] >> 4; + result.WriteByte((byte) HttpEncoder.hexChars[index2]); + int index3 = (int) bytes[index1] & 15; + result.WriteByte((byte) HttpEncoder.hexChars[index3]); + } + } + else if (c == ' ') + { + result.WriteByte((byte) 37); + result.WriteByte((byte) 50); + result.WriteByte((byte) 48); + } + else + result.WriteByte((byte) c); + } + + private static void InitEntities() + { + HttpEncoder.entities = new SortedDictionary((IComparer) StringComparer.Ordinal); + HttpEncoder.entities.Add("nbsp", ' '); + HttpEncoder.entities.Add("iexcl", '¡'); + HttpEncoder.entities.Add("cent", '¢'); + HttpEncoder.entities.Add("pound", '£'); + HttpEncoder.entities.Add("curren", '¤'); + HttpEncoder.entities.Add("yen", '¥'); + HttpEncoder.entities.Add("brvbar", '¦'); + HttpEncoder.entities.Add("sect", '§'); + HttpEncoder.entities.Add("uml", '¨'); + HttpEncoder.entities.Add("copy", '©'); + HttpEncoder.entities.Add("ordf", 'ª'); + HttpEncoder.entities.Add("laquo", '«'); + HttpEncoder.entities.Add("not", '¬'); + HttpEncoder.entities.Add("shy", '\u00AD'); + HttpEncoder.entities.Add("reg", '®'); + HttpEncoder.entities.Add("macr", '¯'); + HttpEncoder.entities.Add("deg", '°'); + HttpEncoder.entities.Add("plusmn", '±'); + HttpEncoder.entities.Add("sup2", '\u00B2'); + HttpEncoder.entities.Add("sup3", '\u00B3'); + HttpEncoder.entities.Add("acute", '´'); + HttpEncoder.entities.Add("micro", 'µ'); + HttpEncoder.entities.Add("para", '¶'); + HttpEncoder.entities.Add("middot", '·'); + HttpEncoder.entities.Add("cedil", '¸'); + HttpEncoder.entities.Add("sup1", '\u00B9'); + HttpEncoder.entities.Add("ordm", 'º'); + HttpEncoder.entities.Add("raquo", '»'); + HttpEncoder.entities.Add("frac14", '\u00BC'); + HttpEncoder.entities.Add("frac12", '\u00BD'); + HttpEncoder.entities.Add("frac34", '\u00BE'); + HttpEncoder.entities.Add("iquest", '¿'); + HttpEncoder.entities.Add("Agrave", 'À'); + HttpEncoder.entities.Add("Aacute", 'Á'); + HttpEncoder.entities.Add("Acirc", 'Â'); + HttpEncoder.entities.Add("Atilde", 'Ã'); + HttpEncoder.entities.Add("Auml", 'Ä'); + HttpEncoder.entities.Add("Aring", 'Å'); + HttpEncoder.entities.Add("AElig", 'Æ'); + HttpEncoder.entities.Add("Ccedil", 'Ç'); + HttpEncoder.entities.Add("Egrave", 'È'); + HttpEncoder.entities.Add("Eacute", 'É'); + HttpEncoder.entities.Add("Ecirc", 'Ê'); + HttpEncoder.entities.Add("Euml", 'Ë'); + HttpEncoder.entities.Add("Igrave", 'Ì'); + HttpEncoder.entities.Add("Iacute", 'Í'); + HttpEncoder.entities.Add("Icirc", 'Î'); + HttpEncoder.entities.Add("Iuml", 'Ï'); + HttpEncoder.entities.Add("ETH", 'Ð'); + HttpEncoder.entities.Add("Ntilde", 'Ñ'); + HttpEncoder.entities.Add("Ograve", 'Ò'); + HttpEncoder.entities.Add("Oacute", 'Ó'); + HttpEncoder.entities.Add("Ocirc", 'Ô'); + HttpEncoder.entities.Add("Otilde", 'Õ'); + HttpEncoder.entities.Add("Ouml", 'Ö'); + HttpEncoder.entities.Add("times", '×'); + HttpEncoder.entities.Add("Oslash", 'Ø'); + HttpEncoder.entities.Add("Ugrave", 'Ù'); + HttpEncoder.entities.Add("Uacute", 'Ú'); + HttpEncoder.entities.Add("Ucirc", 'Û'); + HttpEncoder.entities.Add("Uuml", 'Ü'); + HttpEncoder.entities.Add("Yacute", 'Ý'); + HttpEncoder.entities.Add("THORN", 'Þ'); + HttpEncoder.entities.Add("szlig", 'ß'); + HttpEncoder.entities.Add("agrave", 'à'); + HttpEncoder.entities.Add("aacute", 'á'); + HttpEncoder.entities.Add("acirc", 'â'); + HttpEncoder.entities.Add("atilde", 'ã'); + HttpEncoder.entities.Add("auml", 'ä'); + HttpEncoder.entities.Add("aring", 'å'); + HttpEncoder.entities.Add("aelig", 'æ'); + HttpEncoder.entities.Add("ccedil", 'ç'); + HttpEncoder.entities.Add("egrave", 'è'); + HttpEncoder.entities.Add("eacute", 'é'); + HttpEncoder.entities.Add("ecirc", 'ê'); + HttpEncoder.entities.Add("euml", 'ë'); + HttpEncoder.entities.Add("igrave", 'ì'); + HttpEncoder.entities.Add("iacute", 'í'); + HttpEncoder.entities.Add("icirc", 'î'); + HttpEncoder.entities.Add("iuml", 'ï'); + HttpEncoder.entities.Add("eth", 'ð'); + HttpEncoder.entities.Add("ntilde", 'ñ'); + HttpEncoder.entities.Add("ograve", 'ò'); + HttpEncoder.entities.Add("oacute", 'ó'); + HttpEncoder.entities.Add("ocirc", 'ô'); + HttpEncoder.entities.Add("otilde", 'õ'); + HttpEncoder.entities.Add("ouml", 'ö'); + HttpEncoder.entities.Add("divide", '÷'); + HttpEncoder.entities.Add("oslash", 'ø'); + HttpEncoder.entities.Add("ugrave", 'ù'); + HttpEncoder.entities.Add("uacute", 'ú'); + HttpEncoder.entities.Add("ucirc", 'û'); + HttpEncoder.entities.Add("uuml", 'ü'); + HttpEncoder.entities.Add("yacute", 'ý'); + HttpEncoder.entities.Add("thorn", 'þ'); + HttpEncoder.entities.Add("yuml", 'ÿ'); + HttpEncoder.entities.Add("fnof", 'ƒ'); + HttpEncoder.entities.Add("Alpha", 'Α'); + HttpEncoder.entities.Add("Beta", 'Β'); + HttpEncoder.entities.Add("Gamma", 'Γ'); + HttpEncoder.entities.Add("Delta", 'Δ'); + HttpEncoder.entities.Add("Epsilon", 'Ε'); + HttpEncoder.entities.Add("Zeta", 'Ζ'); + HttpEncoder.entities.Add("Eta", 'Η'); + HttpEncoder.entities.Add("Theta", 'Θ'); + HttpEncoder.entities.Add("Iota", 'Ι'); + HttpEncoder.entities.Add("Kappa", 'Κ'); + HttpEncoder.entities.Add("Lambda", 'Λ'); + HttpEncoder.entities.Add("Mu", 'Μ'); + HttpEncoder.entities.Add("Nu", 'Ν'); + HttpEncoder.entities.Add("Xi", 'Ξ'); + HttpEncoder.entities.Add("Omicron", 'Ο'); + HttpEncoder.entities.Add("Pi", 'Π'); + HttpEncoder.entities.Add("Rho", 'Ρ'); + HttpEncoder.entities.Add("Sigma", 'Σ'); + HttpEncoder.entities.Add("Tau", 'Τ'); + HttpEncoder.entities.Add("Upsilon", 'Υ'); + HttpEncoder.entities.Add("Phi", 'Φ'); + HttpEncoder.entities.Add("Chi", 'Χ'); + HttpEncoder.entities.Add("Psi", 'Ψ'); + HttpEncoder.entities.Add("Omega", 'Ω'); + HttpEncoder.entities.Add("alpha", 'α'); + HttpEncoder.entities.Add("beta", 'β'); + HttpEncoder.entities.Add("gamma", 'γ'); + HttpEncoder.entities.Add("delta", 'δ'); + HttpEncoder.entities.Add("epsilon", 'ε'); + HttpEncoder.entities.Add("zeta", 'ζ'); + HttpEncoder.entities.Add("eta", 'η'); + HttpEncoder.entities.Add("theta", 'θ'); + HttpEncoder.entities.Add("iota", 'ι'); + HttpEncoder.entities.Add("kappa", 'κ'); + HttpEncoder.entities.Add("lambda", 'λ'); + HttpEncoder.entities.Add("mu", 'μ'); + HttpEncoder.entities.Add("nu", 'ν'); + HttpEncoder.entities.Add("xi", 'ξ'); + HttpEncoder.entities.Add("omicron", 'ο'); + HttpEncoder.entities.Add("pi", 'π'); + HttpEncoder.entities.Add("rho", 'ρ'); + HttpEncoder.entities.Add("sigmaf", 'ς'); + HttpEncoder.entities.Add("sigma", 'σ'); + HttpEncoder.entities.Add("tau", 'τ'); + HttpEncoder.entities.Add("upsilon", 'υ'); + HttpEncoder.entities.Add("phi", 'φ'); + HttpEncoder.entities.Add("chi", 'χ'); + HttpEncoder.entities.Add("psi", 'ψ'); + HttpEncoder.entities.Add("omega", 'ω'); + HttpEncoder.entities.Add("thetasym", 'ϑ'); + HttpEncoder.entities.Add("upsih", 'ϒ'); + HttpEncoder.entities.Add("piv", 'ϖ'); + HttpEncoder.entities.Add("bull", '•'); + HttpEncoder.entities.Add("hellip", '…'); + HttpEncoder.entities.Add("prime", '′'); + HttpEncoder.entities.Add("Prime", '″'); + HttpEncoder.entities.Add("oline", '‾'); + HttpEncoder.entities.Add("frasl", '⁄'); + HttpEncoder.entities.Add("weierp", '℘'); + HttpEncoder.entities.Add("image", 'ℑ'); + HttpEncoder.entities.Add("real", 'ℜ'); + HttpEncoder.entities.Add("trade", '™'); + HttpEncoder.entities.Add("alefsym", 'ℵ'); + HttpEncoder.entities.Add("larr", '←'); + HttpEncoder.entities.Add("uarr", '↑'); + HttpEncoder.entities.Add("rarr", '→'); + HttpEncoder.entities.Add("darr", '↓'); + HttpEncoder.entities.Add("harr", '↔'); + HttpEncoder.entities.Add("crarr", '↵'); + HttpEncoder.entities.Add("lArr", '⇐'); + HttpEncoder.entities.Add("uArr", '⇑'); + HttpEncoder.entities.Add("rArr", '⇒'); + HttpEncoder.entities.Add("dArr", '⇓'); + HttpEncoder.entities.Add("hArr", '⇔'); + HttpEncoder.entities.Add("forall", '∀'); + HttpEncoder.entities.Add("part", '∂'); + HttpEncoder.entities.Add("exist", '∃'); + HttpEncoder.entities.Add("empty", '∅'); + HttpEncoder.entities.Add("nabla", '∇'); + HttpEncoder.entities.Add("isin", '∈'); + HttpEncoder.entities.Add("notin", '∉'); + HttpEncoder.entities.Add("ni", '∋'); + HttpEncoder.entities.Add("prod", '∏'); + HttpEncoder.entities.Add("sum", '∑'); + HttpEncoder.entities.Add("minus", '−'); + HttpEncoder.entities.Add("lowast", '∗'); + HttpEncoder.entities.Add("radic", '√'); + HttpEncoder.entities.Add("prop", '∝'); + HttpEncoder.entities.Add("infin", '∞'); + HttpEncoder.entities.Add("ang", '∠'); + HttpEncoder.entities.Add("and", '∧'); + HttpEncoder.entities.Add("or", '∨'); + HttpEncoder.entities.Add("cap", '∩'); + HttpEncoder.entities.Add("cup", '∪'); + HttpEncoder.entities.Add("int", '∫'); + HttpEncoder.entities.Add("there4", '∴'); + HttpEncoder.entities.Add("sim", '∼'); + HttpEncoder.entities.Add("cong", '≅'); + HttpEncoder.entities.Add("asymp", '≈'); + HttpEncoder.entities.Add("ne", '≠'); + HttpEncoder.entities.Add("equiv", '≡'); + HttpEncoder.entities.Add("le", '≤'); + HttpEncoder.entities.Add("ge", '≥'); + HttpEncoder.entities.Add("sub", '⊂'); + HttpEncoder.entities.Add("sup", '⊃'); + HttpEncoder.entities.Add("nsub", '⊄'); + HttpEncoder.entities.Add("sube", '⊆'); + HttpEncoder.entities.Add("supe", '⊇'); + HttpEncoder.entities.Add("oplus", '⊕'); + HttpEncoder.entities.Add("otimes", '⊗'); + HttpEncoder.entities.Add("perp", '⊥'); + HttpEncoder.entities.Add("sdot", '⋅'); + HttpEncoder.entities.Add("lceil", '⌈'); + HttpEncoder.entities.Add("rceil", '⌉'); + HttpEncoder.entities.Add("lfloor", '⌊'); + HttpEncoder.entities.Add("rfloor", '⌋'); + HttpEncoder.entities.Add("lang", '〈'); + HttpEncoder.entities.Add("rang", '〉'); + HttpEncoder.entities.Add("loz", '◊'); + HttpEncoder.entities.Add("spades", '♠'); + HttpEncoder.entities.Add("clubs", '♣'); + HttpEncoder.entities.Add("hearts", '♥'); + HttpEncoder.entities.Add("diams", '♦'); + HttpEncoder.entities.Add("quot", '"'); + HttpEncoder.entities.Add("amp", '&'); + HttpEncoder.entities.Add("lt", '<'); + HttpEncoder.entities.Add("gt", '>'); + HttpEncoder.entities.Add("OElig", 'Œ'); + HttpEncoder.entities.Add("oelig", 'œ'); + HttpEncoder.entities.Add("Scaron", 'Š'); + HttpEncoder.entities.Add("scaron", 'š'); + HttpEncoder.entities.Add("Yuml", 'Ÿ'); + HttpEncoder.entities.Add("circ", 'ˆ'); + HttpEncoder.entities.Add("tilde", '˜'); + HttpEncoder.entities.Add("ensp", ' '); + HttpEncoder.entities.Add("emsp", ' '); + HttpEncoder.entities.Add("thinsp", ' '); + HttpEncoder.entities.Add("zwnj", '\u200C'); + HttpEncoder.entities.Add("zwj", '\u200D'); + HttpEncoder.entities.Add("lrm", '\u200E'); + HttpEncoder.entities.Add("rlm", '\u200F'); + HttpEncoder.entities.Add("ndash", '–'); + HttpEncoder.entities.Add("mdash", '—'); + HttpEncoder.entities.Add("lsquo", '‘'); + HttpEncoder.entities.Add("rsquo", '’'); + HttpEncoder.entities.Add("sbquo", '‚'); + HttpEncoder.entities.Add("ldquo", '“'); + HttpEncoder.entities.Add("rdquo", '”'); + HttpEncoder.entities.Add("bdquo", '„'); + HttpEncoder.entities.Add("dagger", '†'); + HttpEncoder.entities.Add("Dagger", '‡'); + HttpEncoder.entities.Add("permil", '‰'); + HttpEncoder.entities.Add("lsaquo", '‹'); + HttpEncoder.entities.Add("rsaquo", '›'); + HttpEncoder.entities.Add("euro", '€'); + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs.meta b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs.meta new file mode 100644 index 000000000..a7ba535f7 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpEncoder.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3c9f699c227f48e381db521abe59c2e1 +timeCreated: 1701789490 \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs new file mode 100644 index 000000000..ea17c9e3d --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs @@ -0,0 +1,230 @@ +// MIRROR CHANGE: drop in Codice.Utils HttpUtility subset to not depend on Unity's plastic scm package +// SOURCE: Unity Plastic SCM package + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.IO; +using System.Text; + +namespace Edgegap.Codice.Utils // MIRROR CHANGE: namespace Edgegap.* to not collide if anyone has Plastic SCM installed already +{ + public sealed class HttpUtility + { + private static void WriteCharBytes(IList buf, char ch, Encoding e) + { + if (ch > 'ÿ') + { + Encoding encoding = e; + char[] chars = new char[1]{ ch }; + foreach (byte num in encoding.GetBytes(chars)) + buf.Add((object) num); + } + else + buf.Add((object) (byte) ch); + } + + public static string UrlDecode(string s, Encoding e) + { + if (null == s) + return (string) null; + if (s.IndexOf('%') == -1 && s.IndexOf('+') == -1) + return s; + if (e == null) + e = Encoding.UTF8; + long length = (long) s.Length; + List buf = new List(); + for (int index = 0; (long) index < length; ++index) + { + char ch = s[index]; + if (ch == '%' && (long) (index + 2) < length && s[index + 1] != '%') + { + if (s[index + 1] == 'u' && (long) (index + 5) < length) + { + int num = HttpUtility.GetChar(s, index + 2, 4); + if (num != -1) + { + HttpUtility.WriteCharBytes((IList) buf, (char) num, e); + index += 5; + } + else + HttpUtility.WriteCharBytes((IList) buf, '%', e); + } + else + { + int num; + if ((num = HttpUtility.GetChar(s, index + 1, 2)) != -1) + { + HttpUtility.WriteCharBytes((IList) buf, (char) num, e); + index += 2; + } + else + HttpUtility.WriteCharBytes((IList) buf, '%', e); + } + } + else if (ch == '+') + HttpUtility.WriteCharBytes((IList) buf, ' ', e); + else + HttpUtility.WriteCharBytes((IList) buf, ch, e); + } + byte[] array = buf.ToArray(); + return e.GetString(array); + } + + private static int GetInt(byte b) + { + char ch = (char) b; + if (ch >= '0' && ch <= '9') + return (int) ch - 48; + if (ch >= 'a' && ch <= 'f') + return (int) ch - 97 + 10; + return ch >= 'A' && ch <= 'F' ? (int) ch - 65 + 10 : -1; + } + + private static int GetChar(string str, int offset, int length) + { + int num1 = 0; + int num2 = length + offset; + for (int index = offset; index < num2; ++index) + { + char b = str[index]; + if (b > '\u007F') + return -1; + int num3 = HttpUtility.GetInt((byte) b); + if (num3 == -1) + return -1; + num1 = (num1 << 4) + num3; + } + return num1; + } + + public static string UrlEncode(string str) => HttpUtility.UrlEncode(str, Encoding.UTF8); + + public static string UrlEncode(string s, Encoding Enc) + { + if (s == null) + return (string) null; + if (s == string.Empty) + return string.Empty; + bool flag = false; + int length = s.Length; + for (int index = 0; index < length; ++index) + { + char c = s[index]; + if ((c < '0' || c < 'A' && c > '9' || c > 'Z' && c < 'a' || c > 'z') && !HttpEncoder.NotEncoded(c)) + { + flag = true; + break; + } + } + if (!flag) + return s; + byte[] bytes1 = new byte[Enc.GetMaxByteCount(s.Length)]; + int bytes2 = Enc.GetBytes(s, 0, s.Length, bytes1, 0); + return Encoding.ASCII.GetString(HttpUtility.UrlEncodeToBytes(bytes1, 0, bytes2)); + } + + public static byte[] UrlEncodeToBytes(byte[] bytes, int offset, int count) => bytes == null ? (byte[]) null : HttpEncoder.Current.UrlEncode(bytes, offset, count); + + public static string HtmlDecode(string s) + { + if (s == null) + return (string) null; + using (StringWriter output = new StringWriter()) + { + HttpEncoder.Current.HtmlDecode(s, (TextWriter) output); + return output.ToString(); + } + } + + public static NameValueCollection ParseQueryString(string query) => HttpUtility.ParseQueryString(query, Encoding.UTF8); + + public static NameValueCollection ParseQueryString( + string query, + Encoding encoding) + { + if (query == null) + throw new ArgumentNullException(nameof (query)); + if (encoding == null) + throw new ArgumentNullException(nameof (encoding)); + if (query.Length == 0 || query.Length == 1 && query[0] == '?') + return (NameValueCollection) new HttpUtility.HttpQSCollection(); + if (query[0] == '?') + query = query.Substring(1); + NameValueCollection result = (NameValueCollection) new HttpUtility.HttpQSCollection(); + HttpUtility.ParseQueryString(query, encoding, result); + return result; + } + + internal static void ParseQueryString( + string query, + Encoding encoding, + NameValueCollection result) + { + if (query.Length == 0) + return; + string str1 = HttpUtility.HtmlDecode(query); + int length = str1.Length; + int num1 = 0; + bool flag = true; + while (num1 <= length) + { + int startIndex = -1; + int num2 = -1; + for (int index = num1; index < length; ++index) + { + if (startIndex == -1 && str1[index] == '=') + startIndex = index + 1; + else if (str1[index] == '&') + { + num2 = index; + break; + } + } + if (flag) + { + flag = false; + if (str1[num1] == '?') + ++num1; + } + string name; + if (startIndex == -1) + { + name = (string) null; + startIndex = num1; + } + else + name = HttpUtility.UrlDecode(str1.Substring(num1, startIndex - num1 - 1), encoding); + if (num2 < 0) + { + num1 = -1; + num2 = str1.Length; + } + else + num1 = num2 + 1; + string str2 = HttpUtility.UrlDecode(str1.Substring(startIndex, num2 - startIndex), encoding); + result.Add(name, str2); + if (num1 == -1) + break; + } + } + + private sealed class HttpQSCollection : NameValueCollection + { + public override string ToString() + { + int count = this.Count; + if (count == 0) + return ""; + StringBuilder stringBuilder = new StringBuilder(); + string[] allKeys = this.AllKeys; + for (int index = 0; index < count; ++index) + stringBuilder.AppendFormat("{0}={1}&", (object) allKeys[index], (object) HttpUtility.UrlEncode(this[allKeys[index]])); + if (stringBuilder.Length > 0) + --stringBuilder.Length; + return stringBuilder.ToString(); + } + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs.meta b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs.meta new file mode 100644 index 000000000..578640ebd --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dependencies/HttpUtility.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6f83f468a8b546fd92606db56038f9e6 +timeCreated: 1701785025 \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Dockerfile b/Assets/Mirror/Hosting/Edgegap/Dockerfile new file mode 100644 index 000000000..258a141dc --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:bionic + +ARG DEBIAN_FRONTEND=noninteractive + +COPY Builds/EdgegapServer /root/build/ + +WORKDIR /root/ + +RUN chmod +x /root/build/ServerBuild + +RUN apt-get update && \ + apt-get install -y ca-certificates && \ + apt-get clean && \ + update-ca-certificates + +ENTRYPOINT [ "/root/build/ServerBuild", "-batchmode", "-nographics"] diff --git a/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta b/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta new file mode 100644 index 000000000..654f29901 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Dockerfile.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 78b80371aabba1d48aac39ec7ccfe7c5 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef b/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef new file mode 100644 index 000000000..e9811e2c2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef @@ -0,0 +1,20 @@ +{ + "name": "Edgegap", + "rootNamespace": "", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [ + { + "name": "com.unity.nuget.newtonsoft-json", + "expression": "", + "define": "NEWTONSOFT_JSON" + } + ], + "noEngineReferences": false +} \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef.meta b/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef.meta new file mode 100644 index 000000000..6e3765a55 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Edgegap.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 58ff3a2ca929d114eaf0ca373ff1e07a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor.meta b/Assets/Mirror/Hosting/Edgegap/Editor.meta new file mode 100755 index 000000000..e5593273f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 635b395f47dc9f742b4d71144921bb0d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api.meta new file mode 100755 index 000000000..bb66b470b --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 6d2a4589d6738cb4b82bb1ceebd1453f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs new file mode 100755 index 000000000..fd0490189 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Specialized; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +// using Codice.Utils; // MIRROR CHANGE +using Edgegap.Codice.Utils; // MIRROR CHANGE +using UnityEngine; + +namespace Edgegap.Editor.Api +{ + /// + /// Handles base URL and common methods for all Edgegap APIs. + /// + public abstract class EdgegapApiBase + { + #region Vars + private readonly HttpClient _httpClient = new HttpClient(); // Base address set // MIRROR CHANGE: Unity 2020 support + protected ApiEnvironment SelectedApiEnvironment { get; } + protected EdgegapWindowMetadata.LogLevel LogLevel { get; set; } + protected bool IsLogLevelDebug => LogLevel == EdgegapWindowMetadata.LogLevel.Debug; + + /// Based on SelectedApiEnvironment. + /// + private string GetBaseUrl() => + SelectedApiEnvironment == ApiEnvironment.Staging + ? ApiEnvironment.Staging.GetApiUrl() + : ApiEnvironment.Console.GetApiUrl(); + #endregion // Vars + + + /// "console" || "staging-console"? + /// Without the "token " prefix, although we'll clear this if present + /// You may want more-verbose logs other than errs + protected EdgegapApiBase( + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + { + this.SelectedApiEnvironment = apiEnvironment; + + this._httpClient.BaseAddress = new Uri($"{GetBaseUrl()}/"); + this._httpClient.DefaultRequestHeaders.Accept.Add( + new MediaTypeWithQualityHeaderValue("application/json")); + + string cleanedApiToken = apiToken.Replace("token ", ""); // We already prefixed token below + this._httpClient.DefaultRequestHeaders.Authorization = + new AuthenticationHeaderValue("token", cleanedApiToken); + + this.LogLevel = logLevel; + } + + + #region HTTP Requests + /// + /// POST | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. + /// + /// + /// Serialize to your model via Newtonsoft + /// + /// - Success => returns HttpResponseMessage result + /// - Error => Catches errs => returns null (no rethrow) + /// + protected async Task PostAsync(string relativePath = "", string json = "{}") + { + StringContent stringContent = CreateStringContent(json); + Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize POST uri: Can't end with `/`. + + if (IsLogLevelDebug) + Debug.Log($"PostAsync to: `{uri}` with json: `{json}`"); + + try + { + return await ExecuteRequestAsync(() => _httpClient.PostAsync(uri, stringContent)); + } + catch (Exception e) + { + Debug.LogError($"Error: {e}"); + throw; + } + } + + /// + /// PATCH | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. + /// + /// + /// Serialize to your model via Newtonsoft + /// + /// - Success => returns HttpResponseMessage result + /// - Error => Catches errs => returns null (no rethrow) + /// + protected async Task PatchAsync(string relativePath = "", string json = "{}") + { + StringContent stringContent = CreateStringContent(json); + Uri uri = new Uri(_httpClient.BaseAddress, relativePath); // Normalize PATCH uri: Can't end with `/`. + + if (IsLogLevelDebug) + Debug.Log($"PatchAsync to: `{uri}` with json: `{json}`"); + + // (!) As of 11/15/2023, .PatchAsync() is "unsupported by Unity" -- so we manually set the verb and SendAsync() + // Create the request manually + HttpRequestMessage patchRequest = new HttpRequestMessage(new HttpMethod("PATCH"), uri) + { + Content = stringContent, + }; + + try + { + return await ExecuteRequestAsync(() => _httpClient.SendAsync(patchRequest)); + } + catch (Exception e) + { + Debug.LogError($"Error: {e}"); + throw; + } + } + + /// + /// GET | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. + /// + /// + /// + /// To append to the URL; eg: "foo=0&bar=1" + /// (!) First query key should prefix nothing, as shown + /// + /// - Success => returns HttpResponseMessage result + /// - Error => Catches errs => returns null (no rethrow) + /// + protected async Task GetAsync(string relativePath = "", string customQuery = "") + { + string completeRelativeUri = prepareEdgegapUriWithQuery( + relativePath, + customQuery); + + if (IsLogLevelDebug) + Debug.Log($"GetAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); + + try + { + return await ExecuteRequestAsync(() => _httpClient.GetAsync(completeRelativeUri)); + } + catch (Exception e) + { + Debug.LogError($"Error: {e}"); + throw; + } + } + + /// + /// DELETE | We already added "https://api.edgegap.com/" (or similar) BaseAddress via constructor. + /// + /// + /// + /// To append to the URL; eg: "foo=0&bar=1" + /// (!) First query key should prefix nothing, as shown + /// + /// - Success => returns HttpResponseMessage result + /// - Error => Catches errs => returns null (no rethrow) + /// + protected async Task DeleteAsync(string relativePath = "", string customQuery = "") + { + string completeRelativeUri = prepareEdgegapUriWithQuery( + relativePath, + customQuery); + + if (IsLogLevelDebug) + Debug.Log($"DeleteAsync to: `{completeRelativeUri} with customQuery: `{customQuery}`"); + + try + { + return await ExecuteRequestAsync(() => _httpClient.DeleteAsync(completeRelativeUri)); + } + catch (Exception e) + { + Debug.LogError($"Error: {e}"); + throw; + } + } + + /// POST || GET + /// + /// + /// + private static async Task ExecuteRequestAsync( + Func> requestFunc, + CancellationToken cancellationToken = default) + { + HttpResponseMessage response = null; + try + { + response = await requestFunc(); + } + catch (HttpRequestException e) + { + Debug.LogError($"HttpRequestException: {e.Message}"); + return null; + } + catch (TaskCanceledException e) + { + if (cancellationToken.IsCancellationRequested) + Debug.LogError("Task was cancelled by caller."); + else + Debug.LogError($"TaskCanceledException: Timeout - {e.Message}"); + return null; + } + catch (Exception e) // Generic exception handler + { + Debug.LogError($"Unexpected error occurred: {e.Message}"); + return null; + } + + // Check for a successful status code + if (response == null) + { + Debug.Log("!Success (null response) - returning 500"); + return CreateUnknown500Err(); + } + + if (!response.IsSuccessStatusCode) + { + HttpMethod httpMethod = response.RequestMessage.Method; + Debug.Log($"!Success: {(short)response.StatusCode} {response.ReasonPhrase} - " + + $"{httpMethod} | {response.RequestMessage.RequestUri}` - " + + $"{response.Content?.ReadAsStringAsync().Result}"); + } + + return response; + } + #endregion // HTTP Requests + + + #region Utils + /// Creates a UTF-8 encoded application/json + json obj + /// Arbitrary json obj + /// + private StringContent CreateStringContent(string json = "{}") => + new StringContent(json, Encoding.UTF8, "application/json"); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + private static HttpResponseMessage CreateUnknown500Err() => + new HttpResponseMessage(HttpStatusCode.InternalServerError); // 500 - Unknown // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + /// + /// Merges Edgegap-required query params (source) -> merges with custom query -> normalizes. + /// + /// + /// + /// + private string prepareEdgegapUriWithQuery(string relativePath, string customQuery) + { + // Create UriBuilder using the BaseAddress + UriBuilder uriBuilder = new UriBuilder(_httpClient.BaseAddress); + + // Add the relative path to the UriBuilder's path + uriBuilder.Path += relativePath; + + // Parse the existing query from the UriBuilder + NameValueCollection query = HttpUtility.ParseQueryString(uriBuilder.Query); + + // Add default "source=unity" param + query["source"] = "unity"; + + // Parse and merge the custom query parameters + NameValueCollection customParams = HttpUtility.ParseQueryString(customQuery); + foreach (string key in customParams) + { + query[key] = customParams[key]; + } + + // Set the merged query back to the UriBuilder + uriBuilder.Query = query.ToString(); + + // Extract the complete relative URI and return it + return uriBuilder.Uri.PathAndQuery; + } + #endregion // Utils + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs.meta new file mode 100755 index 000000000..735b9236e --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapApiBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 50379f30f5137224aa05a5c7b6b5ebba +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs new file mode 100755 index 000000000..4e7eab14e --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs @@ -0,0 +1,148 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Edgegap.Editor.Api.Models.Requests; +using Edgegap.Editor.Api.Models.Results; + +namespace Edgegap.Editor.Api +{ + /// + /// Wraps the v1/app API endpoint: Applications Control API. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications + /// + public class EdgegapAppApi : EdgegapApiBase + { + public EdgegapAppApi( + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + : base(apiEnvironment, apiToken, logLevel) + { + } + + + #region API Methods + /// + /// POST to v1/app + /// - Create an application that will regroup application versions. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/application-post + /// + /// + /// Http info with GetCreateAppResult data model + /// - Success: 200 + /// - Fail: 409 (app already exists), 400 (reached limit) + /// + public async Task> CreateApp(CreateAppRequest request) + { + HttpResponseMessage response = await PostAsync("v1/app", request.ToString()); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// GET to v1/app + /// - Get an application that will regroup application versions. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/application-post + /// + /// + /// Http info with GetCreateAppResult data model + /// - Success: 200 + /// + public async Task> GetApp(string appName) + { + HttpResponseMessage response = await GetAsync($"v1/app/{appName}"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// PATCH to v1/app/{app_name}/version/{version_name} + /// - Update an *existing* application version with new specifications. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-versions-patch + /// + /// + /// Http info with UpdateAppVersionRequest data model + /// - Success: 200 + /// + public async Task> UpdateAppVersion(UpdateAppVersionRequest request) + { + string relativePath = $"v1/app/{request.AppName}/version/{request.VersionName}"; + HttpResponseMessage response = await PatchAsync(relativePath, request.ToString()); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// POST to v1/app/{app_name}/version + /// - Create an new application version with new specifications. + /// - API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-version-post + /// + /// + /// Http info with UpdateAppVersionRequest data model + /// - Success: 200 (no result model) + /// - Fail: 409 (app already exists), 400 (reached limit) + /// + public async Task> CreateAppVersion(CreateAppVersionRequest request) + { + string relativePath = $"v1/app/{request.AppName}/version"; + HttpResponseMessage response = await PostAsync(relativePath, request.ToString()); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + + if (!isSuccess) + return result; + + return result; + } + #endregion // API Methods + + + #region Chained API Methods + /// + /// PATCH and/or POST to v1/app/: Upsert an *existing* application version with new specifications. + /// - Consumes either 1 or 2 API calls: 1st tries to PATCH, then POST if PATCH fails (!exists). + /// - API POST Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-version-post + /// - API PATCH Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-versions-patch + /// + /// + /// Http info with UpdateAppVersionRequest data model + /// - Success: 200 (no result model) + /// - Fail: 409 (app already exists), 400 (reached limit) + /// + public async Task> UpsertAppVersion(UpdateAppVersionRequest request) + { + EdgegapHttpResult result = await UpdateAppVersion(request); // PATCH + + if (result.HasErr) + { + // Try to create, instead + CreateAppVersionRequest createAppVersionRequest = CreateAppVersionRequest.FromUpdateRequest(request); + result = await CreateAppVersion(createAppVersionRequest); // POST + } + + bool isSuccess = result.StatusCode == HttpStatusCode.OK; // 200 + + if (!isSuccess) + return result; + + return result; + } + #endregion // Chained API Methods + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs.meta new file mode 100755 index 000000000..7f2170156 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapAppApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3b0b3b865abe64b49a4000294c4e9593 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs new file mode 100755 index 000000000..dda3ce24a --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs @@ -0,0 +1,177 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Edgegap.Editor.Api.Models.Requests; +using Edgegap.Editor.Api.Models.Results; +using UnityEngine.Assertions; + +namespace Edgegap.Editor.Api +{ + /// + /// Wraps the v1/[deploy | status | stop] API endpoints: Deployments Control API. + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments + /// + public class EdgegapDeploymentsApi : EdgegapApiBase + { + public EdgegapDeploymentsApi( + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + : base(apiEnvironment, apiToken, logLevel) + { + } + + + #region API Methods + /// + /// POST v1/deploy + /// - Create a new deployment. Deployment is a server instance of your application version. + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments + /// + /// + /// Http info with CreateDeploymentResult data model + /// - Success: 200 + /// + public async Task> CreateDeploymentAsync( + CreateDeploymentRequest request) + { + HttpResponseMessage response = await PostAsync("v1/deploy", request.ToString()); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// GET v1/status/{requestId} + /// - Retrieve the information for a deployment. + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deployment-status-get + /// + /// + /// Unique Identifier to keep track of your request across all Arbitrium ecosystem. + /// It's included in the response of the app deploy. Ex: "93924761ccde" + /// + /// Http info with GetDeploymentStatusResult data model + /// - Success: 200 + /// + public async Task> GetDeploymentStatusAsync(string requestId) + { + HttpResponseMessage response = await GetAsync($"v1/status/{requestId}"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + + /// + /// DELETE v1/stop/{requestId} + /// - Delete an instance of deployment. It will stop the running container and all its games. + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deployment-status-get + /// + /// + /// Unique Identifier to keep track of your request across all Arbitrium ecosystem. + /// It's included in the response of the app deploy. Ex: "93924761ccde" + /// + /// Http info with GetDeploymentStatusResult data model + /// - Success: 200 + /// + public async Task> StopActiveDeploymentAsync(string requestId) + { + HttpResponseMessage response = await DeleteAsync($"v1/stop/{requestId}"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + #endregion // API Methods + + + #region Chained API Methods + /// + /// POST v1/deploy => GET v1/status/{requestId} + /// - Create a new deployment. Deployment is a server instance of your application version. + /// - Then => await READY status. + /// - API Doc | https://docs.edgegap.com/api/#tag/Deployments + /// + /// + /// Http info with CreateDeploymentResult data model (with a READY deployment status) + /// - Success: 200 + /// - Error: If createResult.HasErr, returns createResult + /// + public async Task> CreateDeploymentAwaitReadyStatusAsync( + CreateDeploymentRequest request, TimeSpan pollInterval) + { + EdgegapHttpResult createResponse = await CreateDeploymentAsync(request); + + // Create => + bool isCreateSuccess = createResponse.StatusCode == HttpStatusCode.OK; // 200 + if (!isCreateSuccess) + return createResponse; + + // Await Status READY => + string requestId = createResponse.Data.RequestId; + _ = await AwaitReadyStatusAsync(requestId, pollInterval); + + // Return no matter what the result; no need to validate + return createResponse; + } + + /// If you recently deployed but want to await READY status. + /// + /// + public async Task> AwaitReadyStatusAsync( + string requestId, + TimeSpan pollInterval) + { + Assert.IsTrue(!string.IsNullOrEmpty(requestId)); // Validate + + EdgegapHttpResult statusResponse = null; + CancellationTokenSource cts = new CancellationTokenSource (TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS)); + bool isReady = false; + + while (!isReady && !cts.Token.IsCancellationRequested) + { + await Task.Delay(pollInterval, cts.Token); + statusResponse = await GetDeploymentStatusAsync(requestId); + isReady = statusResponse.Data.CurrentStatus == EdgegapWindowMetadata.READY_STATUS; + } + + return statusResponse; + } + + /// If you recently stopped a deployment, but want to await TERMINATED (410) status. + /// + /// + public async Task> AwaitTerminatedDeleteStatusAsync( + string requestId, + TimeSpan pollInterval) + { + EdgegapHttpResult deleteResponse = null; + CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMinutes( // MIRROR CHANGE: 'new()' not supported in Unity 2020 + EdgegapWindowMetadata.DEPLOYMENT_AWAIT_READY_STATUS_TIMEOUT_MINS)); + bool isStopped = false; + + while (!isStopped && !cts.Token.IsCancellationRequested) + { + await Task.Delay(pollInterval, cts.Token); + deleteResponse = await StopActiveDeploymentAsync(requestId); + isStopped = deleteResponse.StatusCode == HttpStatusCode.Gone; // 410 + } + + return deleteResponse; + } + #endregion Chained API Methods + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs.meta new file mode 100755 index 000000000..08f9b3465 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapDeploymentsApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 37ecdc6abda4402419438f2284ef2d95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs new file mode 100755 index 000000000..86e4b26a3 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs @@ -0,0 +1,47 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Edgegap.Editor.Api.Models.Results; + +namespace Edgegap.Editor.Api +{ + /// + /// Wraps the v1/ip API endpoint: "IP Lookup" API. + /// - API Doc | https://docs.edgegap.com/api/#tag/IP-Lookup + /// + public class EdgegapIpApi : EdgegapApiBase + { + public EdgegapIpApi( + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + : base(apiEnvironment, apiToken, logLevel) + { + } + + + #region API Methods + /// + /// GET to v1/app + /// - Retrieve your public IP address. + /// - API Doc | https://docs.edgegap.com/api/#tag/IP-Lookup/operation/IP + /// + /// + /// Http info with GetCreateAppResult data model + /// - Success: 200 + /// - Fail: 409 (app already exists), 400 (reached limit) + /// + public async Task> GetYourPublicIp() + { + HttpResponseMessage response = await GetAsync("v1/ip"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + bool isSuccess = response.StatusCode == HttpStatusCode.OK; // 200 + if (!isSuccess) + return result; + + return result; + } + #endregion // API Methods + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs.meta new file mode 100755 index 000000000..fa966ab9e --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapIpApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fac4a7425623f39488af09d60549313e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs new file mode 100755 index 000000000..5d4a25ff5 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs @@ -0,0 +1,52 @@ +using System.Net.Http; +using System.Threading.Tasks; +using Edgegap.Editor.Api.Models.Results; +using Newtonsoft.Json.Linq; + +namespace Edgegap.Editor.Api +{ + /// Wraps the v1/wizard API endpoint. Used for internal purposes. + public class EdgegapWizardApi : EdgegapApiBase + { + /// Extended path after the base uri + public EdgegapWizardApi( + ApiEnvironment apiEnvironment, + string apiToken, + EdgegapWindowMetadata.LogLevel logLevel = EdgegapWindowMetadata.LogLevel.Error) + : base(apiEnvironment, apiToken, logLevel) + { + } + + + #region API Methods + /// POST to v1/wizard/init-quick-start + /// + /// Http info with no explicit data model + /// - Success: 204 (no result model) + /// + public async Task InitQuickStart() + { + string json = new JObject { ["source"] = "unity" }.ToString(); + HttpResponseMessage response = await PostAsync("v1/wizard/init-quick-start", json); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + return result; + } + + /// GET to v1/wizard/registry-credentials + /// + /// - Http info with GetRegistryCredentialsResult data model + /// - Success: 200 + /// - Error: Likely if called before a successful InitQuickStart(), + /// or if called in a staging env. Soon, this will be available in production. + /// + public async Task> GetRegistryCredentials() + { + HttpResponseMessage response = await GetAsync("v1/wizard/registry-credentials"); + EdgegapHttpResult result = new EdgegapHttpResult(response); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + return result; + } + #endregion // API Methods + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs.meta new file mode 100755 index 000000000..5e167becb --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/EdgegapWizardApi.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6986ee67361f0b45928ccd70c7ab12c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models.meta new file mode 100755 index 000000000..2c5a79310 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aed107c714fce71449ef56590221c567 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs new file mode 100755 index 000000000..782bc7b1e --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models +{ + /// + /// Used in `UpdateAppVersionRequest`, `CreateAppVersionRequest`. + /// For GetDeploymentStatusResult, see DeploymentPortsData + /// + public class AppPortsData + { + /// 1024~49151; Default 7770 + [JsonProperty("port")] + public int Port { get; set; } = EdgegapWindowMetadata.PORT_DEFAULT; + + /// Default "UDP" + [JsonProperty("protocol")] + public string ProtocolStr { get; set; } = EdgegapWindowMetadata.DEFAULT_PROTOCOL_TYPE.ToString(); + + [JsonProperty("to_check")] + public bool ToCheck { get; set; } = true; + + [JsonProperty("tls_upgrade")] + public bool TlsUpgrade { get; set; } + + [JsonProperty("name")] + public string PortName { get; set; } = "Game Port"; + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs.meta new file mode 100755 index 000000000..d7e6083c0 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/AppPortsData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b6d4864ea3706574fb35920c6fab46fa +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs new file mode 100755 index 000000000..972205b70 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models +{ + /// Used in `GetDeploymentStatus`. + public class DeploymentPortsData + { + [JsonProperty("external")] + public int External { get; set; } + + [JsonProperty("internal")] + public int Internal { get; set; } + + [JsonProperty("protocol")] + public string Protocol { get; set; } + + [JsonProperty("name")] + public string PortName { get; set; } + + [JsonProperty("tls_upgrade")] + public bool TlsUpgrade { get; set; } + + [JsonProperty("link")] + public string Link { get; set; } + + [JsonProperty("proxy")] + public int? Proxy { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs.meta new file mode 100755 index 000000000..0efdcc2c1 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/DeploymentPortsData.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 62d51b44b8414c9f968ca607ccb06b7e +timeCreated: 1701522748 \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs new file mode 100755 index 000000000..d9c89bb1c --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models +{ + public class LocationData + { + [JsonProperty("city")] + public string City { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + + [JsonProperty("continent")] + public string Continent { get; set; } + + [JsonProperty("administrative_division")] + public string AdministrativeDivision { get; set; } + + [JsonProperty("timezone")] + public string Timezone { get; set; } + + [JsonProperty("latitude")] + public double Latitude { get; set; } + + [JsonProperty("longitude")] + public double Longitude { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs.meta new file mode 100755 index 000000000..c99e80189 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/LocationData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 57eed0dbd556e074c992cf6599a1f6bd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs new file mode 100755 index 000000000..d72086db0 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs @@ -0,0 +1,18 @@ +namespace Edgegap.Editor.Api.Models +{ + /// + /// Unity default: UDP. + /// (!) UDP !works in WebGL. + /// + public enum ProtocolType + { + /// Unity default - fastest; !works in WebGL. + UDP, + + /// Slower, but more reliable; works in WebGL. + TCP, + + /// Slower, but more reliable; works in WebGL. + WS, + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs.meta new file mode 100755 index 000000000..06560580a --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/ProtocolType.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be5acd63e783b364ebdbb783639e2d32 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests.meta new file mode 100755 index 000000000..bcd87e449 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d1b2a5c481353934f906c30ba047df9b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs new file mode 100755 index 000000000..91e3c2616 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs @@ -0,0 +1,55 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Requests +{ + /// + /// Request model for https://docs.edgegap.com/api/#tag/Applications/operation/application-post + /// + public class CreateAppRequest + { + #region Required + /// *The application name. + [JsonProperty("name")] + public string AppName { get; set; } + #endregion // Required + + + #region Optional + /// *If the application can be deployed. + [JsonProperty("is_active")] + public bool IsActive { get; set; } + + /// *Image base64 string. + [JsonProperty("image")] + public string Image { get; set; } + + /// If the telemetry agent is installed on the versions of this app. + [JsonProperty("is_telemetry_agent_active")] + public bool IsTelemetryAgentActive { get; set; } + #endregion // Optional + + + /// Used by Newtonsoft + public CreateAppRequest() + { + } + + /// Init with required info + /// The application name + /// If the application can be deployed + /// Image base64 string + public CreateAppRequest( + string appName, + bool isActive, + string image) + { + this.AppName = appName; + this.IsActive = isActive; + this.Image = image; + } + + /// Parse to json str + public override string ToString() => + JsonConvert.SerializeObject(this); + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs.meta new file mode 100755 index 000000000..268be5cc2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0a492d7c515b8894ea30b37db6b7efe4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs new file mode 100755 index 000000000..360ed43b6 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs @@ -0,0 +1,225 @@ +using System; +using Newtonsoft.Json; +using UnityEngine; + +namespace Edgegap.Editor.Api.Models.Requests +{ + /// + /// Request model for `POST v1/app/{app_name}/version`. + /// API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-version-post + /// + public class CreateAppVersionRequest + { + #region Required + /// *The name of the application associated. + [JsonIgnore] // *Path var + public string AppName { get; set; } + + /// *The name of the application associated. + [JsonProperty("name")] + public string VersionName { get; set; } = EdgegapWindowMetadata.DEFAULT_VERSION_TAG; + + /// *The tag of your image. Default == "latest". + /// "0.1.2" || "latest" (although "latest" !recommended; use actual versions in production) + [JsonProperty("docker_tag")] + public string DockerTag { get; set; } = EdgegapWindowMetadata.DEFAULT_VERSION_TAG; + + /// *The name of your image. + /// "edgegap/demo" || "myCompany-someId/mylowercaseapp" + [JsonProperty("docker_image")] + public string DockerImage { get; set; } = ""; + + /// *The Repository where the image is. + /// "registry.edgegap.com" || "harbor.edgegap.com" || "docker.io" + [JsonProperty("docker_repository")] + public string DockerRepository { get; set; } = ""; + + /// *Units of vCPU needed (1024 = 1vcpu) + [JsonProperty("req_cpu")] + public int ReqCpu { get; set; } = 256; + + /// *Units of memory in MB needed (1024 = 1 GPU) + [JsonProperty("req_memory")] + public int ReqMemory { get; set; } = 256; + + /// *Required: At least 1 { Port, ProtocolStr }. + [JsonProperty("ports")] + public AppPortsData[] Ports { get; set; } = {}; + + /// The username to access the docker repository + [JsonProperty("private_username")] + public string PrivateUsername { get; set; } = ""; + + /// The Private Password or Token of the username (We recommend to use a token) + [JsonProperty("private_token")] + public string PrivateToken { get; set; } = ""; + #endregion // Required + + + // #region Optional + // [JsonProperty("is_active")] + // public bool IsActive { get; set; } = true; + // + // [JsonProperty("req_video")] + // public int ReqVideo { get; set; } = 256; + // + // [JsonProperty("max_duration")] + // public int MaxDuration { get; set; } = 30; + // + // [JsonProperty("use_telemetry")] + // public bool UseTelemetry { get; set; } = true; + // + // [JsonProperty("inject_context_env")] + // public bool InjectContextEnv { get; set; } = true; + // + // [JsonProperty("whitelisting_active")] + // public bool WhitelistingActive { get; set; } = true; + // + // [JsonProperty("force_cache")] + // public bool ForceCache { get; set; } + // + // [JsonProperty("cache_min_hour")] + // public int CacheMinHour { get; set; } + // + // [JsonProperty("cache_max_hour")] + // public int CacheMaxHour { get; set; } + // + // [JsonProperty("time_to_deploy")] + // public int TimeToDeploy { get; set; } = 15; + // + // [JsonProperty("enable_all_locations")] + // public bool EnableAllLocations { get; set; } + // + // [JsonProperty("termination_grace_period_seconds")] + // public int TerminationGracePeriodSeconds { get; set; } = 5; + // + // [JsonProperty("endpoint_storage")] + // public string EndpointStorage { get; set; } = ""; + // + // [JsonProperty("command")] + // public string Command { get; set; } + // + // [JsonProperty("arguments")] + // public string Arguments { get; set; } + // + // [JsonProperty("verify_image")] + // public bool VerifyImage { get; set; } + // + // [JsonProperty("session_config")] + // public SessionConfigData SessionConfig { get; set; } = new(); + // + // [JsonProperty("probe")] + // public ProbeData Probe { get; set; } = new(); + // + // [JsonProperty("envs")] + // public EnvsData[] Envs { get; set; } = {}; + // + // public class SessionConfigData + // { + // [JsonProperty("kind")] + // public string Kind { get; set; } = "Seat"; + // + // [JsonProperty("sockets")] + // public int Sockets { get; set; } = 10; + // + // [JsonProperty("autodeploy")] + // public bool Autodeploy { get; set; } = true; + // + // [JsonProperty("empty_ttl")] + // public int EmptyTtl { get; set; } = 60; + // + // [JsonProperty("session_max_duration")] + // public int SessionMaxDuration { get; set; } = 60; + // } + // + // + // public class ProbeData + // { + // [JsonProperty("optimal_ping")] + // public int OptimalPing { get; set; } = 60; + // + // [JsonProperty("rejected_ping")] + // public int RejectedPing { get; set; } = 180; + // } + // + // public class EnvsData + // { + // [JsonProperty("key")] + // public string Key { get; set; } + // + // [JsonProperty("value")] + // public string Value { get; set; } + // + // [JsonProperty("is_secret")] + // public bool IsSecret { get; set; } = true; + // } + // #endregion // Optional + + /// Used by Newtonsoft + public CreateAppVersionRequest() + { + } + + /// + /// Init with required info. + /// (!) If looking for refs, also see FromUpdateRequest() builder below. + /// + /// The name of the application. + /// + /// + /// + /// + public CreateAppVersionRequest( + string appName, + string containerRegistryUsername, + string containerRegistryPasswordToken, + int portNum, + ProtocolType protocolType) + { + this.AppName = appName; + this.PrivateUsername = containerRegistryUsername; + this.PrivateToken = containerRegistryPasswordToken; + this.Ports = new AppPortsData[] + { + new AppPortsData() // MIRROR CHANGE: 'new()' not supported in Unity 2020 + { + Port = portNum, + ProtocolStr = protocolType.ToString(), + }, + }; + } + + /// + /// Port from Update PATCH model: If you tried to Update, but !exists, you probably want to create it next. + /// + /// + public static CreateAppVersionRequest FromUpdateRequest(UpdateAppVersionRequest updateRequest) + { + // Convert the updateRequest to JSON + string json = JsonConvert.SerializeObject(updateRequest); + + // Deserialize the JSON back to CreateAppVersionRequest + CreateAppVersionRequest createReq = null; + + try + { + createReq = JsonConvert.DeserializeObject(json); + createReq.AppName = updateRequest.AppName; // Normally JsonIgnored in Update + createReq.VersionName = updateRequest.VersionName; // Normally JsonIgnored in Update + createReq.PrivateUsername = updateRequest.PrivateUsername; + createReq.PrivateToken = updateRequest.PrivateToken; + } + catch (Exception e) + { + Debug.LogError($"Error (when parsing CreateAppVersionRequest from CreateAppVersionRequest): {e}"); + throw; + } + + return createReq; + } + + /// Parse to json str + public override string ToString() => + JsonConvert.SerializeObject(this); + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs.meta new file mode 100755 index 000000000..cf964e404 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateAppVersionRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0bb645e2f9d04384a85739269cc8a4e1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs new file mode 100755 index 000000000..968821cc9 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs @@ -0,0 +1,63 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Requests +{ + /// + /// Request model for `POST v1/deploy`. + /// API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deploy + /// + public class CreateDeploymentRequest + { + #region Required + /// *Required: The name of the App you want to deploy. + [JsonProperty("app_name")] + public string AppName { get; set; } + + /// + /// *Required: The name of the App Version you want to deploy; + /// if not present, the last version created is picked. + /// + [JsonProperty("version_name")] + public string VersionName { get; set; } + + /// + /// *Required: The List of IP of your user. + /// + [JsonProperty("ip_list")] + public string[] IpList { get; set; } + + /// + /// *Required: The list of IP of your user with their location (latitude, longitude). + /// + [JsonProperty("geo_ip_list")] + public string[] GeoIpList { get; set; } = {}; + #endregion // Required + + + /// Used by Newtonsoft + public CreateDeploymentRequest() + { + } + + /// Init with required info; used for a single external IP address. + /// The name of the application. + /// + /// The name of the App Version you want to deploy, if not present, + /// the last version created is picked. + /// + /// Obtain from IpApi. + public CreateDeploymentRequest( + string appName, + string versionName, + string externalIp) + { + this.AppName = appName; + this.VersionName = versionName; + this.IpList = new[] { externalIp }; + } + + /// Parse to json str + public override string ToString() => + JsonConvert.SerializeObject(this); + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs.meta new file mode 100755 index 000000000..3391cdd8e --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/CreateDeploymentRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: aae7b317093230e419bc0f8be1097ea6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs new file mode 100755 index 000000000..8a1d76b1c --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs @@ -0,0 +1,175 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Requests +{ + /// + /// Request model for `PATCH v1/app/{app_name}/version/{version_name}`. + /// Request model for https://docs.edgegap.com/api/#tag/Applications/operation/app-versions-patch + /// TODO: Split "Create" and "Update" into their own, separate models: CTRL+F for "(!)" for more info. + /// + public class UpdateAppVersionRequest + { + #region Required + /// *Required: The name of the application. + [JsonIgnore] // *Path var + public string AppName { get; set; } + #endregion // Required + + + #region Optional + /// The name of the application version. + [JsonIgnore] // *Path var + public string VersionName { get; set; } = EdgegapWindowMetadata.DEFAULT_VERSION_TAG; + + /// At least 1 { Port, ProtocolStr } + [JsonProperty("ports")] + public AppPortsData[] Ports { get; set; } = {}; + + /// The Repository where the image is. + /// "registry.edgegap.com" || "harbor.edgegap.com" || "docker.io" + [JsonProperty("docker_repository")] + public string DockerRepository { get; set; } = ""; + + /// The name of your image. + /// "edgegap/demo" || "myCompany-someId/mylowercaseapp" + [JsonProperty("docker_image")] + public string DockerImage { get; set; } = ""; + + /// The tag of your image. Default == "latest". + /// "0.1.2" || "latest" (although "latest" !recommended; use actual versions in production) + [JsonProperty("docker_tag")] + public string DockerTag { get; set; } = EdgegapWindowMetadata.DEFAULT_VERSION_TAG; + + [JsonProperty("is_active")] + public bool IsActive { get; set; } = true; + + [JsonProperty("private_username")] + public string PrivateUsername { get; set; } = ""; + + [JsonProperty("private_token")] + public string PrivateToken { get; set; } = ""; + + #region (!) Shows in API docs for PATCH, but could be CREATE only? "Unknown Args" + // [JsonProperty("req_cpu")] + // public int ReqCpu { get; set; } = 256; + // + // [JsonProperty("req_memory")] + // public int ReqMemory { get; set; } = 256; + // + // [JsonProperty("req_video")] + // public int ReqVideo { get; set; } = 256; + #endregion // (!) Shows in API docs for PATCH, but could be CREATE only? "Unknown Args" + + [JsonProperty("max_duration")] + public int MaxDuration { get; set; } = 60; + + [JsonProperty("use_telemetry")] + public bool UseTelemetry { get; set; } = true; + + [JsonProperty("inject_context_env")] + public bool InjectContextEnv { get; set; } = true; + + [JsonProperty("whitelisting_active")] + public bool WhitelistingActive { get; set; } = false; + + [JsonProperty("force_cache")] + public bool ForceCache { get; set; } + + [JsonProperty("cache_min_hour")] + public int CacheMinHour { get; set; } + + [JsonProperty("cache_max_hour")] + public int CacheMaxHour { get; set; } + + [JsonProperty("time_to_deploy")] + public int TimeToDeploy { get; set; } = 120; + + [JsonProperty("enable_all_locations")] + public bool EnableAllLocations { get; set; } + + [JsonProperty("termination_grace_period_seconds")] + public int TerminationGracePeriodSeconds { get; set; } = 5; + + // // (!) BUG: Expects empty string "" at minimum; however, empty string will throw server err + // [JsonProperty("endpoint_storage")] + // public string EndpointStorage { get; set; } + + [JsonProperty("command")] + public string Command { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + + // /// + // /// (!) Setting this will trigger a very specific type of game that will affect the AppVersion. + // /// TODO: Is leaving as null the same as commenting out? + // /// + // [JsonProperty("session_config")] + // public SessionConfigData SessionConfig { get; set; } + + [JsonProperty("probe")] + public ProbeData Probe { get; set; } = new ProbeData(); // MIRROR CHANGE: 'new()' not supported in Unity 2020 + + [JsonProperty("envs")] + public EnvsData[] Envs { get; set; } = {}; + + public class SessionConfigData + { + [JsonProperty("kind")] + public string Kind { get; set; } = "Seat"; + + [JsonProperty("sockets")] + public int Sockets { get; set; } = 10; + + [JsonProperty("autodeploy")] + public bool Autodeploy { get; set; } = true; + + [JsonProperty("empty_ttl")] + public int EmptyTtl { get; set; } = 60; + + [JsonProperty("session_max_duration")] + public int SessionMaxDuration { get; set; } = 60; + } + + public class ProbeData + { + [JsonProperty("optimal_ping")] + public int OptimalPing { get; set; } = 60; + + [JsonProperty("rejected_ping")] + public int RejectedPing { get; set; } = 180; + } + + public class EnvsData + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + + [JsonProperty("is_secret")] + public bool IsSecret { get; set; } = true; + } + #endregion // Optional + + /// Used by Newtonsoft + public UpdateAppVersionRequest() + { + } + + /// + /// Init with required info. Default version/tag == "default". + /// Since we're updating, we only require the AppName. + /// + /// The name of the application. + public UpdateAppVersionRequest(string appName) + { + this.AppName = appName; + } + + /// Parse to json str + public override string ToString() => + JsonConvert.SerializeObject(this); + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs.meta new file mode 100755 index 000000000..eca01ed65 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Requests/UpdateAppVersionRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8da9712633ee1e64faca0b960d4bed31 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results.meta new file mode 100755 index 000000000..63ab7622f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa4ceffbc97b8254885a63937def2324 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs new file mode 100755 index 000000000..407d681a2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs @@ -0,0 +1,53 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `POST v1/deploy`. + /// + public class CreateDeploymentResult + { + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("request_dns")] + public string RequestDns { get; set; } + + [JsonProperty("request_app")] + public string RequestApp { get; set; } + + [JsonProperty("request_version")] + public string RequestVersion { get; set; } + + [JsonProperty("request_user_count")] + public int RequestUserCount { get; set; } + + [JsonProperty("city")] + public string City { get; set; } + + [JsonProperty("country")] + public string Country { get; set; } + + [JsonProperty("continent")] + public string Continent { get; set; } + + [JsonProperty("administrative_division")] + public string AdministrativeDivision { get; set; } + + [JsonProperty("tags")] + public string[] Tags { get; set; } + + [JsonProperty("container_log_storage")] + public ContainerLogStorageData ContainerLogStorage { get; set; } + + + public class ContainerLogStorageData + { + [JsonProperty("enabled")] + public bool Enabled { get; set; } + + [JsonProperty("endpoint_storage")] + public string EndpointStorage { get; set; } + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs.meta new file mode 100755 index 000000000..23484d230 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/CreateDeploymentResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8361abc6f84fccd4cba26dc285d335dd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs new file mode 100755 index 000000000..55b03f2c2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// Edgegap error, generally just containing `message` + public class EdgegapErrorResult + { + /// Friendly, UI-facing error message from Edgegap; can be lengthy. + [JsonProperty("message")] + public string ErrorMessage { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs.meta new file mode 100755 index 000000000..be171dd66 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapErrorResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5b29093cb10cf3040b76f4fbe77a435d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs new file mode 100755 index 000000000..55b4892b8 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs @@ -0,0 +1,118 @@ +using System; +using System.Net; +using System.Net.Http; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using UnityEngine; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Wraps the inner json data with outer http info. + /// This class overload contains no json-deserialiable data result. + /// + public class EdgegapHttpResult + { + /// HTTP Status code for the request. + public HttpStatusCode StatusCode { get; } + + /// This could be err, success, or null. + public string Json { get; } + + /// eg: "POST" + public HttpMethod HttpMethod; + + /// + /// Typically is sent by servers together with the status code. + /// Useful for fallback err descriptions, often based on the status code. + /// + public string ReasonPhrase { get; } + + /// Contains `message` with friendly info. + public bool HasErr => Error != null; + public EdgegapErrorResult Error { get; set; } + + #region Common Shortcuts + /// OK + public bool IsResultCode200 => StatusCode == HttpStatusCode.OK; + + /// NoContent + public bool IsResultCode204 => StatusCode == HttpStatusCode.NoContent; + + /// Forbidden + public bool IsResultCode403 => StatusCode == HttpStatusCode.Forbidden; + + /// Conflict + public bool IsResultCode409 => StatusCode == HttpStatusCode.Conflict; + + /// BadRequest + public bool IsResultCode400 => StatusCode == HttpStatusCode.BadRequest; + + /// Gone + public bool IsResultCode410 => StatusCode == HttpStatusCode.Gone; + #endregion // Common Shortcuts + + + /// + /// Constructor that initializes the class based on an HttpResponseMessage. + /// + public EdgegapHttpResult(HttpResponseMessage httpResponse) + { + this.ReasonPhrase = httpResponse.ReasonPhrase; + this.StatusCode = httpResponse.StatusCode; + + try + { + // TODO: This can be read async with `await`, but can't do this in a Constructor. + // Instead, make a factory builder Task => + this.Json = httpResponse.Content.ReadAsStringAsync().Result; + + this.Error = JsonConvert.DeserializeObject(Json); + if (Error != null && string.IsNullOrEmpty(Error.ErrorMessage)) + Error = null; + } + catch (Exception e) + { + Debug.LogError("Error (reading httpResponse.Content): Client expected json, " + + $"but server returned !json: {e} - "); + } + } + } + + /// + /// Wraps the inner json data with outer http info. + /// This class overload contains json-deserialiable data result. + /// + public class EdgegapHttpResult : EdgegapHttpResult + { + /// The actual result model from Json. Could be null! + public TResult Data { get; set; } + + + public EdgegapHttpResult(HttpResponseMessage httpResponse, bool isLogLevelDebug = false) + : base(httpResponse) + { + this.HttpMethod = httpResponse.RequestMessage.Method; + + // Assuming JSON content and using Newtonsoft.Json for deserialization + bool isDeserializable = httpResponse.Content != null && + httpResponse.Content.Headers.ContentType.MediaType == "application/json"; + + if (isDeserializable) + { + try + { + this.Data = JsonConvert.DeserializeObject(Json); + } + catch (Exception e) + { + Debug.LogError($"Error (deserializing EdgegapHttpResult.Data): {e} - json: {Json}"); + throw; + } + } + + if (isLogLevelDebug) + UnityEngine.Debug.Log($"{typeof(TResult).Name} result: {JObject.Parse(Json)}"); // Prettified + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs.meta new file mode 100755 index 000000000..bc6797509 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/EdgegapHttpResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 888bfc2c113487b44a3103648d2c2ae3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs new file mode 100755 index 000000000..b9df98f49 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `[GET | POST] v1/app`. + /// POST API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/application-post + /// GET API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/application-get + /// + public class GetCreateAppResult + { + [JsonProperty("name")] + public string AppName { get; set; } + + [JsonProperty("is_active")] + public bool IsActive { get; set; } + + /// Optional + [JsonProperty("is_telemetry_agent_active")] + public bool IsTelemetryAgentActive { get; set; } + + [JsonProperty("image")] + public string Image { get; set; } + + [JsonProperty("create_time")] + public string CreateTimeStr { get; set; } + + [JsonProperty("last_updated")] + public string LastUpdatedStr { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs.meta new file mode 100755 index 000000000..ca1a37eec --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetCreateAppResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a66c935238edd8846b1e9e9e19cfab70 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs new file mode 100755 index 000000000..57c4540ff --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `GET v1/status/{request_id}`. + /// API Doc | https://docs.edgegap.com/api/#tag/Deployments/operation/deployment-status-get + /// + public class GetDeploymentStatusResult + { + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("fqdn")] + public string Fqdn { get; set; } + + [JsonProperty("app_name")] + public string AppName { get; set; } + + [JsonProperty("app_version")] + public string AppVersion { get; set; } + + [JsonProperty("current_status")] + public string CurrentStatus { get; set; } + + [JsonProperty("running")] + public bool Running { get; set; } + + [JsonProperty("whitelisting_active")] + public bool WhitelistingActive { get; set; } + + [JsonProperty("start_time")] + public string StartTime { get; set; } + + [JsonProperty("removal_time")] + public string RemovalTime { get; set; } + + [JsonProperty("elapsed_time")] + public int? ElapsedTime { get; set; } + + [JsonProperty("last_status")] + public string LastStatus { get; set; } + + [JsonProperty("error")] + public bool Error { get; set; } + + [JsonProperty("error_detail")] + public string ErrorDetail { get; set; } + + [JsonProperty("public_ip")] + public string PublicIp { get; set; } + + [JsonProperty("sessions")] + public SessionData[] Sessions { get; set; } + + [JsonProperty("location")] + public LocationData Location { get; set; } + + [JsonProperty("tags")] + public string[] Tags { get; set; } + + [JsonProperty("sockets")] + public string Sockets { get; set; } + + [JsonProperty("sockets_usage")] + public string SocketsUsage { get; set; } + + [JsonProperty("command")] + public string Command { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + + /// + /// TODO: Server should swap `ports` to an array of DeploymentPortsData (instead of an object of dynamic unknown objects). + /// + /// { + /// "7777", {} + /// }, + /// { + /// "Some Port Name", {} + /// } + /// + /// + [JsonProperty("ports")] + public Dictionary PortsDict { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs.meta new file mode 100755 index 000000000..55eef85f0 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetDeploymentStatusResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c658b7f5c5d5d0648934b0ae1d71de9a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs new file mode 100755 index 000000000..8c3bd5457 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `GET v1/wizard/registry-credentials`. + /// + public class GetRegistryCredentialsResult + { + [JsonProperty("registry_url")] + public string RegistryUrl { get; set; } + + [JsonProperty("project")] + public string Project { get; set; } + + [JsonProperty("username")] + public string Username { get; set; } + + [JsonProperty("token")] + public string Token { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs.meta new file mode 100755 index 000000000..d8b5099dc --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetRegistryCredentialsResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8e6af130c329d2b43b2f4b0dc8639477 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs new file mode 100755 index 000000000..71d76b77f --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for `GET v1/ip`. + /// GET API Doc | https://docs.edgegap.com/api/#tag/IP-Lookup/operation/IP + /// + public class GetYourPublicIpResult + { + [JsonProperty("public_ip")] + public string PublicIp { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs.meta new file mode 100755 index 000000000..4f6f0ae33 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/GetYourPublicIpResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73c9651ef0fdfcb449ec0120016963a0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs new file mode 100755 index 000000000..5d4d6a0b4 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs @@ -0,0 +1,89 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + public class StopActiveDeploymentResult + { + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("deployment_summary")] + public DeploymentSummaryData DeploymentSummary { get; set; } + + + public class DeploymentSummaryData + { + [JsonProperty("request_id")] + public string RequestId { get; set; } + + [JsonProperty("fqdn")] + public string Fqdn { get; set; } + + [JsonProperty("app_name")] + public string AppName { get; set; } + + [JsonProperty("app_version")] + public string AppVersion { get; set; } + + [JsonProperty("current_status")] + public string CurrentStatus { get; set; } + + [JsonProperty("running")] + public bool Running { get; set; } + + [JsonProperty("whitelisting_active")] + public bool WhitelistingActive { get; set; } + + [JsonProperty("start_time")] + public string StartTime { get; set; } + + [JsonProperty("removal_time")] + public string RemovalTime { get; set; } + + [JsonProperty("elapsed_time")] + public int ElapsedTime { get; set; } + + [JsonProperty("last_status")] + public string LastStatus { get; set; } + + [JsonProperty("error")] + public bool Error { get; set; } + + [JsonProperty("error_detail")] + public string ErrorDetail { get; set; } + + [JsonProperty("ports")] + public PortsData Ports { get; set; } + + [JsonProperty("public_ip")] + public string PublicIp { get; set; } + + [JsonProperty("sessions")] + public SessionData[] Sessions { get; set; } + + [JsonProperty("location")] + public LocationData Location { get; set; } + + [JsonProperty("tags")] + public string[] Tags { get; set; } + + [JsonProperty("sockets")] + public string Sockets { get; set; } + + [JsonProperty("sockets_usage")] + public string SocketsUsage { get; set; } + + [JsonProperty("command")] + public string Command { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + + } + + public class PortsData + { + + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs.meta new file mode 100755 index 000000000..4adcc65ac --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/StopActiveDeploymentResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e2e4d9424ca8f7459803e631acf912f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs new file mode 100755 index 000000000..bc869d5f2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs @@ -0,0 +1,165 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models.Results +{ + /// + /// Result model for: + /// - `POST 1/app/{app_name}/version` + /// - `PATCH v1/app/{app_name}/version/{version_name}` + /// POST API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/application-post + /// PATCH API Doc | https://docs.edgegap.com/api/#tag/Applications/operation/app-versions-patch + /// + public class UpsertAppVersionResult + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("version")] + public VersionData Version { get; set; } + + public class VersionData + { + [JsonProperty("name")] + public string VersionName { get; set; } + + [JsonProperty("is_active")] + public bool IsActive { get; set; } + + [JsonProperty("docker_repository")] + public string DockerRepository { get; set; } + + [JsonProperty("docker_image")] + public string DockerImage { get; set; } + + [JsonProperty("docker_tag")] + public string DockerTag { get; set; } + + [JsonProperty("private_username")] + public string PrivateUsername { get; set; } + + [JsonProperty("private_token")] + public string PrivateToken { get; set; } + + [JsonProperty("req_cpu")] + public int? ReqCpu { get; set; } + + [JsonProperty("req_memory")] + public int? ReqMemory { get; set; } + + [JsonProperty("req_video")] + public int? ReqVideo { get; set; } + + [JsonProperty("max_duration")] + public int? MaxDuration { get; set; } + + [JsonProperty("use_telemetry")] + public bool UseTelemetry { get; set; } + + [JsonProperty("inject_context_env")] + public bool InjectContextEnv { get; set; } + + [JsonProperty("whitelisting_active")] + public bool WhitelistingActive { get; set; } + + [JsonProperty("force_cache")] + public bool ForceCache { get; set; } + + [JsonProperty("cache_min_hour")] + public int? CacheMinHour { get; set; } + + [JsonProperty("cache_max_hour")] + public int? CacheMaxHour { get; set; } + + [JsonProperty("time_to_deploy")] + public int? TimeToDeploy { get; set; } + + [JsonProperty("enable_all_locations")] + public bool EnableAllLocations { get; set; } + + [JsonProperty("session_config")] + public SessionConfigData SessionConfig { get; set; } + + [JsonProperty("ports")] + public PortsData[] Ports { get; set; } + + [JsonProperty("probe")] + public ProbeData Probe { get; set; } + + [JsonProperty("envs")] + public EnvsData[] Envs { get; set; } + + [JsonProperty("verify_image")] + public bool VerifyImage { get; set; } + + [JsonProperty("termination_grace_period_seconds")] + public int? TerminationGracePeriodSeconds { get; set; } + + [JsonProperty("endpoint_storage")] + public string EndpointStorage { get; set; } + + [JsonProperty("command")] + public string Command { get; set; } + + [JsonProperty("arguments")] + public string Arguments { get; set; } + } + + public class SessionConfigData + { + [JsonProperty("kind")] + public string Kind { get; set; } + + [JsonProperty("sockets")] + public int? Sockets { get; set; } + + [JsonProperty("autodeploy")] + public bool Autodeploy { get; set; } + + [JsonProperty("empty_ttl")] + public int? EmptyTtl { get; set; } + + [JsonProperty("session_max_duration")] + public int? SessionMaxDuration { get; set; } + } + + public class PortsData + { + [JsonProperty("port")] + public int? Port { get; set; } + + [JsonProperty("protocol")] + public string Protocol { get; set; } + + [JsonProperty("to_check")] + public bool ToCheck { get; set; } + + [JsonProperty("tls_upgrade")] + public bool TlsUpgrade { get; set; } + + [JsonProperty("name")] + public string PortName { get; set; } + } + + public class ProbeData + { + [JsonProperty("optimal_ping")] + public int? OptimalPing { get; set; } + + [JsonProperty("rejected_ping")] + public int? RejectedPing { get; set; } + } + + public class EnvsData + { + [JsonProperty("key")] + public string Key { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + + [JsonProperty("is_secret")] + public bool IsSecret { get; set; } + } + + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs.meta new file mode 100755 index 000000000..e08dbf440 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/Results/UpsertAppVersionResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7dde7d59c66d8c44b86af35e853f9f5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs new file mode 100755 index 000000000..458da78d2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace Edgegap.Editor.Api.Models +{ + /// + /// Shared model for `GetDeploymentStatusResult`, `StopActiveDeploymentResult`. + /// + public class SessionData + { + [JsonProperty("session_id")] + public string SessionId { get; set; } + + [JsonProperty("status")] + public string Status { get; set; } + + [JsonProperty("ready")] + public bool Ready { get; set; } + + [JsonProperty("linked")] + public bool Linked { get; set; } + + [JsonProperty("kind")] + public string Kind { get; set; } + + [JsonProperty("user_count")] + public string UserCount { get; set; } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs.meta new file mode 100755 index 000000000..86d9d288b --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/Api/Models/SessionData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5f9024e4ca5438e4788e461387313531 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs b/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs new file mode 100755 index 000000000..40203eaa6 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using UnityEngine.UIElements; + +namespace Edgegap.Editor +{ + /// Slightly shake a UI button to indicate attention. + public class ButtonShaker + { + const string SHAKE_START_CLASS = "shakeStart"; + const string SHAKE_STOP_CLASS = "shakeEnd"; + private Button targetButton; + + public ButtonShaker(Button buttonToShake) => + targetButton = buttonToShake; + + /// Shake the button x times for x msDelayBetweenShakes each. + /// 1 shake = 1 bigger -> followed by 1 smaller. + /// + /// # of shakes + public async Task ApplyShakeAsync(int msDelayBetweenShakes = 40, int iterations = 2) + { + for (int i = 0; i < iterations; i++) + await shakeOnce(msDelayBetweenShakes); + } + + private async Task shakeOnce(int msDelayBetweenShakes) + { + targetButton.AddToClassList(SHAKE_START_CLASS); + await Task.Delay(msDelayBetweenShakes); // duration of the first transition + targetButton.RemoveFromClassList(SHAKE_START_CLASS); + + targetButton.AddToClassList(SHAKE_STOP_CLASS); + await Task.Delay(msDelayBetweenShakes); // duration of the second transition + targetButton.RemoveFromClassList(SHAKE_STOP_CLASS); + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs.meta new file mode 100755 index 000000000..7272ca696 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/ButtonShaker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f963d64ffcf32ba4bba54fe1cd70b5a1 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs new file mode 100755 index 000000000..777e833be --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs @@ -0,0 +1,307 @@ +#if UNITY_EDITOR +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using UnityEditor; +using UnityEditor.Build.Reporting; + +using Debug = UnityEngine.Debug; + +namespace Edgegap +{ + internal static class EdgegapBuildUtils + { + public static bool IsArmCPU() => + RuntimeInformation.ProcessArchitecture == Architecture.Arm || + RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + + public static BuildReport BuildServer() + { + IEnumerable scenes = EditorBuildSettings.scenes + .Where(s => s.enabled) + .Select(s => s.path); + BuildPlayerOptions options = new BuildPlayerOptions + { + scenes = scenes.ToArray(), + target = BuildTarget.StandaloneLinux64, + // MIRROR CHANGE +#if UNITY_2021_3_OR_NEWER + subtarget = (int)StandaloneBuildSubtarget.Server, // dedicated server with UNITY_SERVER define +#else + options = BuildOptions.EnableHeadlessMode, // obsolete and missing UNITY_SERVER define +#endif + // END MIRROR CHANGE + locationPathName = "Builds/EdgegapServer/ServerBuild" + }; + + return BuildPipeline.BuildPlayer(options); + } + + public static async Task DockerSetupAndInstallationCheck() + { + if (!File.Exists("Dockerfile")) + { + File.WriteAllText("Dockerfile", dockerFileText); + } + + string output = null; + string error = null; + await RunCommand_DockerVersion(msg => output = msg, msg => error = msg); // MIRROR CHANGE + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError(error); + return false; + } + Debug.Log($"[Edgegap] Docker version detected: {output}"); // MIRROR CHANGE + return true; + } + + // MIRROR CHANGE + static async Task RunCommand_DockerVersion(Action outputReciever = null, Action errorReciever = null) + { +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", "/c docker --version", outputReciever, errorReciever); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", "-c \"docker --version\"", outputReciever, errorReciever); +#else + Debug.LogError("The platform is not supported yet."); +#endif + } + + // MIRROR CHANGE + public static async Task RunCommand_DockerBuild(string registry, string imageRepo, string tag, Action onStatusUpdate) + { + string realErrorMessage = null; + + // ARM -> x86 support: + // build commands use 'buildx' on ARM cpus for cross compilation. + // otherwise docker builds would not launch when deployed because + // Edgegap's infrastructure is on x86. instead the deployment logs + // would show an error in a linux .go file with 'not found'. + string buildCommand = IsArmCPU() ? "buildx build --platform linux/amd64" : "build"; + +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"{buildCommand} -t {registry}/{imageRepo}:{tag} .", onStatusUpdate, +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate, +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker {buildCommand} -t {registry}/{imageRepo}:{tag} .\"", onStatusUpdate, +#endif + (msg) => + { + if (msg.Contains("ERROR")) + { + realErrorMessage = msg; + } + onStatusUpdate(msg); + }); + + if(realErrorMessage != null) + { + throw new Exception(realErrorMessage); + } + } + + public static async Task<(bool, string)> RunCommand_DockerPush(string registry, string imageRepo, string tag, Action onStatusUpdate) + { + string error = string.Empty; +#if UNITY_EDITOR_WIN + await RunCommand("docker.exe", $"push {registry}/{imageRepo}:{tag}", onStatusUpdate, (msg) => error += msg + "\n"); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker push {registry}/{imageRepo}:{tag}\"", onStatusUpdate, (msg) => error += msg + "\n"); +#endif + if (!string.IsNullOrEmpty(error)) + { + Debug.LogError(error); + return (false, error); + } + return (true, null); + } + // END MIRROR CHANGE + + static async Task RunCommand(string command, string arguments, Action outputReciever = null, Action errorReciever = null) + { + ProcessStartInfo startInfo = new ProcessStartInfo() + { + FileName = command, + Arguments = arguments, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + + // MIRROR CHANGE +#if !UNITY_EDITOR_WIN + // on mac, commands like 'docker' aren't found because it's not in the application's PATH + // even if it runs on mac's terminal. + // to solve this we need to do two steps: + // 1. add /usr/bin/local to PATH if it's not there already. often this is missing in the application. + // this is where docker is usually instaled. + // 2. add PATH to ProcessStartInfo + string existingPath = Environment.GetEnvironmentVariable("PATH"); + string customPath = $"{existingPath}:/usr/local/bin"; + startInfo.EnvironmentVariables["PATH"] = customPath; + // Debug.Log("PATH: " + customPath); +#endif + // END MIRROR CHANGE + + Process proc = new Process() { StartInfo = startInfo, }; + proc.EnableRaisingEvents = true; + + ConcurrentQueue errors = new ConcurrentQueue(); + ConcurrentQueue outputs = new ConcurrentQueue(); + + void pipeQueue(ConcurrentQueue q, Action opt) + { + while (!q.IsEmpty) + { + if (q.TryDequeue(out string msg) && !string.IsNullOrWhiteSpace(msg)) + { + opt?.Invoke(msg); + } + } + } + + proc.OutputDataReceived += (s, e) => outputs.Enqueue(e.Data); + proc.ErrorDataReceived += (s, e) => errors.Enqueue(e.Data); + + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + + while (!proc.HasExited) + { + await Task.Delay(100); + pipeQueue(errors, errorReciever); + pipeQueue(outputs, outputReciever); + } + + pipeQueue(errors, errorReciever); + pipeQueue(outputs, outputReciever); + + + } + + static void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + throw new NotImplementedException(); + } + + static Regex lastDigitsRegex = new Regex("([0-9])+$"); + + public static string IncrementTag(string tag) + { + Match lastDigits = lastDigitsRegex.Match(tag); + if (!lastDigits.Success) + { + return tag + " _1"; + } + + int number = int.Parse(lastDigits.Groups[0].Value); + + number++; + + return lastDigitsRegex.Replace(tag, number.ToString()); + } + + public static void UpdateEdgegapAppTag(string tag) + { + // throw new NotImplementedException(); + } + + // -batchmode -nographics remains for Unity 2019/2020 support pre-dedicated server builds + static string dockerFileText = @"FROM ubuntu:bionic + +ARG DEBIAN_FRONTEND=noninteractive + +COPY Builds/EdgegapServer /root/build/ + +WORKDIR /root/ + +RUN chmod +x /root/build/ServerBuild + +RUN apt-get update && \ + apt-get install -y ca-certificates && \ + apt-get clean && \ + update-ca-certificates + +ENTRYPOINT [ ""/root/build/ServerBuild"", ""-batchmode"", ""-nographics""] +"; + + /// Run a Docker cmd with streaming log response. TODO: Plugin to other Docker cmds + /// Throws if logs contain "ERROR" + /// + /// ex: "registry.edgegap.com" + /// ex: "robot$mycompany-asdf+client-push" + /// Different from ApiToken; sometimes called "Container Registry Password" + /// Log stream + // MIRROR CHANGE: CROSS PLATFORM SUPPORT + static async Task RunCommand_DockerLogin( + string registryUrl, + string repoUsername, + string repoPasswordToken, + Action outputReciever = null, Action errorReciever = null) + { + // TODO: Use --password-stdin for security (!) This is no easy task for child Process | https://stackoverflow.com/q/51489359/6541639 + // (!) Don't use single quotes for cross-platform support (works unexpectedly in `cmd`). + + try + { +#if UNITY_EDITOR_WIN + await RunCommand("cmd.exe", $"/c docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"", outputReciever, errorReciever); +#elif UNITY_EDITOR_OSX + await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever); +#elif UNITY_EDITOR_LINUX + await RunCommand("/bin/bash", $"-c \"docker login -u \"{repoUsername}\" --password \"{repoPasswordToken}\" \"{registryUrl}\"\"", outputReciever, errorReciever); +#else + Debug.LogError("The platform is not supported yet."); +#endif + } + catch (Exception e) + { + Debug.LogError($"Error: {e}"); + return false; + } + + return true; + } + + /// + /// v2: Login to Docker Registry via RunCommand(), returning streamed log messages: + /// "docker login {registryUrl} {repository} {repoUsername} {repoPasswordToken}" + /// + /// ex: "registry.edgegap.com" + /// ex: "robot$mycompany-asdf+client-push" + /// Different from ApiToken; sometimes called "Container Registry Password" + /// Log stream + /// isSuccess + public static async Task LoginContainerRegistry( + string registryUrl, + string repoUsername, + string repoPasswordToken, + Action onStatusUpdate) + { + string error = null; + await RunCommand_DockerLogin(registryUrl, repoUsername, repoPasswordToken, onStatusUpdate, msg => error = msg); // MIRROR CHANGE + if (error.Contains("ERROR")) + { + Debug.LogError(error); + return false; + } + return true; + } + + } +} +#endif diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta new file mode 100755 index 000000000..03529c425 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapBuildUtils.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 97dadab2a073d8b47bf9a270401f0a8f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs new file mode 100755 index 000000000..4c329b243 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs @@ -0,0 +1,27 @@ +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine.UIElements; +using Edgegap; + +[CustomEditor(typeof(EdgegapToolScript))] +public class EdgegapPluginScriptEditor : Editor +{ + VisualElement _serverDataContainer; + + private void OnEnable() + { + _serverDataContainer = EdgegapServerDataManager.GetServerDataVisualTree(); + EdgegapServerDataManager.RegisterServerDataContainer(_serverDataContainer); + } + + private void OnDisable() + { + EdgegapServerDataManager.DeregisterServerDataContainer(_serverDataContainer); + } + + public override VisualElement CreateInspectorGUI() + { + return _serverDataContainer; + } +} +#endif \ No newline at end of file diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta new file mode 100755 index 000000000..60f708f6b --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapScriptEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c4c676ae6dcca0e458c6a8f06571f8fc +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss new file mode 100755 index 000000000..15cb5e392 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss @@ -0,0 +1,81 @@ +.row__port-table { + padding: 2px 4px; + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-around; + -unity-text-align: middle-left; +} + + .row__port-table > * { + width: 0px; + flex-grow: 1; + align-self: center; + margin: 0px; + padding: 0px; + } + +.focusable:hover { + background-color: rgba(0,0,0,0.2); + border-radius: 3px; +} + +.row__dns { + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-between; + -unity-text-align: middle-left; +} + +.row__status { + display: flex; + flex-direction: row; + align-items: center; + width: auto; + justify-content: space-between; + -unity-text-align: middle-left; +} + +.label__header { + -unity-font-style: bold +} + +.label__status { + -unity-font-style: bold; + border-radius: 2px; + width: 100px; + color: #fff; + -unity-text-align: middle-center; +} + +.label__info-text { + padding: 8px; + margin: 4px; + border-radius: 3px; + -unity-text-align: middle-center; + white-space: normal; + background-color: rgba(42, 42, 42, 0.5); +} + +.container { + margin: 8px 0px; +} + +.bg--secondary { + background-color: #8a8a8a; +} + +.bg--success { + background-color: #90be6d; +} + +.bg--danger { + background-color: #f94144; +} + +.bg--warning { + background-color: #f9c74f; +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta new file mode 100755 index 000000000..b9cce488c --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerData.uss.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: da5e3f58bd8cde14789f7c61df3f59f4 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0} + disableValidation: 0 diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs new file mode 100755 index 000000000..b4dc287dd --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs @@ -0,0 +1,242 @@ +using IO.Swagger.Model; +using System.Collections.Generic; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; + +namespace Edgegap +{ + static class EdgegapServerDataManagerUtils + { + public static Label GetHeader(string text) + { + Label header = new Label(text); + header.AddToClassList("label__header"); + + return header; + } + + public static VisualElement GetHeaderRow() + { + VisualElement row = new VisualElement(); + row.AddToClassList("row__port-table"); + row.AddToClassList("label__header"); + + row.Add(new Label("Name")); + row.Add(new Label("External")); + row.Add(new Label("Internal")); + row.Add(new Label("ProtocolStr")); + row.Add(new Label("Link")); + + return row; + } + + public static VisualElement GetRowFromPortResponse(PortMapping port) + { + VisualElement row = new VisualElement(); + row.AddToClassList("row__port-table"); + row.AddToClassList("focusable"); + + + row.Add(new Label(port.Name)); + row.Add(new Label(port.External.ToString())); + row.Add(new Label(port.Internal.ToString())); + row.Add(new Label(port.Protocol)); + row.Add(GetCopyButton("Copy", port.Link)); + + return row; + } + + public static Button GetCopyButton(string btnText, string copiedText) + { + Button copyBtn = new Button(); + copyBtn.text = btnText; + copyBtn.clickable.clicked += () => GUIUtility.systemCopyBuffer = copiedText; + + return copyBtn; + } + + public static Button GetLinkButton(string btnText, string targetUrl) + { + Button copyBtn = new Button(); + copyBtn.text = btnText; + copyBtn.clickable.clicked += () => UnityEngine.Application.OpenURL(targetUrl); + + return copyBtn; + } + public static Label GetInfoText(string innerText) + { + Label infoText = new Label(innerText); + infoText.AddToClassList("label__info-text"); + + return infoText; + } + } + + /// + /// Utility class to centrally manage the Edgegap server data, and create / update the elements displaying the server info. + /// + public static class EdgegapServerDataManager + { + internal static Status _serverData; + private static ApiEnvironment _apiEnvironment; + + // UI elements + private static readonly StyleSheet _serverDataStylesheet; + private static readonly List _serverDataContainers = new List(); + + public static Status GetServerStatus() => _serverData; + + static EdgegapServerDataManager() + { +#if UNITY_EDITOR + _serverDataStylesheet = AssetDatabase.LoadAssetAtPath("Assets/Edgegap/Editor/EdgegapServerData.uss"); +#endif + } + public static void RegisterServerDataContainer(VisualElement serverDataContainer) + { + _serverDataContainers.Add(serverDataContainer); + } + public static void DeregisterServerDataContainer(VisualElement serverDataContainer) + { + _serverDataContainers.Remove(serverDataContainer); + } + public static void SetServerData(Status serverData, ApiEnvironment apiEnvironment) + { + _serverData = serverData; + _apiEnvironment = apiEnvironment; + RefreshServerDataContainers(); + } + + private static VisualElement GetStatusSection() + { + ServerStatus serverStatus = _serverData.GetServerStatus(); + string dashboardUrl = _apiEnvironment.GetDashboardUrl(); + string requestId = _serverData.RequestId; + string deploymentDashboardUrl = ""; + + if (!string.IsNullOrEmpty(requestId) && !string.IsNullOrEmpty(dashboardUrl)) + { + deploymentDashboardUrl = $"{dashboardUrl}/arbitrium/deployment/read/{requestId}/"; + } + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server Status")); + + VisualElement row = new VisualElement(); + row.AddToClassList("row__status"); + + // Status pill + Label statusLabel = new Label(serverStatus.GetLabelText()); + statusLabel.AddToClassList(serverStatus.GetStatusBgClass()); + statusLabel.AddToClassList("label__status"); + row.Add(statusLabel); + + // Link to dashboard + if (!string.IsNullOrEmpty(deploymentDashboardUrl)) + { + row.Add(EdgegapServerDataManagerUtils.GetLinkButton("See in the dashboard", deploymentDashboardUrl)); + } + else + { + row.Add(new Label("Could not resolve link to this deployment")); + } + + container.Add(row); + + return container; + } + + private static VisualElement GetDnsSection() + { + string serverDns = _serverData.Fqdn; + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server DNS")); + + VisualElement row = new VisualElement(); + row.AddToClassList("row__dns"); + row.AddToClassList("focusable"); + + row.Add(new Label(serverDns)); + row.Add(EdgegapServerDataManagerUtils.GetCopyButton("Copy", serverDns)); + + container.Add(row); + + return container; + } + + private static VisualElement GetPortsSection() + { + List serverPorts = _serverData.Ports.Values.ToList(); + + VisualElement container = new VisualElement(); + container.AddToClassList("container"); + + container.Add(EdgegapServerDataManagerUtils.GetHeader("Server PortsDict")); + container.Add(EdgegapServerDataManagerUtils.GetHeaderRow()); + + VisualElement portList = new VisualElement(); + + if (serverPorts.Count > 0) + { + foreach (PortMapping port in serverPorts) + { + portList.Add(EdgegapServerDataManagerUtils.GetRowFromPortResponse(port)); + } + } + else + { + portList.Add(new Label("No port configured for this app version.")); + } + + container.Add(portList); + + return container; + } + + public static VisualElement GetServerDataVisualTree() + { + VisualElement serverDataTree = new VisualElement(); + serverDataTree.styleSheets.Add(_serverDataStylesheet); + + bool hasServerData = _serverData != null; + bool isReady = hasServerData && _serverData.GetServerStatus().IsOneOf(ServerStatus.Ready, ServerStatus.Error); + + if (hasServerData) + { + serverDataTree.Add(GetStatusSection()); + + if (isReady) + { + serverDataTree.Add(GetDnsSection()); + serverDataTree.Add(GetPortsSection()); + } + else + { + serverDataTree.Add(EdgegapServerDataManagerUtils.GetInfoText("Additional information will be displayed when the server is ready.")); + } + } + else + { + serverDataTree.Add(EdgegapServerDataManagerUtils.GetInfoText("Server data will be displayed here when a server is running.")); + } + + return serverDataTree; + } + + private static void RefreshServerDataContainers() + { + foreach (VisualElement serverDataContainer in _serverDataContainers) + { + serverDataContainer.Clear(); + serverDataContainer.Add(GetServerDataVisualTree()); // Cannot reuse a same instance of VisualElement + } + } + } +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta new file mode 100755 index 000000000..65da2ab42 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapServerDataManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 39f5f27c13279a34eb116630a00e41c2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs new file mode 100755 index 000000000..870190d23 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs @@ -0,0 +1,12 @@ +using UnityEngine; +using Edgegap; +using IO.Swagger.Model; + +/// +/// This script acts as an interface to display and use the necessary variables from the Edgegap tool. +/// The server info can be accessed from the tool window, as well as through the public script property. +/// +public class EdgegapToolScript : MonoBehaviour +{ + public Status ServerStatus => EdgegapServerDataManager.GetServerStatus(); +} diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta new file mode 100755 index 000000000..d584893d2 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapToolScript.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5963202433da25448a22def99f5a598b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs new file mode 100755 index 000000000..c85723347 --- /dev/null +++ b/Assets/Mirror/Hosting/Edgegap/Editor/EdgegapWindow.cs @@ -0,0 +1,988 @@ +// MIRROR CHANGE: disable this completely. otherwise InitUIElements can still throw NRE. +/* +#if UNITY_EDITOR +using UnityEditor; +using UnityEngine; +using UnityEngine.UIElements; +using UnityEditor.UIElements; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Collections.Generic; +using Newtonsoft.Json; +using System.Net; +using System.Text; +using System; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using IO.Swagger.Model; +using UnityEditor.Build.Reporting; +using Application = UnityEngine.Application; + +namespace Edgegap +{ + public class EdgegapWindow : EditorWindow + { + // MIRROR CHANGE: create HTTPClient in-place to avoid InvalidOperationExceptions when reusing + // static readonly HttpClient _httpClient = new HttpClient(); + // END MIRROR CHANGE + + const string EditorDataSerializationName = "EdgegapSerializationData"; + const int ServerStatusCronjobIntervalMs = 10000; // Interval at which the server status is updated + + // MIRROR CHANGE + // get the path of this .cs file so we don't need to hardcode paths to + // the .uxml and .uss files: + // https://forum.unity.com/threads/too-many-hard-coded-paths-in-the-templates-and-documentation.728138/ + // this way users can move this folder without breaking UIToolkit paths. + internal string StylesheetPath => + Path.GetDirectoryName(AssetDatabase.GetAssetPath(MonoScript.FromScriptableObject(this))); + // END MIRROR CHANGE + + readonly System.Timers.Timer _updateServerStatusCronjob = new System.Timers.Timer(ServerStatusCronjobIntervalMs); + + [SerializeField] string _userExternalIp; + [SerializeField] string _apiKey; + [SerializeField] ApiEnvironment _apiEnvironment; + [SerializeField] string _appName; + [SerializeField] string _appVersionName; + [SerializeField] string _deploymentRequestId; + + [SerializeField] string _containerRegistry; + [SerializeField] string _containerImageRepo; + [SerializeField] string _containerImageTag; + [SerializeField] bool _autoIncrementTag = true; + + + VisualTreeAsset _visualTree; + bool _shouldUpdateServerStatus = false; + + // Interactable elements + EnumField _apiEnvironmentSelect; + TextField _apiKeyInput; + TextField _appNameInput; + TextField _appVersionNameInput; + TextField _containerRegistryInput; + TextField _containerImageRepoInput; + TextField _containerImageTagInput; + Toggle _autoIncrementTagInput; + Button _connectionButton; + Button _serverActionButton; + Button _documentationBtn; + Button _buildAndPushServerBtn; + + // Readonly elements + Label _connectionStatusLabel; + VisualElement _serverDataContainer; + + // server data manager + StyleSheet _serverDataStylesheet; + List _serverDataContainers = new List(); + + [Obsolete("See EdgegapWindowV2.ShowEdgegapToolWindow()")] + // [MenuItem("Edgegap/Server Management")] + public static void ShowEdgegapToolWindow() + { + EdgegapWindow window = GetWindow(); + window.titleContent = new GUIContent("Edgegap Hosting"); // MIRROR CHANGE + } + + protected void OnEnable() + { + // Set root VisualElement and style + // BEGIN MIRROR CHANGE + _visualTree = AssetDatabase.LoadAssetAtPath($"{StylesheetPath}/EdgegapWindow.uxml"); + StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath($"{StylesheetPath}/EdgegapWindow.uss"); + _serverDataStylesheet = AssetDatabase.LoadAssetAtPath($"{StylesheetPath}/EdgegapServerData.uss"); + // END MIRROR CHANGE + rootVisualElement.styleSheets.Add(styleSheet); + + LoadToolData(); + + if (string.IsNullOrWhiteSpace(_userExternalIp)) + { + _userExternalIp = GetExternalIpAddress(); + } + } + + protected void Update() + { + if (_shouldUpdateServerStatus) + { + _shouldUpdateServerStatus = false; + UpdateServerStatus(); + } + } + + public void CreateGUI() + { + rootVisualElement.Clear(); + _visualTree.CloneTree(rootVisualElement); + + InitUIElements(); + SyncFormWithObject(); + + bool hasActiveDeployment = !string.IsNullOrEmpty(_deploymentRequestId); + + if (hasActiveDeployment) + { + RestoreActiveDeployment(); + } + else + { + DisconnectCallback(); + } + } + + protected void OnDestroy() + { + bool deploymentActive = !string.IsNullOrEmpty(_deploymentRequestId); + + if (deploymentActive) + { + EditorUtility.DisplayDialog( + "Warning", + $"You have an active deployment ({_deploymentRequestId}) that won't be stopped automatically.", + "Ok" + ); + } + } + + protected void OnDisable() + { + SyncObjectWithForm(); + SaveToolData(); + DeregisterServerDataContainer(_serverDataContainer); + } + + /// + /// Binds the form inputs to the associated variables and initializes the inputs as required. + /// Requires the VisualElements to be loaded before this call. Otherwise, the elements cannot be found. + /// + void InitUIElements() + { + _apiEnvironmentSelect = rootVisualElement.Q("environmentSelect"); + _apiKeyInput = rootVisualElement.Q("apiKey"); + _appNameInput = rootVisualElement.Q("appName"); + _appVersionNameInput = rootVisualElement.Q("appVersionName"); + + _containerRegistryInput = rootVisualElement.Q("containerRegistry"); + _containerImageRepoInput = rootVisualElement.Q("containerImageRepo"); + _containerImageTagInput = rootVisualElement.Q("tag"); + _autoIncrementTagInput = rootVisualElement.Q("autoIncrementTag"); + + _connectionButton = rootVisualElement.Q