// $Id$ // // Copyright 2007-2008 Cisco Systems Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. using System; using System.IO; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using System.Net; using Etch.Transport; using Etch.Util; using System.Reflection; namespace BasicHttpListener { public delegate void NewClient(HttpSession session); public class HttpServerImpl : Transport { private List list; // used by reference implementation, not etch tool chain public event NewClient NewClientConnected; public object methodLock = new object(); public static ConstructorInfo JsonReaderConstructor { get { return jsonReaderConstructor; } } public static ConstructorInfo JsonWriterConstructor { get { return jsonWriterConstructor; } } public static MethodInfo JsonReaderDeserialize { get { return jsonReaderDeserialize; } } public static MethodInfo JsonWriterWrite { get { return jsonWriterWrite; } } private static Assembly jsonFxAssembly; private static ConstructorInfo jsonReaderConstructor; private static ConstructorInfo jsonWriterConstructor; private static MethodInfo jsonReaderDeserialize; private static MethodInfo jsonWriterWrite; #region Field Constants public const string VersionField = "version"; public const string SessionId = "sessionId"; public const string MethodName = "method"; public const string Arguments = "args"; public const string EventMessages = "eventMessages"; public const string TransactionId = "transactionId"; public const string Data = "data"; #endregion public enum UriType { start, request, @event, response, invalid, comettest } #region Error Message Constants public const string RequestBodyParseFailure = "Unable to parse the JSON request body."; public const string NoSessionId = "Unable to find a SessionId in the JSON request body."; #endregion private static Regex uriChecker = new Regex(@"^http.*/(?(start|request|event|response|comettest))/?.*$", RegexOptions.Compiled | RegexOptions.Singleline); private HttpListener listener = null; private SessionIdFactory factory = null; private AsyncCallback callback; private static long asyncTracker = 0; private object comettest; private HttpServerHandler session; public HttpServerImpl( bool secure, short port ) : this( secure, port, new SimpleSessionIdFactory() ) { } public HttpServerImpl(bool secure, short port, SessionIdFactory factory) : this() { this.list = new List(); this.factory = factory; this.callback = new AsyncCallback(ReceivedRequest); this.listener = new HttpListener(); this.listener.Prefixes.Add("http" + (secure ? "s" : "") + "://+:" + port + "/"); this.comettest = new object(); } protected HttpServerImpl() { string jsonFxPath = null; try { // Determine path of this dll. It is expected that JsonFx.Json.dll live in the same directory. FileInfo callingAssembly = new FileInfo( Assembly.GetCallingAssembly().Location ); jsonFxPath = Path.Combine( callingAssembly.DirectoryName, "JsonFx.Json.dll" ); jsonFxAssembly = Assembly.LoadFile( jsonFxPath ); } catch(FileNotFoundException fnfe) { string jsonFxDllNotFound = String.Format("Unable to find JsonFx.Json.dll at path {0}! Exception:\n{1}", jsonFxAssembly, fnfe); Console.WriteLine( jsonFxDllNotFound ); throw new Exception( jsonFxDllNotFound ); } catch( Exception e ) { string jsonFxDllNotLoaded = "Unable to load JsonFx.Json.dll! Exception:\n" + e; Console.WriteLine( jsonFxDllNotLoaded ); throw new Exception( jsonFxDllNotLoaded ); } try { Type readerType = jsonFxAssembly.GetType( "JsonFx.Json.JsonReader", true ); jsonReaderConstructor = readerType.GetConstructor( BindingFlags.ExactBinding | BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance, null, CallingConventions.Any, new Type[] { typeof( Stream ) }, null ); jsonReaderDeserialize = readerType.GetMethod( "Deserialize", new Type[0] ); } catch( Exception e ) { string jsonReaderUnavailable = "JsonFx.Json.JsonReader not found or not of the expected interface in JsonFx.Json.dll. " + e; Console.WriteLine( jsonReaderUnavailable ); throw new Exception( jsonReaderUnavailable ); } try { Type writerType = jsonFxAssembly.GetType( "JsonFx.Json.JsonWriter", true ); jsonWriterConstructor = writerType.GetConstructor( BindingFlags.ExactBinding | BindingFlags.Instance | BindingFlags.Public | BindingFlags.CreateInstance, null, CallingConventions.Any, new Type[] { typeof( TextWriter ) }, null ); jsonWriterWrite = writerType.GetMethod( "Write", new Type[] { typeof(Dictionary) } ); } catch( Exception e ) { string jsonWriterUnavailable = "JsonFx.Json.JsonWriter not found or not of the expected interface in JsonFx.Json.dll. " + e; Console.WriteLine( jsonWriterUnavailable ); throw new Exception( jsonWriterUnavailable ); } } public void Start() { listener.Start(); listener.BeginGetContext(callback, asyncTracker++); } public void ReceivedRequest(IAsyncResult asyncResult) { // TODO: Remove LOCK //lock(methodLock) // There is no reason for this to be single threaded like this--this is only temporary to prove certain stress tests //{ try { listener.BeginGetContext(callback, asyncTracker++); HttpListenerContext context = listener.EndGetContext(asyncResult); // Serve up HandleRequest(context.Request, context.Response); } catch(Exception e) { Console.WriteLine("Unexpected error: " + e); } //} } public void Stop() { // notify all open clients of server shutdown NotifyShutdown(); listener.Close(); } private void NotifyShutdown() { // ...TODO } /// Entry point for handling requests and serving responses private void HandleRequest(HttpListenerRequest request, HttpListenerResponse response) { Console.WriteLine( "request: {0}", request.Url ); #region Sanity Checks // check content-type //if(!IsCorrectContentType(request.ContentType)) //{ // SendBadContentTypeResponse(response); // return; //} // check if is valid URI UriType type; if(!IsValidURI(request.Url.ToString(), out type)) { SendUnknownUri(request.Url.ToString(), response); return; } #region Hacky Test Code if(type == UriType.comettest) { System.Threading.Thread.Sleep(5000); list.Add(response); ThreadStorage storage = new ThreadStorage(); storage.timer = new System.Threading.Timer(new System.Threading.TimerCallback(timer_Elapsed), storage, 30000, System.Threading.Timeout.Infinite); return; } #endregion response.KeepAlive = true; Dictionary data = null; try { Object reader = jsonReaderConstructor.Invoke( new object[] { request.InputStream } ); data = (Dictionary) jsonReaderDeserialize.Invoke( reader, null ); //JsonReader reader = new JsonReader(request.InputStream); //data = reader.Deserialize() as Dictionary; } catch(Exception e) { Console.WriteLine("Exception in parsing request message.\n{0}", e); SendMalformedRequest(RequestBodyParseFailure, response); return; } #endregion response.ContentType = "application/json"; // determine if this is a new session, extracting sessionId if found if(IsNewSession(type)) { HttpSession session = HttpSessionHandler.CreateSession(this.factory, request.RemoteEndPoint.Address.ToString()); // Should be sending this near bottom, when we know whether or not we have successfully created the Etch session SendNewSession( session, response ); System.Diagnostics.Debug.Assert( this.session != null, "Unable to indicate to HttpServerHandler that a new client has connected." ); try { // TODO: refactor out this check once reference implementation stabilizes-- // this event is only consumed by the reference implementation if( NewClientConnected != null ) { NewClientConnected( session ); } this.session.NewClient( session ); } catch( Exception e ) { Console.WriteLine( "Unable to notify server of new client.\n" + e ); } } else { String sessionId = ExtractSessionId(data); if(sessionId != null) { // The version of the message int version = ExtractVersion( data ); // if this is any request HttpSession session = HttpSessionHandler.GetSession(sessionId); // This must be session bound. Otherwise we have an issue! if(session != null) { Dictionary responseData = null; // determine the type of message switch(type) { case UriType.request: String methodName = ExtractMethodName(data); if(methodName != null) { responseData = session.HandleIncomingChannel(version, methodName, ExtractTransactionId(data), ExtractArguments(data)); } else { SendMalformedRequest("No method defined.", response); } break; case UriType.@event: session.HandleOutgoingChannel(response); break; case UriType.response: String originalMethodName = ExtractMethodName( data ); string transactionId = ExtractTransactionId(data); responseData = session.HandleResponseChannel( version, transactionId, originalMethodName, ExtractReturnData(data) ); break; } if(type != UriType.@event) { // send the response back StreamWriter streamWriter = null; try { streamWriter = new StreamWriter(response.OutputStream); Object writer = jsonWriterConstructor.Invoke( new object[] { streamWriter } ); jsonWriterWrite.Invoke( writer, new object[] { responseData } ); //JsonWriter writer = new JsonWriter(streamWriter); //writer.Write(responseData); } catch(Exception e) { Console.WriteLine("Unable to respond to the session.\n{0}", e); } finally { if(streamWriter != null) { streamWriter.Dispose(); } response.Close(); } } } else { SendNoSession(response); } } } } #region Hacky Test Code public class ThreadStorage { public ThreadStorage() { } public System.Threading.Timer timer; } // void timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) void timer_Elapsed(object sender) { lock(comettest) { try { if(list.Count > 0) { HttpListenerResponse response = list[0]; list.RemoveAt(0); response.ContentType = "text/plain"; StreamWriter writer = new StreamWriter(response.OutputStream); writer.WriteLine("oeuoeuoeu"); writer.Close(); writer.Dispose(); } ThreadStorage storage = sender as ThreadStorage; storage.timer.Dispose(); } catch(Exception exp) { Console.WriteLine(exp); } } } #endregion /// Confirms that the content-type is 'application/json' /// The content-type of the request /// true if the content-type is 'application/json' private bool IsCorrectContentType(string contentType) { return String.Compare(contentType, "application/json", true) == 0; } private UriType DetermineUriType(string uri) { UriType type = UriType.invalid; Match match = uriChecker.Match(uri); if(match != null) { string typeValue = match.Groups["type"].Value; if(Enum.IsDefined(typeof(UriType), typeValue)) { type = (UriType) Enum.Parse(typeof(UriType), typeValue); } } return type; } /// Determines if this is a request initiating a new session. /// The type of the request private bool IsNewSession(UriType type) { return type == UriType.start; } private string ExtractSessionId(Dictionary data) { String sessionId = data[SessionId] as string; return sessionId; } private int ExtractVersion( Dictionary data ) { int version = (int) data[VersionField]; return version; } private string ExtractMethodName(Dictionary data) { String methodName = data[MethodName] as string; return methodName; } private string ExtractTransactionId(Dictionary data) { string transactionId = data[TransactionId] as string; return transactionId; } private Dictionary ExtractArguments(Dictionary data) { Dictionary arguments = null; if(data.ContainsKey(Arguments)) { arguments = data[Arguments] as Dictionary; } return arguments; } private object ExtractReturnData( Dictionary data ) { object value = null; if(data.ContainsKey(Data)) { value = data[Data]; } return value; } private bool IsValidURI(string uri, out UriType type) { type = DetermineUriType(uri); return type != UriType.invalid; } private void SendBadContentTypeResponse(HttpListenerResponse response) { response.StatusCode = 500; // send json response body with error message System.Diagnostics.Debug.Assert(false, "Not implemented"); } private void SendNoSession(HttpListenerResponse response) { Console.WriteLine( "No session exists!" ); //System.Diagnostics.Debug.Assert(false, "Not implemented"); } private void SendUnknownUri(string uri, HttpListenerResponse response) { System.Diagnostics.Debug.Assert(false, "Not implemented"); } private void SendNewSession(HttpSession session, HttpListenerResponse response) { StreamWriter streamie = new StreamWriter(response.OutputStream); Dictionary data = new Dictionary(); data[SessionId] = session.Id; Object writer = jsonWriterConstructor.Invoke( new object[] { streamie } ); jsonWriterWrite.Invoke( writer, new object[] { data } ); //JsonWriter writer = new JsonWriter( streamie ); //writer.Write( data ); streamie.Dispose(); response.Close(); } private void SendMalformedRequest(string message, HttpListenerResponse response) { System.Diagnostics.Debug.Assert(false, "Not implemented"); } #region Transport Members public void TransportControl( object control, object value ) { if( control.Equals( TransportConsts.START ) ) { Start(); return; } if( control.Equals( TransportConsts.START_AND_WAIT_UP ) ) { Start(); return; } if( control.Equals( TransportConsts.STOP ) ) { Stop(); return; } if( control is TransportConsts.WaitDown ) { Stop(); return; } if( control.Equals( TransportConsts.STOP_AND_WAIT_DOWN ) ) { Stop(); return; } throw new Exception( "unknown control " + control ); } public void TransportNotify( object eventObj ) { // What should be done here? throw new Exception( "The method or operation is not implemented." ); } public object TransportQuery( object query ) { // What should be done here? throw new Exception( "The method or operation is not implemented." ); } #endregion #region Transport Members public HttpServerHandler GetSession() { throw new Exception( "The method or operation is not implemented." ); } public void SetSession( HttpServerHandler session ) { this.session = session; } #endregion } }