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