using System; using System.Collections; using System.Collections.Generic; using System.Xml; using System.IO; using System.Text; using System.Net; using UnityEngine; public class RPCmin_XML { public class RPCFault { // a simple struct with a code and a string. All errors will be given to you as one of these public int FaultCode = 400; // if there's a better "generic fault", let me know public string FaultString; public RPCFault(string faultString = "") { FaultString = faultString; } }; public string URL; public RPCmin_XML(string _URL = null) { // hang on to the URL. If we make several calls, it'll likely be to the same URL URL = _URL; } // the following calls are Unity-specific. If you're using generic C#, use the blocking calls or roll your own threaded version public static MonoBehaviour MBHook { protected get; set; } = null; // coroutines require a monobehavior, so we'll hook into an existing one. Set this to a monobehavior (preferably a singleton) for the coroutine public bool Call(MonoBehaviour mBHook, string method, object[] parms = null, Action fnSuccess = null, Action fnFault = null) { MBHook = mBHook; return Call(method, parms, fnSuccess, fnFault); } public bool Call(string method, object[] parms = null, Action fnSuccess = null, Action fnFault = null) { // non-blocking call that returns immediately and returns success or fault as actions. Returns true if call was sent bool RetVal = false; if ((URL != null) && (MBHook != null)) { // build the request var Form = new WWWForm(); var Headers = Form.headers; Headers["Content-Type"] = "text/xml;charset=UTF-8"; string Request = EncodeXMLRPCRequest(method, parms); // Debug.Log(Request); var wwwRequest = new WWW(URL, Encoding.UTF8.GetBytes(Request), Headers); MBHook.StartCoroutine(WaitForRequest(wwwRequest, fnSuccess, fnFault)); // Start a coroutine which will wait until our servers comes back RetVal = true; } return RetVal; } static public bool RPCXMLCall(string _URL, MonoBehaviour mBHook, string method, object[] parms = null, Action fnSuccess = null, Action fnFault = null) { // if you're just making one call, do this. It'll make the call, return from whence it came, and then disappear return new RPCmin_XML(_URL).Call(mBHook, method, parms, fnSuccess, fnFault); } static public bool RPCXMLCall(string _URL, string method, object[] parms = null, Action fnSuccess = null, Action fnFault = null) { // if you're just making one call, do this. It'll make the call, return from whence it came, and then disappear return new RPCmin_XML(_URL).Call(method, parms, fnSuccess, fnFault); } protected IEnumerator WaitForRequest(WWW wwwResult, Action fnSuccess, Action fnFault) { // coroutine that yields until a result appears. Might want to add a timeout here eventually. yield return wwwResult; if (String.IsNullOrEmpty(wwwResult.error)) { // parse the XML and see if there's a fault string HTMLResult = Encoding.UTF8.GetString(wwwResult.bytes); object Ob = DecodeXMLRPCResult(HTMLResult); if (Ob == null) fnFault.Invoke(new RPCFault("Could not parse " + HTMLResult)); // not something parse-able else if (Ob.GetType().ToString().Contains("RPCFault")) fnFault.Invoke((RPCFault)Ob); // RPC didn't run, most likely passed it bogus info else fnSuccess.Invoke(Ob); // returned a non-fault object, which is good } else { // we got some error text from the http server RPCFault Fault = new RPCFault(wwwResult.error); if ((wwwResult.responseHeaders != null) && wwwResult.responseHeaders.ContainsKey("STATUS")) { // getting the numeric fault code isn't easy string[] StatusStrs = wwwResult.responseHeaders["STATUS"].Split(' '); int StatusInt; if ((StatusStrs.Length >= 3) && int.TryParse(StatusStrs[1], out StatusInt)) Fault.FaultCode = StatusInt; // the second value was a number, so assumme it's the fault code } fnFault.Invoke(Fault); } } // End of the Unity-specific stuff public static RPCFault Fault(object Ob) { // pass the result of the Call() below here, it will return a RPCFault if it's a fault, or null if not return Ob.GetType().Equals(typeof(RPCFault)) ? (RPCFault)Ob : null; } public object Call(string method, object[] parms = null) { // returns an object that you can cast to the results you expect, null if there is no URL, or a RPCmin_XML.RPCFault if there is a fault object RetVal = null; if (URL != null) { // build the request HttpWebRequest HTTPRequest = (HttpWebRequest)WebRequest.Create(URL); HTTPRequest.Method = "POST"; HTTPRequest.ContentType = "text/xml"; // Add the request stream. Stream RequestStream = HTTPRequest.GetRequestStream(); string Request = EncodeXMLRPCRequest(method, parms); // Debug.Log(Request); byte[] Bytes = Encoding.UTF8.GetBytes(Request); RequestStream.Write(Bytes, 0, Bytes.Length); RequestStream.Close(); HttpWebResponse HTTPResponse = (HttpWebResponse)HTTPRequest.GetResponse(); // Now send it off // And we're (hopefully) back. Get the response as a stream with the required encoding format. StreamReader ReadStream = new StreamReader(HTTPResponse.GetResponseStream(), Encoding.UTF8); RetVal = DecodeXMLRPCResult(ReadStream.ReadToEnd()); // now parse the received XML back into an object ReadStream.Close(); HTTPResponse.Close(); } return RetVal; } // End of the public interface protected const string XMLRPC_STRING = "string"; // string protected const string XMLRPC_INT = "int"; // int protected const string XMLRPC_I4 = "i4"; // int protected const string XMLRPC_DOUBLE = "double"; // double, float protected const string XMLRPC_BOOLEAN = "boolean"; // bool protected const string XMLRPC_ARRAY = "array"; // Array protected const string XMLRPC_STRUCT = "struct"; // Hashtable protected const string XMLRPC_DATE = "dateTime.iso8601"; // DateTimeOffset (preferred) or DateTime protected const string XMLRPC_BASE64 = "base64"; // byte[] protected string XMLRPCTypeOf(object obValue) { // converts a C# type to its equivalent in XMLRPC Type ObType = obValue.GetType(); if (ObType.Equals(typeof(int)) || ObType.Equals(typeof(uint)) || ObType.Equals(typeof(short)) || ObType.Equals(typeof(ushort))) return XMLRPC_INT; // you can send all kinds of ints, but only an standard 32-bit signed int comes back else if (ObType.Equals(typeof(double)) || ObType.Equals(typeof(float))) return XMLRPC_DOUBLE; // you can send floats or doubles, but only a double comes back else if (ObType.Equals(typeof(bool))) return XMLRPC_BOOLEAN; else if (ObType.Equals(typeof(Hashtable))) return XMLRPC_STRUCT; else if (ObType.Equals(typeof(DateTime)) || ObType.Equals(typeof(DateTimeOffset))) return XMLRPC_DATE; // you can pass a DateTime or a DateTimeOffset. A DateTimeOffset comes back else if (ObType.Equals(typeof(byte[]))) return XMLRPC_BASE64; // It converts to Base64 and back, so you'll get a byte[] back else if (ObType.IsArray) return XMLRPC_ARRAY; else // everything else is a string, per the spec return XMLRPC_STRING; } protected string EncodeXMLRPCValue(object obValue) { // figures out what an object is, converts it to a string, and returns it bracketed with String RetVal = null; string TypeStr = XMLRPCTypeOf(obValue); switch (TypeStr) { case XMLRPC_INT: RetVal = ((int)obValue).ToString(); break; case XMLRPC_DOUBLE: RetVal = ((double)obValue).ToString(); break; case XMLRPC_BOOLEAN: RetVal = (((bool)obValue) ? "1" : "0"); break; case XMLRPC_ARRAY: { StringBuilder S = new StringBuilder(); foreach (object Ob in (Array)obValue) { string Node = EncodeXMLRPCValue(Ob); if (Node != null) S.AppendLine(Node); } if (S.Length > 0) // we encoded something RetVal = "" + S.ToString() + ""; } break; case XMLRPC_STRUCT: { StringBuilder S = new StringBuilder(); foreach (string Key in ((Hashtable)obValue).Keys) { string Node = EncodeXMLRPCValue(((Hashtable)obValue)[Key]); if (Node != null) { // this was a good value, so save the key and value S.AppendLine(""); S.AppendLine("" + Key + ""); S.AppendLine(Node); S.AppendLine(""); } } if (S.Length > 0) // we encoded something RetVal = S.ToString(); } break; case XMLRPC_DATE: { DateTimeOffset D; if (obValue.GetType().ToString() == "System.DateTime") D = new DateTimeOffset((DateTime)obValue); else D = (DateTimeOffset)obValue; RetVal = D.ToString("o"); } break; case XMLRPC_BASE64: RetVal = Convert.ToBase64String((byte[])obValue); break; default: // assume string TypeStr = XMLRPC_STRING; RetVal = ((string)obValue).ToString(); break; } return String.IsNullOrEmpty(RetVal) ? null : "<" + TypeStr + ">" + RetVal.ToString() + ""; } protected string EncodeXMLRPCRequest(string method, object[] parms) { // returns an XMLRPC-formatted object to send to a server StringBuilder XMLRPC = new StringBuilder(); XMLRPC.AppendLine(""); XMLRPC.AppendLine(""); XMLRPC.AppendLine("" + method + ""); if (parms != null) { StringBuilder S = new StringBuilder(); foreach (object Item in parms) { string ObEncoded = EncodeXMLRPCValue(Item); if (ObEncoded != null) S.AppendLine("" + ObEncoded + ""); } if (S.Length > 0) XMLRPC.AppendLine("" + S.ToString() + ""); } XMLRPC.AppendLine(""); return XMLRPC.ToString(); } protected static readonly string[] Format8601 = new string[] { "s", "u", "o" }; // permissive date parsing, as we don't know what the server thinks is standard protected object DecodeXMLRPCValue(XmlNode Node) { // converts a single XML string to an object object RetVal = null; XmlNode ValueNode = Node.FirstChild; switch (ValueNode.LocalName) { case XMLRPC_INT: case XMLRPC_I4: case XMLRPC_BOOLEAN: { int Val = 0; int.TryParse(ValueNode.InnerText, out Val); if (ValueNode.LocalName == XMLRPC_BOOLEAN) RetVal = (Val == 0) ? false : true; else RetVal = Val; } break; case XMLRPC_STRING: RetVal = ValueNode.InnerText; break; case XMLRPC_DOUBLE: { double Val = 0.0; double.TryParse(ValueNode.InnerText, out Val); RetVal = Val; } break; case XMLRPC_ARRAY: { List ObArr = new List(); foreach (XmlNode ArrayNode in ValueNode.FirstChild) // there's a single member, so get around it { object ObArrayElement = DecodeXMLRPCValue(ArrayNode); if (ObArrayElement != null) ObArr.Add(ObArrayElement); } if (ObArr.Count > 0) RetVal = ObArr.ToArray(); // there's an array with items, so return it } break; case XMLRPC_STRUCT: { Hashtable H = new Hashtable(); foreach (XmlNode StructNode in ValueNode) // loop through the members { string Key = null; object Value = null; XmlNode StructNameNode = StructNode.SelectSingleNode("name"); if (StructNameNode != null) Key = StructNameNode.InnerText; XmlNode StructValueNode = StructNode.SelectSingleNode("value"); if (StructValueNode != null) Value = DecodeXMLRPCValue(StructValueNode); if ((Key != null) && (Value != null)) H[Key] = Value; } if (H.Count > 0) // there was something in the struct RetVal = H; } break; case XMLRPC_DATE: DateTimeOffset Result; if (DateTimeOffset.TryParseExact(ValueNode.InnerText, Format8601, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AllowWhiteSpaces | System.Globalization.DateTimeStyles.AdjustToUniversal, out Result)) RetVal = Result; break; case XMLRPC_BASE64: RetVal = Convert.FromBase64String(ValueNode.InnerText); break; } return RetVal; } protected object DecodeXMLRPCResult(string XMLResultString) { // Debug.Log(XMLResultString); object RetVal = null; XmlDocument XML = new XmlDocument(); XML.LoadXml(XMLResultString); XmlNode XMLMethodResponse = XML.SelectSingleNode("/methodResponse"); if (XMLMethodResponse != null) { // we have a response XmlNode XMLFault = XMLMethodResponse.SelectSingleNode("fault"); if (XMLFault != null) { // we have a tag, so create a fault RetVal = new RPCFault(); foreach (XmlNode XMLFaultMember in XMLFault.SelectNodes("value/struct/member")) { // get the value object XMLFaultMemberValue = DecodeXMLRPCValue(XMLFaultMember.SelectSingleNode("value")); switch (XMLRPCTypeOf(XMLFaultMemberValue)) { // only concerned about string and int values, as those are the code and the error case XMLRPC_STRING: ((RPCFault)RetVal).FaultString = (string)XMLFaultMemberValue; break; case XMLRPC_INT: ((RPCFault)RetVal).FaultCode = (int)XMLFaultMemberValue; break; } } } else // no fault, so parse the value RetVal = DecodeXMLRPCValue(XMLMethodResponse.SelectNodes("params/param/value").Item(0)); // return only has one param and value, so assume the first } return RetVal; } }