johnhattan icon

RPCmin_XML (minimal XML-RPC client) in C#

johnhattan | PRO | 10/27/17 08:36:14 PM UTC | 0 ⭐ | 306 👁️ | Never ⏰ | []
C# |

13.58 KB

|

None

|

0 👍

/

0 👎

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<object> fnSuccess = null, Action<RPCFault> fnFault = null)
    {
        MBHook = mBHook;
        return Call(method, parms, fnSuccess, fnFault);
    }
 
    public bool Call(string method, object[] parms = null, Action<object> fnSuccess = null, Action<RPCFault> 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<object> fnSuccess = null, Action<RPCFault> 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<object> fnSuccess = null, Action<RPCFault> 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<object> fnSuccess, Action<RPCFault> 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 <value><type>
        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 = "<data>" + S.ToString() + "</data>";
            }
            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("<member>");
                        S.AppendLine("<name>" + Key + "</name>");
                        S.AppendLine(Node);
                        S.AppendLine("</member>");
                    }
                }
 
                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 : "<value><" + TypeStr + ">" + RetVal.ToString() + "</" + TypeStr + "></value>";
    }
 
    protected string EncodeXMLRPCRequest(string method, object[] parms)
    { // returns an XMLRPC-formatted object to send to a server
        StringBuilder XMLRPC = new StringBuilder();
        XMLRPC.AppendLine("<?xml version = \"1.0\" ?>");
        XMLRPC.AppendLine("<methodCall>");
        XMLRPC.AppendLine("<methodName>" + method + "</methodName>");
 
        if (parms != null)
        {
            StringBuilder S = new StringBuilder();
 
            foreach (object Item in parms)
            {
                string ObEncoded = EncodeXMLRPCValue(Item);
                if (ObEncoded != null)
                    S.AppendLine("<param>" + ObEncoded + "</param>");
            }
 
            if (S.Length > 0)
                XMLRPC.AppendLine("<params>" + S.ToString() + "</params>");
        }
 
        XMLRPC.AppendLine("</methodCall>");
        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 <type> 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<object> ObArr = new List<object>();
 
                foreach (XmlNode ArrayNode in ValueNode.FirstChild) // there's a single <data> 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 <member> 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 <fault> 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;
    }
}

Comments