/* * Copyright (c) Contributors, http://opensimulator.org/ * See CONTRIBUTORS.TXT for a full list of copyright holders. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the OpenSimulator Project nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections.Generic; using System.IO; using System.Net; using System.Reflection; using System.Text; using System.Threading; using System.Web; using log4net; using OpenSim.Framework.ServiceAuth; namespace OpenSim.Framework { /// /// Implementation of a generic REST client /// /// /// This class is a generic implementation of a REST (Representational State Transfer) web service. This /// class is designed to execute both synchronously and asynchronously. /// /// Internally the implementation works as a two stage asynchronous web-client. /// When the request is initiated, RestClient will query asynchronously for for a web-response, /// sleeping until the initial response is returned by the server. Once the initial response is retrieved /// the second stage of asynchronous requests will be triggered, in an attempt to read of the response /// object into a memorystream as a sequence of asynchronous reads. /// /// The asynchronisity of RestClient is designed to move as much processing into the back-ground, allowing /// other threads to execute, while it waits for a response from the web-service. RestClient itself can be /// invoked by the caller in either synchronous mode or asynchronous modes. /// public class RestClient : IDisposable { private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType); // private string realuri; #region member variables /// /// The base Uri of the web-service e.g. http://www.google.com /// private string _url; /// /// Path elements of the query /// private List _pathElements = new List(); /// /// Parameter elements of the query, e.g. min=34 /// private Dictionary _parameterElements = new Dictionary(); /// /// Request method. E.g. GET, POST, PUT or DELETE /// private string _method; /// /// Temporary buffer used to store bytes temporarily as they come in from the server /// private byte[] _readbuf; /// /// MemoryStream representing the resulting resource /// private Stream _resource; /// /// WebRequest object, held as a member variable /// private HttpWebRequest _request; /// /// WebResponse object, held as a member variable, so we can close it /// private HttpWebResponse _response; /// /// This flag will help block the main synchroneous method, in case we run in synchroneous mode /// //public static ManualResetEvent _allDone = new ManualResetEvent(false); /// /// Default time out period /// //private const int DefaultTimeout = 10*1000; // 10 seconds timeout /// /// Default Buffer size of a block requested from the web-server /// private const int BufferSize = 4096; // Read blocks of 4 KB. /// /// if an exception occours during async processing, we need to save it, so it can be /// rethrown on the primary thread; /// private Exception _asyncException; #endregion member variables #region constructors /// /// Instantiate a new RestClient /// /// Web-service to query, e.g. http://osgrid.org:8003 public RestClient(string url) { _url = url; _readbuf = new byte[BufferSize]; _resource = new MemoryStream(); _request = null; _response = null; _lock = new object(); } private object _lock; #endregion constructors #region Dispose private bool disposed = false; public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposed) return; if (disposing) { _resource.Dispose(); } disposed = true; } #endregion Dispose /// /// Add a path element to the query, e.g. assets /// /// path entry public void AddResourcePath(string element) { if (isSlashed(element)) _pathElements.Add(element.Substring(0, element.Length - 1)); else _pathElements.Add(element); } /// /// Add a query parameter to the Url /// /// Name of the parameter, e.g. min /// Value of the parameter, e.g. 42 public void AddQueryParameter(string name, string value) { try { _parameterElements.Add(HttpUtility.UrlEncode(name), HttpUtility.UrlEncode(value)); } catch (ArgumentException) { m_log.Error("[REST]: Query parameter " + name + " is already added."); } catch (Exception e) { m_log.Error("[REST]: An exception was raised adding query parameter to dictionary. Exception: {0}",e); } } /// /// Add a query parameter to the Url /// /// Name of the parameter, e.g. min public void AddQueryParameter(string name) { try { _parameterElements.Add(HttpUtility.UrlEncode(name), null); } catch (ArgumentException) { m_log.Error("[REST]: Query parameter " + name + " is already added."); } catch (Exception e) { m_log.Error("[REST]: An exception was raised adding query parameter to dictionary. Exception: {0}",e); } } /// /// Web-Request method, e.g. GET, PUT, POST, DELETE /// public string RequestMethod { get { return _method; } set { _method = value; } } /// /// True if string contains a trailing slash '/' /// /// string to be examined /// true if slash is present private static bool isSlashed(string s) { return s.Substring(s.Length - 1, 1) == "/"; } /// /// Build a Uri based on the initial Url, path elements and parameters /// /// fully constructed Uri private Uri buildUri() { StringBuilder sb = new StringBuilder(); sb.Append(_url); foreach (string e in _pathElements) { sb.Append("/"); sb.Append(e); } bool firstElement = true; foreach (KeyValuePair kv in _parameterElements) { if (firstElement) { sb.Append("?"); firstElement = false; } else sb.Append("&"); sb.Append(kv.Key); if (!string.IsNullOrEmpty(kv.Value)) { sb.Append("="); sb.Append(kv.Value); } } // realuri = sb.ToString(); //m_log.InfoFormat("[REST CLIENT]: RestURL: {0}", realuri); return new Uri(sb.ToString()); } #region Async communications with server /// /// Async method, invoked when a block of data has been received from the service /// /// private void StreamIsReadyDelegate(IAsyncResult ar) { try { Stream s = (Stream) ar.AsyncState; int read = s.EndRead(ar); if (read > 0) { _resource.Write(_readbuf, 0, read); // IAsyncResult asynchronousResult = // s.BeginRead(_readbuf, 0, BufferSize, new AsyncCallback(StreamIsReadyDelegate), s); s.BeginRead(_readbuf, 0, BufferSize, new AsyncCallback(StreamIsReadyDelegate), s); // TODO! Implement timeout, without killing the server //ThreadPool.RegisterWaitForSingleObject(asynchronousResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); } else { s.Close(); //_allDone.Set(); } } catch (Exception e) { //_allDone.Set(); _asyncException = e; } } #endregion Async communications with server /// /// Perform a synchronous request /// public Stream Request() { return Request(null); } /// /// Perform a synchronous request /// public Stream Request(IServiceAuth auth) { lock (_lock) { _request = (HttpWebRequest) WebRequest.Create(buildUri()); _request.KeepAlive = false; _request.ContentType = "application/xml"; _request.Timeout = 200000; _request.Method = RequestMethod; _asyncException = null; if (auth != null) auth.AddAuthorization(_request.Headers); int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.DebugFormat("[LOGHTTP]: HTTP OUT {0} REST {1} to {2}", reqnum, _request.Method, _request.RequestUri); // IAsyncResult responseAsyncResult = _request.BeginGetResponse(new AsyncCallback(ResponseIsReadyDelegate), _request); try { using (_response = (HttpWebResponse) _request.GetResponse()) { using (Stream src = _response.GetResponseStream()) { int length = src.Read(_readbuf, 0, BufferSize); while (length > 0) { _resource.Write(_readbuf, 0, length); length = src.Read(_readbuf, 0, BufferSize); } // TODO! Implement timeout, without killing the server // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted //ThreadPool.RegisterWaitForSingleObject(responseAsyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); // _allDone.WaitOne(); } } } catch (WebException e) { using (HttpWebResponse errorResponse = e.Response as HttpWebResponse) { if (null != errorResponse && HttpStatusCode.NotFound == errorResponse.StatusCode) { // This is often benign. E.g., requesting a missing asset will return 404. m_log.DebugFormat("[REST CLIENT] Resource not found (404): {0}", _request.Address.ToString()); } else { m_log.Error(string.Format("[REST CLIENT] Error fetching resource from server: {0} ", _request.Address.ToString()), e); } } return null; } if (_asyncException != null) throw _asyncException; if (_resource != null) { _resource.Flush(); _resource.Seek(0, SeekOrigin.Begin); } if (WebUtil.DebugLevel >= 5) WebUtil.LogResponseDetail(reqnum, _resource); return _resource; } } public Stream Request(Stream src, IServiceAuth auth) { _request = (HttpWebRequest) WebRequest.Create(buildUri()); _request.KeepAlive = false; _request.ContentType = "application/xml"; _request.Timeout = 90000; _request.Method = RequestMethod; _asyncException = null; _request.ContentLength = src.Length; if (auth != null) auth.AddAuthorization(_request.Headers); src.Seek(0, SeekOrigin.Begin); int reqnum = WebUtil.RequestNumber++; if (WebUtil.DebugLevel >= 3) m_log.DebugFormat("[LOGHTTP]: HTTP OUT {0} REST {1} to {2}", reqnum, _request.Method, _request.RequestUri); if (WebUtil.DebugLevel >= 5) WebUtil.LogOutgoingDetail(string.Format("SEND {0}: ", reqnum), src); try { using (Stream dst = _request.GetRequestStream()) { // m_log.Debug("[REST]: GetRequestStream is ok"); byte[] buf = new byte[1024]; int length = src.Read(buf, 0, 1024); // m_log.Debug("[REST]: First Read is ok"); while (length > 0) { dst.Write(buf, 0, length); length = src.Read(buf, 0, 1024); } } _response = (HttpWebResponse)_request.GetResponse(); } catch (WebException e) { m_log.WarnFormat("[REST]: Request {0} {1} failed with status {2} and message {3}", RequestMethod, _request.RequestUri, e.Status, e.Message); return null; } catch (Exception e) { m_log.WarnFormat( "[REST]: Request {0} {1} failed with exception {2} {3}", RequestMethod, _request.RequestUri, e.Message, e.StackTrace); return null; } if (WebUtil.DebugLevel >= 5) { using (Stream responseStream = _response.GetResponseStream()) { using (StreamReader reader = new StreamReader(responseStream)) { string responseStr = reader.ReadToEnd(); WebUtil.LogResponseDetail(reqnum, responseStr); } } } if (_response != null) _response.Close(); // IAsyncResult responseAsyncResult = _request.BeginGetResponse(new AsyncCallback(ResponseIsReadyDelegate), _request); // TODO! Implement timeout, without killing the server // this line implements the timeout, if there is a timeout, the callback fires and the request becomes aborted //ThreadPool.RegisterWaitForSingleObject(responseAsyncResult.AsyncWaitHandle, new WaitOrTimerCallback(TimeoutCallback), _request, DefaultTimeout, true); return null; } #region Async Invocation public IAsyncResult BeginRequest(AsyncCallback callback, object state) { /// /// In case, we are invoked asynchroneously this object will keep track of the state /// AsyncResult ar = new AsyncResult(callback, state); Util.FireAndForget(RequestHelper, ar, "RestClient.BeginRequest"); return ar; } public Stream EndRequest(IAsyncResult asyncResult) { AsyncResult ar = (AsyncResult) asyncResult; // Wait for operation to complete, then return result or // throw exception return ar.EndInvoke(); } private void RequestHelper(Object asyncResult) { // We know that it's really an AsyncResult object AsyncResult ar = (AsyncResult) asyncResult; try { // Perform the operation; if sucessful set the result Stream s = Request(null); ar.SetAsCompleted(s, false); } catch (Exception e) { // If operation fails, set the exception ar.HandleException(e, false); } } #endregion Async Invocation } internal class SimpleAsyncResult : IAsyncResult { private readonly AsyncCallback m_callback; /// /// Is process completed? /// /// Should really be boolean, but VolatileRead has no boolean method private byte m_completed; /// /// Did process complete synchronously? /// /// I have a hard time imagining a scenario where this is the case, again, same issue about /// booleans and VolatileRead as m_completed /// private byte m_completedSynchronously; private readonly object m_asyncState; private ManualResetEvent m_waitHandle; private Exception m_exception; internal SimpleAsyncResult(AsyncCallback cb, object state) { m_callback = cb; m_asyncState = state; m_completed = 0; m_completedSynchronously = 1; } #region IAsyncResult Members public object AsyncState { get { return m_asyncState; } } public WaitHandle AsyncWaitHandle { get { if (m_waitHandle == null) { bool done = IsCompleted; ManualResetEvent mre = new ManualResetEvent(done); if (Interlocked.CompareExchange(ref m_waitHandle, mre, null) != null) { mre.Close(); } else { if (!done && IsCompleted) { m_waitHandle.Set(); } } } return m_waitHandle; } } public bool CompletedSynchronously { get { return Thread.VolatileRead(ref m_completedSynchronously) == 1; } } public bool IsCompleted { get { return Thread.VolatileRead(ref m_completed) == 1; } } #endregion #region class Methods internal void SetAsCompleted(bool completedSynchronously) { m_completed = 1; if (completedSynchronously) m_completedSynchronously = 1; else m_completedSynchronously = 0; SignalCompletion(); } internal void HandleException(Exception e, bool completedSynchronously) { m_completed = 1; if (completedSynchronously) m_completedSynchronously = 1; else m_completedSynchronously = 0; m_exception = e; SignalCompletion(); } private void SignalCompletion() { if (m_waitHandle != null) m_waitHandle.Set(); if (m_callback != null) m_callback(this); } public void EndInvoke() { // This method assumes that only 1 thread calls EndInvoke if (!IsCompleted) { // If the operation isn't done, wait for it AsyncWaitHandle.WaitOne(); AsyncWaitHandle.Close(); m_waitHandle.Close(); m_waitHandle = null; // Allow early GC } // Operation is done: if an exception occured, throw it if (m_exception != null) throw m_exception; } #endregion } internal class AsyncResult : SimpleAsyncResult { private T m_result = default(T); public AsyncResult(AsyncCallback asyncCallback, Object state) : base(asyncCallback, state) { } public void SetAsCompleted(T result, bool completedSynchronously) { // Save the asynchronous operation's result m_result = result; // Tell the base class that the operation completed // sucessfully (no exception) base.SetAsCompleted(completedSynchronously); } public new T EndInvoke() { base.EndInvoke(); return m_result; } } }