WebUtil.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Collections.Specialized;
  30. using System.IO;
  31. using System.Net;
  32. using System.Net.Security;
  33. using System.Reflection;
  34. using System.Text;
  35. using System.Web;
  36. using log4net;
  37. using OpenSim.Framework.Servers.HttpServer;
  38. using OpenMetaverse.StructuredData;
  39. namespace OpenSim.Framework
  40. {
  41. /// <summary>
  42. /// A generic HTTP request handler
  43. /// </summary>
  44. public class HttpStreamHandler : IStreamHandler
  45. {
  46. public delegate void HttpStreamCallback(OSHttpRequest httpRequest, OSHttpResponse httpResponse);
  47. private string m_contentType;
  48. private string m_httpMethod;
  49. private string m_path;
  50. private HttpStreamCallback m_callback;
  51. public string ContentType { get { return m_contentType; } }
  52. public string HttpMethod { get { return m_httpMethod; } }
  53. public string Path { get { return m_path; } }
  54. /// <summary>
  55. /// Constructor
  56. /// </summary>
  57. public HttpStreamHandler(string contentType, string httpMethod, string path, HttpStreamCallback callback)
  58. {
  59. m_contentType = contentType;
  60. m_httpMethod = httpMethod;
  61. m_path = path;
  62. m_callback = callback;
  63. }
  64. public void Handle(string path, Stream request, Stream response, OSHttpRequest httpRequest, OSHttpResponse httpResponse)
  65. {
  66. m_callback(httpRequest, httpResponse);
  67. }
  68. }
  69. /// <summary>
  70. /// Miscellaneous static methods and extension methods related to the web
  71. /// </summary>
  72. public static class WebUtil
  73. {
  74. private static readonly ILog m_log =
  75. LogManager.GetLogger(
  76. MethodBase.GetCurrentMethod().DeclaringType);
  77. /// <summary>
  78. /// Send LLSD to an HTTP client in application/llsd+json form
  79. /// </summary>
  80. /// <param name="response">HTTP response to send the data in</param>
  81. /// <param name="body">LLSD to send to the client</param>
  82. public static void SendJSONResponse(OSHttpResponse response, OSDMap body)
  83. {
  84. byte[] responseData = Encoding.UTF8.GetBytes(OSDParser.SerializeJsonString(body));
  85. response.ContentEncoding = Encoding.UTF8;
  86. response.ContentLength = responseData.Length;
  87. response.ContentType = "application/llsd+json";
  88. response.Body.Write(responseData, 0, responseData.Length);
  89. }
  90. /// <summary>
  91. /// Send LLSD to an HTTP client in application/llsd+xml form
  92. /// </summary>
  93. /// <param name="response">HTTP response to send the data in</param>
  94. /// <param name="body">LLSD to send to the client</param>
  95. public static void SendXMLResponse(OSHttpResponse response, OSDMap body)
  96. {
  97. byte[] responseData = OSDParser.SerializeLLSDXmlBytes(body);
  98. response.ContentEncoding = Encoding.UTF8;
  99. response.ContentLength = responseData.Length;
  100. response.ContentType = "application/llsd+xml";
  101. response.Body.Write(responseData, 0, responseData.Length);
  102. }
  103. /// <summary>
  104. /// Make a GET or GET-like request to a web service that returns LLSD
  105. /// or JSON data
  106. /// </summary>
  107. public static OSDMap ServiceRequest(string url, string httpVerb)
  108. {
  109. string errorMessage;
  110. try
  111. {
  112. HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
  113. request.Method = httpVerb;
  114. using (WebResponse response = request.GetResponse())
  115. {
  116. using (Stream responseStream = response.GetResponseStream())
  117. {
  118. try
  119. {
  120. string responseStr = responseStream.GetStreamString();
  121. OSD responseOSD = OSDParser.Deserialize(responseStr);
  122. if (responseOSD.Type == OSDType.Map)
  123. return (OSDMap)responseOSD;
  124. else
  125. errorMessage = "Response format was invalid.";
  126. }
  127. catch
  128. {
  129. errorMessage = "Failed to parse the response.";
  130. }
  131. }
  132. }
  133. }
  134. catch (Exception ex)
  135. {
  136. m_log.Warn("GET from URL " + url + " failed: " + ex.Message);
  137. errorMessage = ex.Message;
  138. }
  139. return new OSDMap { { "Message", OSD.FromString("Service request failed. " + errorMessage) } };
  140. }
  141. /// <summary>
  142. /// POST URL-encoded form data to a web service that returns LLSD or
  143. /// JSON data
  144. /// </summary>
  145. public static OSDMap PostToService(string url, NameValueCollection data)
  146. {
  147. string errorMessage;
  148. try
  149. {
  150. string queryString = BuildQueryString(data);
  151. byte[] requestData = System.Text.Encoding.UTF8.GetBytes(queryString);
  152. HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(url);
  153. request.Method = "POST";
  154. request.ContentLength = requestData.Length;
  155. request.ContentType = "application/x-www-form-urlencoded";
  156. using (Stream requestStream = request.GetRequestStream())
  157. requestStream.Write(requestData, 0, requestData.Length);
  158. using (WebResponse response = request.GetResponse())
  159. {
  160. using (Stream responseStream = response.GetResponseStream())
  161. {
  162. string responseStr = null;
  163. try
  164. {
  165. responseStr = responseStream.GetStreamString();
  166. OSD responseOSD = OSDParser.Deserialize(responseStr);
  167. if (responseOSD.Type == OSDType.Map)
  168. return (OSDMap)responseOSD;
  169. else
  170. errorMessage = "Response format was invalid.";
  171. }
  172. catch (Exception ex)
  173. {
  174. if (!String.IsNullOrEmpty(responseStr))
  175. errorMessage = "Failed to parse the response:\n" + responseStr;
  176. else
  177. errorMessage = "Failed to retrieve the response: " + ex.Message;
  178. }
  179. }
  180. }
  181. }
  182. catch (Exception ex)
  183. {
  184. m_log.Warn("POST to URL " + url + " failed: " + ex.Message);
  185. errorMessage = ex.Message;
  186. }
  187. return new OSDMap { { "Message", OSD.FromString("Service request failed. " + errorMessage) } };
  188. }
  189. /// <summary>
  190. /// Encodes a URL string
  191. /// </summary>
  192. /// <param name="str">Unencoded string to encode</param>
  193. /// <returns>URL-encoded string</returns>
  194. public static string UrlEncode(string str)
  195. {
  196. return HttpUtility.UrlEncode(str).Replace("+", "%20");
  197. }
  198. #region Uri
  199. /// <summary>
  200. /// Combines a Uri that can contain both a base Uri and relative path
  201. /// with a second relative path fragment
  202. /// </summary>
  203. /// <param name="uri">Starting (base) Uri</param>
  204. /// <param name="fragment">Relative path fragment to append to the end
  205. /// of the Uri</param>
  206. /// <returns>The combined Uri</returns>
  207. /// <remarks>This is similar to the Uri constructor that takes a base
  208. /// Uri and the relative path, except this method can append a relative
  209. /// path fragment on to an existing relative path</remarks>
  210. public static Uri Combine(this Uri uri, string fragment)
  211. {
  212. string fragment1 = uri.Fragment;
  213. string fragment2 = fragment;
  214. if (!fragment1.EndsWith("/"))
  215. fragment1 = fragment1 + '/';
  216. if (fragment2.StartsWith("/"))
  217. fragment2 = fragment2.Substring(1);
  218. return new Uri(uri, fragment1 + fragment2);
  219. }
  220. /// <summary>
  221. /// Combines a Uri that can contain both a base Uri and relative path
  222. /// with a second relative path fragment. If the fragment is absolute,
  223. /// it will be returned without modification
  224. /// </summary>
  225. /// <param name="uri">Starting (base) Uri</param>
  226. /// <param name="fragment">Relative path fragment to append to the end
  227. /// of the Uri, or an absolute Uri to return unmodified</param>
  228. /// <returns>The combined Uri</returns>
  229. public static Uri Combine(this Uri uri, Uri fragment)
  230. {
  231. if (fragment.IsAbsoluteUri)
  232. return fragment;
  233. string fragment1 = uri.Fragment;
  234. string fragment2 = fragment.ToString();
  235. if (!fragment1.EndsWith("/"))
  236. fragment1 = fragment1 + '/';
  237. if (fragment2.StartsWith("/"))
  238. fragment2 = fragment2.Substring(1);
  239. return new Uri(uri, fragment1 + fragment2);
  240. }
  241. /// <summary>
  242. /// Appends a query string to a Uri that may or may not have existing
  243. /// query parameters
  244. /// </summary>
  245. /// <param name="uri">Uri to append the query to</param>
  246. /// <param name="query">Query string to append. Can either start with ?
  247. /// or just containg key/value pairs</param>
  248. /// <returns>String representation of the Uri with the query string
  249. /// appended</returns>
  250. public static string AppendQuery(this Uri uri, string query)
  251. {
  252. if (String.IsNullOrEmpty(query))
  253. return uri.ToString();
  254. if (query[0] == '?' || query[0] == '&')
  255. query = query.Substring(1);
  256. string uriStr = uri.ToString();
  257. if (uriStr.Contains("?"))
  258. return uriStr + '&' + query;
  259. else
  260. return uriStr + '?' + query;
  261. }
  262. #endregion Uri
  263. #region NameValueCollection
  264. /// <summary>
  265. /// Convert a NameValueCollection into a query string. This is the
  266. /// inverse of HttpUtility.ParseQueryString()
  267. /// </summary>
  268. /// <param name="parameters">Collection of key/value pairs to convert</param>
  269. /// <returns>A query string with URL-escaped values</returns>
  270. public static string BuildQueryString(NameValueCollection parameters)
  271. {
  272. List<string> items = new List<string>(parameters.Count);
  273. foreach (string key in parameters.Keys)
  274. {
  275. string[] values = parameters.GetValues(key);
  276. if (values != null)
  277. {
  278. foreach (string value in values)
  279. items.Add(String.Concat(key, "=", HttpUtility.UrlEncode(value ?? String.Empty)));
  280. }
  281. }
  282. return String.Join("&", items.ToArray());
  283. }
  284. /// <summary>
  285. ///
  286. /// </summary>
  287. /// <param name="collection"></param>
  288. /// <param name="key"></param>
  289. /// <returns></returns>
  290. public static string GetOne(this NameValueCollection collection, string key)
  291. {
  292. string[] values = collection.GetValues(key);
  293. if (values != null && values.Length > 0)
  294. return values[0];
  295. return null;
  296. }
  297. #endregion NameValueCollection
  298. #region Stream
  299. /// <summary>
  300. /// Copies the contents of one stream to another, starting at the
  301. /// current position of each stream
  302. /// </summary>
  303. /// <param name="copyFrom">The stream to copy from, at the position
  304. /// where copying should begin</param>
  305. /// <param name="copyTo">The stream to copy to, at the position where
  306. /// bytes should be written</param>
  307. /// <param name="maximumBytesToCopy">The maximum bytes to copy</param>
  308. /// <returns>The total number of bytes copied</returns>
  309. /// <remarks>
  310. /// Copying begins at the streams' current positions. The positions are
  311. /// NOT reset after copying is complete.
  312. /// </remarks>
  313. public static int CopyTo(this Stream copyFrom, Stream copyTo, int maximumBytesToCopy)
  314. {
  315. byte[] buffer = new byte[4096];
  316. int readBytes;
  317. int totalCopiedBytes = 0;
  318. while ((readBytes = copyFrom.Read(buffer, 0, Math.Min(4096, maximumBytesToCopy))) > 0)
  319. {
  320. int writeBytes = Math.Min(maximumBytesToCopy, readBytes);
  321. copyTo.Write(buffer, 0, writeBytes);
  322. totalCopiedBytes += writeBytes;
  323. maximumBytesToCopy -= writeBytes;
  324. }
  325. return totalCopiedBytes;
  326. }
  327. /// <summary>
  328. /// Converts an entire stream to a string, regardless of current stream
  329. /// position
  330. /// </summary>
  331. /// <param name="stream">The stream to convert to a string</param>
  332. /// <returns></returns>
  333. /// <remarks>When this method is done, the stream position will be
  334. /// reset to its previous position before this method was called</remarks>
  335. public static string GetStreamString(this Stream stream)
  336. {
  337. string value = null;
  338. if (stream != null && stream.CanRead)
  339. {
  340. long rewindPos = -1;
  341. if (stream.CanSeek)
  342. {
  343. rewindPos = stream.Position;
  344. stream.Seek(0, SeekOrigin.Begin);
  345. }
  346. StreamReader reader = new StreamReader(stream);
  347. value = reader.ReadToEnd();
  348. if (rewindPos >= 0)
  349. stream.Seek(rewindPos, SeekOrigin.Begin);
  350. }
  351. return value;
  352. }
  353. #endregion Stream
  354. }
  355. }