/// <summary> /// Used internally. Feeds a <see cref="Record">Record</see> to this request for processing. /// </summary> /// <param name="record">The record to feed.</param> /// <returns>Returns true iff the request is completely received.</returns> internal bool HandleRecord(Record record) { switch (record.Type) { case Record.RecordType.Params: if (record.ContentLength == 0) { ParamStream.Seek(0, SeekOrigin.Begin); Parameters = Record.ReadNameValuePairs(ParamStream); } else { // If the params are not yet finished, write the contents to the ParamStream. ParamStream.Write(record.ContentData, 0, record.ContentLength); } break; case Record.RecordType.Stdin: string data = Encoding.ASCII.GetString(record.ContentData); Body += data; // Finished requests are indicated by an empty stdin record if (record.ContentLength == 0) return true; break; } return false; }
public void FCGIApp_Connections() { // Create an app that actually listens on the loopback interface var app = new FCGIApplication(); app.Timeout = 100; try { app.Listen(new IPEndPoint(IPAddress.Loopback, 21511)); Request receivedRequest = null; app.OnRequestReceived += (sender, request) => { receivedRequest = request; request.WriteResponseASCII("Hello Server!"); request.Close(); }; app.Process(); var serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp); var asyncResult = serverSocket.BeginConnect(new IPEndPoint(IPAddress.Loopback, 21511), (r) => { }, null); while(!asyncResult.IsCompleted) app.Process(); serverSocket.EndConnect(asyncResult); var serverStream = new NetworkStream(serverSocket); // Now send a request to the app and make sure it is received correctly. var requestId = 5172; var beginRequestContent = new byte[] { 0x00, // role byte 1 (byte)Constants.FCGI_RESPONDER, // role byte 2 0x00, // flags 0x00, 0x00, 0x00, 0x00, 0x00 // reserved bytes }; var beginRequest = new Record { Type = Record.RecordType.BeginRequest, RequestId = requestId, ContentLength = beginRequestContent.Length, ContentData = beginRequestContent }; beginRequest.WriteToStream(serverStream); var stdinRequest = new Record { Type = Record.RecordType.Stdin, RequestId = requestId, ContentLength = 0, ContentData = new byte[0] }; stdinRequest.WriteToStream(serverStream); serverStream.Flush(); // Make the app digest everything while(receivedRequest == null) { app.Process(); } // Do we have the correct request? Assert.AreEqual(requestId, receivedRequest.RequestId); // And did the response work? var responseRecord = Record.ReadRecord(serverStream); Assert.AreEqual(Record.RecordType.Stdout, responseRecord.Type); Assert.AreEqual("Hello Server!", Encoding.ASCII.GetString(responseRecord.ContentData)); // Change the timeout while still connected app.Timeout = 100; app.StopListening(); Assert.IsFalse(app.Connected); } // If the port is already in use, an execution can be thrown. // Report the test as inconclusive then. catch(SocketException e) { Assert.Inconclusive("SocketException: " + e.Message); } }
public void FCGIApp_LargeRequest() { var app = new FCGIApplication(); var expectedLength = 128317; app.OnRequestReceived += (sender, request) => { var responseBody = new byte[expectedLength]; request.WriteResponse(responseBody); request.Close(); }; // Connect to the app, impoersonating a webserver var inputStream = new MemoryStream(); inputStream.Capacity = 4096; var outputStream = new MemoryStream(); outputStream.Capacity = 4096 + expectedLength; // Send a request to it and make sure it responds to requests. var requestId = 5172; var beginRequestContent = new byte[] { 0x00, // role byte 1 (byte)Constants.FCGI_RESPONDER, // role byte 2 Constants.FCGI_KEEP_CONN, // flags 0x00, 0x00, 0x00, 0x00, 0x00 // reserved bytes }; var beginRequest = new Record { Type = Record.RecordType.BeginRequest, RequestId = requestId, ContentLength = beginRequestContent.Length, ContentData = beginRequestContent }; beginRequest.WriteToStream(inputStream); // Empty stdin indicates that the request is fully transmitted var stdinRequest = new Record { Type = Record.RecordType.Stdin, RequestId = requestId, ContentLength = 0, ContentData = new byte[0] }; stdinRequest.WriteToStream(inputStream); inputStream.Seek(0, SeekOrigin.Begin); app.ProcessStream(inputStream, outputStream); outputStream.Seek(0, SeekOrigin.Begin); // Now the app should respond with a large response, splitted into 64KB stdout records var response = Record.ReadRecord(outputStream); Assert.AreEqual(65535, response.ContentLength); var totalLength = 0; while (response.ContentLength > 0) { Assert.AreEqual(Record.RecordType.Stdout, response.Type); Assert.AreEqual(requestId, response.RequestId); totalLength += response.ContentLength; response = Record.ReadRecord(outputStream); } Assert.AreEqual(expectedLength, totalLength); // Then, an empty stdout record should indicate the end of the response body Assert.AreEqual(Record.RecordType.Stdout, response.Type); Assert.AreEqual(requestId, response.RequestId); Assert.AreEqual(0, response.ContentLength); // And finally, a EndRequest record should close the request. response = Record.ReadRecord(outputStream); Assert.AreEqual(Record.RecordType.EndRequest, response.Type); Assert.AreEqual(requestId, response.RequestId); }
public void FCGIApp_Request() { var app = new FCGIApplication(); app.OnRequestReceived += (sender, request) => { request.WriteResponseASCII("Hello!"); request.Close(); }; // Connect to the app, impoersonating a webserver var streamServerToApp = new MemoryStream(); streamServerToApp.Capacity = 4096; var streamAppToServer = new MemoryStream(); streamAppToServer.Capacity = 4096; // Send a request to it and make sure it responds to requests. var requestId = 5172; var beginRequestContent = new byte[] { 0x00, // role byte 1 (byte)Constants.FCGI_RESPONDER, // role byte 2 Constants.FCGI_KEEP_CONN, // flags 0x00, 0x00, 0x00, 0x00, 0x00 // reserved bytes }; var beginRequest = new Record { Type = Record.RecordType.BeginRequest, RequestId = requestId, ContentLength = beginRequestContent.Length, ContentData = beginRequestContent }; beginRequest.WriteToStream(streamServerToApp); // Empty stdin indicates that the request is fully transmitted var stdinRequest = new Record { Type = Record.RecordType.Stdin, RequestId = requestId, ContentLength = 0, ContentData = new byte[0] }; stdinRequest.WriteToStream(streamServerToApp); streamServerToApp.Seek(0, SeekOrigin.Begin); app.ProcessStream(streamServerToApp, streamAppToServer);; streamAppToServer.Seek(0, SeekOrigin.Begin); // Now the app should respond with 'Hello!' var response = Record.ReadRecord(streamAppToServer); var expectedBytes = Encoding.ASCII.GetBytes("Hello!"); Assert.AreEqual(Record.RecordType.Stdout, response.Type); Assert.AreEqual(requestId, response.RequestId); Assert.AreEqual(expectedBytes.Length, response.ContentLength); Assert.AreEqual(expectedBytes, response.ContentData); // Then, an empty stdout record should indicate the end of the response body response = Record.ReadRecord(streamAppToServer); Assert.AreEqual(Record.RecordType.Stdout, response.Type); Assert.AreEqual(requestId, response.RequestId); Assert.AreEqual(0, response.ContentLength); // And finally, a EndRequest record should close the request. response = Record.ReadRecord(streamAppToServer); Assert.AreEqual(Record.RecordType.EndRequest, response.Type); Assert.AreEqual(requestId, response.RequestId); }
public void FCGIApp_GetValues() { var app = new FCGIApplication(); // Connect to the app, impoersonating a webserver var inputStream = new MemoryStream(); inputStream.Capacity = 4096; var outputStream = new MemoryStream(); outputStream.Capacity = 4096; // Send a GetValues record var getValues = new Record { Type = Record.RecordType.GetValues, RequestId = 0, ContentLength = 0, }; getValues.WriteToStream(inputStream); inputStream.Seek(0, SeekOrigin.Begin); app.ProcessStream(inputStream, outputStream); outputStream.Seek(0, SeekOrigin.Begin); // Now the app should respond with a GetValuesResult var response = Record.ReadRecord(outputStream); Assert.AreEqual(Record.RecordType.GetValuesResult, response.Type); Assert.AreEqual(0, response.RequestId); var responseValues = response.GetNameValuePairs(); // Response should include these three values Assert.Contains("FCGI_MAX_CONNS", responseValues.Keys); Assert.Contains("FCGI_MAX_REQS", responseValues.Keys); Assert.Contains("FCGI_MPXS_CONNS", responseValues.Keys); // No other values should be included Assert.AreEqual(3, responseValues.Count); int responseMaxConns, responseMaxReqs, responseMultiplexing; // Make sure the returned values look plausible var sMaxConns = Encoding.ASCII.GetString(responseValues["FCGI_MAX_CONNS"]); var sMaxReqs = Encoding.ASCII.GetString(responseValues["FCGI_MAX_REQS"]); var sMultiplexing = Encoding.ASCII.GetString(responseValues["FCGI_MPXS_CONNS"]); Assert.IsTrue(int.TryParse(sMaxConns, out responseMaxConns)); Assert.IsTrue(int.TryParse(sMaxReqs, out responseMaxReqs)); Assert.IsTrue(int.TryParse(sMultiplexing, out responseMultiplexing)); Assert.GreaterOrEqual(responseMaxConns, 0); Assert.GreaterOrEqual(responseMaxReqs, 0); Assert.GreaterOrEqual(responseMultiplexing, 0); Assert.LessOrEqual(responseMultiplexing, 1); }
/// <summary> /// Tries to read and handle a <see cref="Record"/> from inputStream and writes responses to outputStream. /// </summary> /// <remarks> /// Use <see cref="ProcessStream"/> to process all records on a stream. /// Returns true if a record was read, false otherwise. /// </remarks> public bool ProcessSingleRecord(Stream inputStream, Stream outputStream) { if (!inputStream.CanRead) { return(false); } Record r = Record.ReadRecord(inputStream); // No record found on the stream? if (r == null) { return(false); } if (r.Type == Record.RecordType.BeginRequest) { if (OpenRequests.ContainsKey(r.RequestId)) { OpenRequests.Remove(r.RequestId); } var content = new MemoryStream(r.ContentData); var role = Record.ReadInt16(content); // Todo: Refuse requests for other roles than FCGI_RESPONDER var flags = content.ReadByte(); var keepAlive = (flags & Constants.FCGI_KEEP_CONN) != 0; var request = new Request(r.RequestId, outputStream, this, keepAlive: keepAlive); OpenRequests.Add(request.RequestId, request); var incomingHandler = OnRequestIncoming; if (incomingHandler != null) { incomingHandler(this, request); } } else if (r.Type == Record.RecordType.AbortRequest || r.Type == Record.RecordType.EndRequest) { OpenRequests.Remove(r.RequestId); } else if (r.Type == Record.RecordType.GetValues) { var getValuesResult = Record.CreateGetValuesResult(1, 1, false); getValuesResult.Send(outputStream); } else { if (OpenRequests.ContainsKey(r.RequestId)) { var request = OpenRequests[r.RequestId]; bool requestReady = request.HandleRecord(r); if (requestReady) { var evh = OnRequestReceived; if (evh != null) { OnRequestReceived(this, request); } } } } return(true); }
public void FCGIApp_Params() { var app = new FCGIApplication(); Request receivedRequest = null; app.OnRequestReceived += (sender, request) => { receivedRequest = request; }; // Connect to the app, impoersonating a webserver var streamServerToApp = new MemoryStream(); streamServerToApp.Capacity = 4096; var streamAppToServer = new MemoryStream(); streamAppToServer.Capacity = 4096; // Send a request to it and make sure it responds to requests. var requestId = 5172; var beginRequestContent = new byte[] { 0x00, // role byte 1 (byte)Constants.FCGI_RESPONDER, // role byte 2 Constants.FCGI_KEEP_CONN, // flags 0x00, 0x00, 0x00, 0x00, 0x00 // reserved bytes }; var beginRequest = new Record { Type = Record.RecordType.BeginRequest, RequestId = requestId, ContentLength = beginRequestContent.Length, ContentData = beginRequestContent }; beginRequest.WriteToStream(streamServerToApp); var paramDict = new Dictionary<string, byte[]> { {"SOME_NAME", Encoding.UTF8.GetBytes("SOME_KEY") }, {"SOME_OTHER_NAME", Encoding.UTF8.GetBytes("SOME_KEY") }, //{"ONE_MEGABYTE_OF_ZEROS", new byte[1024 * 1024] }, {"EMPTY_VALUE", new byte[0] }, {"1", Encoding.UTF8.GetBytes("☕☳üß - \n \r\n .,;(){}%$!") }, {"2", Encoding.UTF8.GetBytes("") }, {"3", Encoding.UTF8.GetBytes(" ") }, {"4", Encoding.UTF8.GetBytes("\n") }, {"5", Encoding.UTF8.GetBytes(";") }, }; var paramsRequest = new Record { Type = Record.RecordType.Params, RequestId = requestId }; paramsRequest.SetNameValuePairs(paramDict); paramsRequest.WriteToStream(streamServerToApp); // Empty params record indicates that the parameters are fully transmitted var paramsCloseRequest = new Record { Type = Record.RecordType.Params, RequestId = requestId, ContentLength = 0, ContentData = new byte[0] }; paramsCloseRequest.WriteToStream(streamServerToApp); // Empty stdin indicates that the request is fully transmitted var stdinRequest = new Record { Type = Record.RecordType.Stdin, RequestId = requestId, ContentLength = 0, ContentData = new byte[0] }; stdinRequest.WriteToStream(streamServerToApp); streamServerToApp.Seek(0, SeekOrigin.Begin); app.ProcessStream(streamServerToApp, streamAppToServer); // Now the app should have received the request. Make sure the Parameters correctly decoded Assert.IsNotNull(receivedRequest); Assert.AreEqual(paramDict.Count, receivedRequest.Parameters.Count); foreach (var entry in paramDict) { Assert.Contains(entry.Key, receivedRequest.Parameters.Keys); Assert.AreEqual(entry.Value, receivedRequest.Parameters[entry.Key]); } }
public void Records_ToString() { var someRecord = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.Stderr, RequestId = 123, ContentLength = 15, ContentData = new byte[15] }; string stringified = someRecord.ToString(); Assert.AreEqual("{Record type: Stderr, requestId: 123}", stringified); }
public void Records_Equals() { var recordA1 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.Stderr, RequestId = 123, ContentLength = 1, ContentData = new byte[1] { 1 } }; var recordA2 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.Stderr, RequestId = 123, ContentLength = 1, ContentData = new byte[1] { 1 } }; var recordB1 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.BeginRequest, RequestId = 321, ContentLength = 1, ContentData = new byte[1] { 1 } }; var recordB2 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.BeginRequest, RequestId = 321, ContentLength = 1, ContentData = new byte[1] { 1 } }; var recordC1 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.BeginRequest, RequestId = 321, ContentLength = 1, ContentData = new byte[1] { 2 } }; var recordC2 = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.BeginRequest, RequestId = 321, ContentLength = 1, ContentData = new byte[1] { 2 } }; Assert.IsTrue(recordA1.Equals(recordA2)); Assert.IsTrue(recordB1.Equals(recordB2)); Assert.IsTrue(recordC1.Equals(recordC2)); Assert.IsFalse(recordA1.Equals(recordB2)); Assert.IsFalse(recordA1.Equals(recordB1)); Assert.IsFalse(recordA1.Equals(recordC1)); Assert.IsFalse(recordB1.Equals(recordA1)); Assert.IsFalse(recordB1.Equals(recordA2)); Assert.IsFalse(recordB1.Equals(recordC1)); Assert.IsFalse(recordB1.Equals(null)); Assert.IsFalse(recordB1.Equals(5)); }
public void Records_BrokenRecords() { // Create some broken records and make sure the right things happen Record result; // An empty stream should simply result in a null record var emptyStream = new MemoryStream(new byte[0]); result = Record.ReadRecord(emptyStream); Assert.IsNull(result); // A wrong version number bytes should throw a InvalidDataException var wrongVersion = new MemoryStream(new byte[1] { Constants.FCGI_VERSION_1 + 1 }); Assert.Throws(typeof(InvalidDataException), () => { Record.ReadRecord(wrongVersion); }); // So should a bunch of zeroes var zeroes = new MemoryStream(new byte[4] { 0, 0, 0, 0 }); Assert.Throws(typeof(InvalidDataException), () => { Record.ReadRecord(zeroes); }); // And a correct version number with an incomplete header var incomplete = new MemoryStream(new byte[4] { Constants.FCGI_VERSION_1, 0, 0, 0 }); Assert.Throws(typeof(InvalidDataException), () => { Record.ReadRecord(incomplete); }); // Writing should fail for content above 64KB var tooLong = new Record { Version = Constants.FCGI_VERSION_1, Type = Record.RecordType.Stderr, RequestId = 123, ContentLength = 127000, ContentData = new byte[127000] }; Assert.Throws(typeof(InvalidOperationException), () => { tooLong.WriteToStream(new MemoryStream()); }); }
/// <summary> /// Creates a GetValuesResult record from the given config values. /// </summary> public static Record CreateGetValuesResult(int maxConnections, int maxRequests, bool multiplexing) { var nameValuePairs = new Dictionary<string, byte[]>(); // Names and values are encoded as strings. nameValuePairs.Add(Constants.FCGI_MAX_CONNS, Encoding.ASCII.GetBytes(maxConnections.ToString(CultureInfo.InvariantCulture))); nameValuePairs.Add(Constants.FCGI_MAX_REQS, Encoding.ASCII.GetBytes(maxRequests.ToString(CultureInfo.InvariantCulture))); nameValuePairs.Add(Constants.FCGI_MPXS_CONNS, Encoding.ASCII.GetBytes(multiplexing ? "1" : "0")); var record = new Record { RequestId = 0, Type = RecordType.GetValuesResult }; record.SetNameValuePairs(nameValuePairs); return record; }
/// <summary> /// Reads a single Record from the given stream. /// </summary> /// <remarks> /// Returns the retreived record or null if no record could be read. /// Will block if a partial record is on the stream, until the full record has arrived or a timeout occurs. /// </remarks> public static Record ReadRecord(Stream stream) { Record r = new Record(); try { // Try to read a byte from the stream int firstByte = stream.ReadByte(); // Reached end of stream? if (firstByte == -1) return null; // Otherwise, that first byte should be the version byte of a record. // We now assume that we can safely read the rest of the record. // If the rest of the record has not yet been received, it should follow soon. r.Version = (byte)firstByte; if (r.Version != Constants.FCGI_VERSION_1) throw new InvalidDataException("Invalid version number in FastCGI record header. Possibly corrupted data."); r.Type = (Record.RecordType)ReadByte(stream); r.RequestId = ReadInt16(stream); r.ContentLength = ReadInt16(stream); byte paddingLength = ReadByte(stream); // Skip reserved byte ReadByte(stream); r.ContentData = new byte[r.ContentLength]; // Read content if(r.ContentLength > 0) stream.Read(r.ContentData, 0, r.ContentLength); // Skip padding data if (paddingLength > 0) { byte[] ignoredBuf = new byte[paddingLength]; stream.Read(ignoredBuf, 0, paddingLength); } } catch (EndOfStreamException e) { throw new InvalidDataException("Unexpected end of stream. Incomplete record transmitted or corrupted data."); } catch (IOException e) { // Connection has been closed, or an other error occured whie reading a record. Return a null record. return null; } return r; }