// $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.Net; namespace Etch.HttpJsonTransport.TestUtilities { /// A trivial, embeddable HTTP server that can serve up files and also proxy HTTP requests. /// Its purpose in life is to facilite running tests without having an involved test environment. /// /// The mode of operation is basically this: /// 1) You give the Http server a set of files that it will attempt to load into memory, upon initialization. /// 2) When an HTTP request comes in, it will look to see if the incoming request URI seems to match a /// loaded file. If it does, the file gets served out. If their is no match for the request, then /// the HTTP server proxies the HTTP request to pre-configured HTTP proxy URL. That's about it! public class HttpProxyServer { private HttpListener listener; private AsyncCallback callback; private long asyncTracker = 0; private WebFile[] knownFiles; private Uri destinationUri; public class WebFile { public FileInfo file; public string data; } public HttpProxyServer( FileInfo[] files ) { this.knownFiles = CheckFiles( files ); this.listener = new HttpListener(); this.callback = new AsyncCallback( ReceivedRequest ); } /// Invoke to open the Http Server Listener port /// Example: proxy.Start( "http://localhost:12221/", new Uri("http://localhost:12222/proxy/") ); /// The URL that the server listens on. /// The URL that the server should proxy requests on to. public void Start( string serverUrl, Uri destinationUri ) { this.destinationUri = destinationUri; this.listener.Prefixes.Add( serverUrl ); this.listener.Start(); this.listener.BeginGetContext( callback, asyncTracker++ ); } /// Invoke to stop and cleanup the Http Server Listener public void Stop() { try { listener.Stop(); listener.Close(); } catch( Exception e ) { Console.WriteLine( "Stop the HttpListener for the Http Proxy. " + e ); } } /// Responsible for handing off the request to HandleRequest /// as well as informing the HTTP Listener class to begin public void ReceivedRequest( IAsyncResult asyncResult ) { try { listener.BeginGetContext( callback, asyncTracker++ ); HttpListenerContext context = listener.EndGetContext( asyncResult ); HandleRequest( context.Request, context.Response ); } catch( Exception e ) { Console.WriteLine( "Unexpected error in request handler in HttpProxy server: " + e ); } } /// Contains most all logic pertaining to the handling of a new HTTP request private void HandleRequest( HttpListenerRequest request, HttpListenerResponse response ) { WebFile intendedFile; string proxiedUrl; Console.WriteLine( "Request: " + request.Url.AbsolutePath ); if( IsFileRequest( request.Url.AbsolutePath, out intendedFile ) ) { // serve out this file response.StatusCode = 200; StreamWriter writer = new StreamWriter( response.OutputStream ); writer.Write( intendedFile.data ); writer.Dispose(); response.Close(); } else if( IsProxyRequest( request.Url, out proxiedUrl ) ) { HttpWebRequest newRequest = (HttpWebRequest) HttpWebRequest.Create( proxiedUrl ); newRequest.Timeout = 35000; newRequest.ProtocolVersion = request.ProtocolVersion; newRequest.Method = request.HttpMethod; foreach( string header in request.Headers.AllKeys ) { string loweredHeader = header.ToLower(); string value = request.Headers[header]; switch( loweredHeader ) { case "accept": newRequest.Accept = value; break; case "content-type": newRequest.ContentType = value; break; case "connection": if( String.Compare( value, "Keep-Alive", true ) == 0 ) { newRequest.KeepAlive = true; } else { newRequest.KeepAlive = false; } break; case "content-length": // skip break; case "expect": newRequest.Expect = value; break; case "host": // skip break; case "if-modified-since": newRequest.IfModifiedSince = DateTime.Parse( value ); break; case "referer": newRequest.Referer = value; break; case "transfer-encoding": newRequest.TransferEncoding = value; break; case "user-agent": newRequest.UserAgent = value; break; default: newRequest.Headers[header] = value; break; } } char[] buffer = new char[2000]; int index = 0; if( newRequest.Method == "POST" ) { StreamReader originalReader = new StreamReader( request.InputStream ); StreamWriter newWriter = new StreamWriter( newRequest.GetRequestStream() ); while( originalReader.Peek() > -1 ) { int read = 0; read = originalReader.ReadBlock( buffer, index, buffer.Length ); index += read; newWriter.Write( buffer, 0, read ); } originalReader.Close(); originalReader.Dispose(); newWriter.Close(); newWriter.Dispose(); } HttpWebResponse newResponse = (HttpWebResponse) newRequest.GetResponse(); response.StatusCode = (int) newResponse.StatusCode; foreach( string header in newResponse.Headers.AllKeys ) { string loweredHeader = header.ToLower(); string value = newResponse.Headers[header]; switch( loweredHeader ) { case "transfer-encoding": // skip break; default: response.Headers.Add( header, value ); break; } } StreamReader newReader = new StreamReader( newResponse.GetResponseStream() ); StreamWriter originalWriter = new StreamWriter( response.OutputStream ); index = 0; while( newReader.Peek() > -1 ) { int read = 0; read = newReader.ReadBlock( buffer, index, buffer.Length ); index += read; originalWriter.Write( buffer, 0, read ); } newReader.Close(); newReader.Dispose(); originalWriter.Close(); originalWriter.Dispose(); } else { response.StatusCode = (int) HttpStatusCode.NotFound; response.Close(); } } /// Checks to see if the HTTP request is intended for proxying to a backend service private bool IsProxyRequest( Uri originalUri, out string proxiedUrl ) { proxiedUrl = null; bool isProxyRequest = false; // example: // destinationUri: http://localhost:12222/proxy/ // destinationPath example: /proxy/ // intendPath example: /proxy/start string destinationPath = destinationUri.PathAndQuery; string intendedPath = originalUri.PathAndQuery; if( intendedPath.StartsWith( destinationPath ) ) { // we have ourselves a proxy request! isProxyRequest = true; // rip off the proxy prefix string extraPath = intendedPath.Substring( destinationPath.Length ); proxiedUrl = destinationUri.ToString() + extraPath; } return isProxyRequest; } /// Checks to see if HTTP request is for a known file. private bool IsFileRequest( string originalUrl, out WebFile intendedFile ) { intendedFile = null; bool foundFile = false; foreach( WebFile file in knownFiles ) { // a file request will end with the file name as the originalUri if( originalUrl.EndsWith( file.file.Name ) ) { intendedFile = file; foundFile = true; break; } } return foundFile; } /// Loads all files into memory. Throws an exception if any file is missing. private WebFile[] CheckFiles( FileInfo[] files ) { List fileData = new List(); foreach( FileInfo file in files ) { if( file.Exists ) { WebFile webFile = new WebFile(); webFile.file = file; StreamReader reader = new StreamReader( file.Open( FileMode.Open ) ); string fullText = reader.ReadToEnd(); webFile.data = fullText; reader.Close(); fileData.Add( webFile ); } else { throw new Exception( String.Format( "Unable to open file {0}", file.FullName ) ); } } return fileData.ToArray(); } } }