I am trying to extend my HTTP Server with TLS Support
My HTTP Server is here
using System;using System.IO;using System.Net;using System.Net.Sockets;using System.Text;using System.Threading.Tasks;using RawHttpListener.TLS;namespace RawHttpListener{ #region HTTPParser public interface IHTTPClientHandler { Task HTTPClientConnected(HttpContext ctx); } public class HttpContext { public HttpRequest Request { get; set; } public HttpResponse Response { get; set; } public HttpContext(HttpRequest request, HttpResponse response) { Request = request; Response = response; } } public class HttpRequest { public string HttpMethod { get; set; } public string Url { get; set; } public string UserHostName { get; set; } public string UserAgent { get; set; } public string Body { get; set; } public HttpRequest(string httpMethod, string url, string userHostName, string userAgent) { HttpMethod = httpMethod; Url = url; UserHostName = userHostName; UserAgent = userAgent; } } public class HttpResponse { public string ContentType { get; set; } public Encoding ContentEncoding { get; set; } public long ContentLength64 { get; set; } public Stream OutputStream { get; set; } public HttpResponse(Stream outputStream) { OutputStream = outputStream; } } public class HTTPParser : Stream { private readonly IHTTPClientHandler _clientHandler; private readonly Stream _baseStream; private readonly MemoryStream _memoryStream = new MemoryStream(); private const int BufferSize = 4096; private readonly byte[] _buffer = new byte[BufferSize]; public HTTPParser(Stream baseStream, IHTTPClientHandler clientHandler) { _baseStream = baseStream; _clientHandler = clientHandler; } public async Task HandleClientConnected() { int bytesRead; // Read the incoming data until the connection is closed while ((bytesRead = await _baseStream.ReadAsync(_buffer, 0, _buffer.Length)) > 0) { await _memoryStream.WriteAsync(_buffer, 0, bytesRead); // Check for the end of the request (empty line means headers end) if (bytesRead < BufferSize) break; } // Convert the raw request to a string string rawRequest = Encoding.UTF8.GetString(_memoryStream.ToArray()); Console.WriteLine("Raw HTTP Request:"); Console.WriteLine(rawRequest); // Handle the HTTP request await HandleHttpRequest(rawRequest); } private async Task HandleHttpRequest(string rawRequest) { // Fill the MyOwnHttpListenerContext object and call the HTTPClientConnected method var lines = rawRequest.Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries); if (lines.Length == 0) return; // Parse the request line (first line) var requestLine = lines[0].Split(''); if (requestLine.Length < 3) return; // Invalid request string method = requestLine[0]; // e.g., GET, POST string url = requestLine[1]; // e.g., /path string httpVersion = requestLine[2]; // e.g., HTTP/1.1 // Extract headers string userHostName = string.Empty; string userAgent = string.Empty; for (int i = 1; i < lines.Length; i++) // Skip the request line { if (lines[i].StartsWith("Host:")) { userHostName = lines[i].Substring(6).Trim(); // Skip "Host: " } else if (lines[i].StartsWith("User-Agent:")) { userAgent = lines[i].Substring(12).Trim(); // Skip "User-Agent: " } } // Create the request and response objects var request = new HttpRequest(method, url, userHostName, userAgent); var response = new HttpResponse(_baseStream); var ctx = new HttpContext(request, response); await _clientHandler.HTTPClientConnected(ctx); } // Implementing abstract members of Stream class public override bool CanRead => _baseStream.CanRead; public override bool CanSeek => _baseStream.CanSeek; public override bool CanWrite => _baseStream.CanWrite; public override long Length => _baseStream.Length; public override long Position { get => _baseStream.Position; set => _baseStream.Position = value; } public override void Flush() => _baseStream.Flush(); public override int Read(byte[] buffer, int offset, int count) => _baseStream.Read(buffer, offset, count); public override long Seek(long offset, SeekOrigin origin) => _baseStream.Seek(offset, origin); public override void SetLength(long value) => _baseStream.SetLength(value); public override void Write(byte[] buffer, int offset, int count) => _baseStream.Write(buffer, offset, count); } #endregion #region RawHttpServer public class RawHttpServer : IHTTPClientHandler { public string ipAddress = string.Empty; // Change to your desired IP address public int port; // Use a non-privileged port for testing bool isHTTPS; public RawHttpServer() { this.ipAddress = "172.0.0.1"; this.port = 80; this.isHTTPS = false; } public RawHttpServer(IPAddress ip, int port, bool isHTTPS) { this.ipAddress = ip.ToString(); this.port = port; this.isHTTPS = isHTTPS; } public void Start() { TcpListener tcpListener = new TcpListener(IPAddress.Parse(ipAddress), port); tcpListener.Start(); Console.WriteLine($"Listening for connections on {ipAddress}:{port}"); while (true) { TcpClient tcpClient = tcpListener.AcceptTcpClient(); Task.Run(() => HandleClient(tcpClient)); } } private async Task HandleClient(TcpClient tcpClient) { using (NetworkStream stream = tcpClient.GetStream()) { HTTPParser httpParser; if (!this.isHTTPS) { //Reading RAW request, for debugging purposes //byte[] buffer = new byte[4096]; // Adjust the buffer size as necessary //int bytesRead; //while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0) //{ // // Handle the data // string data = Encoding.UTF8.GetString(buffer, 0, bytesRead); // Console.WriteLine("Data: " + data); //} httpParser = new HTTPParser(stream, this); await httpParser.HandleClientConnected(); // Handle the client connection } else { // Wrap the inner stream with TlsStream //using (var tlsStream = new TlsStream(stream, "cert.pem", "key.pem", new[] { "http/1.1" })) using (var tlsStream = new TlsStream(stream, "cert.pem", "key.pem", new[] { "http/1.1" })) { // Perform the TLS handshake await tlsStream.DoHandshakeAsync(); // Read decrypted data byte[] buffer = new byte[4096]; // Adjust the buffer size as necessary int bytesRead; while ((bytesRead = await tlsStream.ReadAsync(buffer, 0, buffer.Length)) > 0) { // Handle the decrypted data string decryptedData = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine("Decrypted Data: " + decryptedData); } // Create a new HTTP parser using the TlsStream for the connection httpParser = new HTTPParser(tlsStream, this); await httpParser.HandleClientConnected(); // Handle the client connection } } } tcpClient.Close(); } public async Task HTTPClientConnected(HttpContext ctx) { // Print out some info about the request Console.WriteLine($"URL: {ctx.Request.Url}"); Console.WriteLine($"Method: {ctx.Request.HttpMethod}"); Console.WriteLine($"User Host Name: {ctx.Request.UserHostName}"); Console.WriteLine($"User Agent: {ctx.Request.UserAgent}"); Console.WriteLine(); if (ctx.Request.HttpMethod == "POST") { Console.WriteLine("POST BODY:"); // Here you can handle the POST body if needed } // Write the response info byte[] data = Encoding.UTF8.GetBytes("<html><body><h1>Hello World</h1></body></html>"); ctx.Response.ContentType = "text/html"; ctx.Response.ContentEncoding = Encoding.UTF8; ctx.Response.ContentLength64 = data.Length; // Write out to the response stream (asynchronously), then close it await ctx.Response.OutputStream.WriteAsync(data, 0, data.Length); ctx.Response.OutputStream.Close(); // Close the output stream } } #endregion //Our main class here class Program { static void Main(string[] args) { bool isHTTPS = true; RawHttpServer server = new RawHttpServer(IPAddress.Parse("192.168.88.12"), 443, isHTTPS); server.Start(); } }}
Here is my TLSStream.cs file
using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Runtime.InteropServices;using System.Text;using System.Threading;using System.Threading.Tasks;namespace RawHttpListener.TLS{ public class TlsStream : Stream { private static unsafe OpenSsl.alpn_select_cb_t _alpnSelectCallback = AlpnSelectCallback; private readonly Stream _innerStream; private readonly byte[] _protocols; private readonly GCHandle _protocolsHandle; private IntPtr _ctx; private IntPtr _ssl; private IntPtr _inputBio; private IntPtr _outputBio; private readonly byte[] _inputBuffer = new byte[1024 * 1024]; private readonly byte[] _outputBuffer = new byte[1024 * 1024]; static TlsStream() { OpenSsl.SSL_library_init(); OpenSsl.SSL_load_error_strings(); OpenSsl.ERR_load_BIO_strings(); OpenSsl.OpenSSL_add_all_algorithms(); } public TlsStream(Stream innerStream, string certificatePath, string privateKeyPath, IEnumerable<string> protocols) { _innerStream = innerStream; _protocols = ToWireFormat(protocols); _protocolsHandle = GCHandle.Alloc(_protocols); _ctx = OpenSsl.SSL_CTX_new(OpenSsl.TLSv1_2_method()); if (_ctx == IntPtr.Zero) { throw new Exception("Unable to create SSL context."); } OpenSsl.SSL_CTX_set_ecdh_auto(_ctx, 1); if (OpenSsl.SSL_CTX_use_certificate_file(_ctx, certificatePath, 1) != 1) { throw new Exception("Unable to load certificate file."); } if (OpenSsl.SSL_CTX_use_PrivateKey_file(_ctx, privateKeyPath, 1) != 1) { throw new Exception("Unable to load private key file."); } OpenSsl.SSL_CTX_set_alpn_select_cb(_ctx, _alpnSelectCallback, GCHandle.ToIntPtr(_protocolsHandle)); _ssl = OpenSsl.SSL_new(_ctx); _inputBio = OpenSsl.BIO_new(OpenSsl.BIO_s_mem()); OpenSsl.BIO_set_mem_eof_return(_inputBio, -1); _outputBio = OpenSsl.BIO_new(OpenSsl.BIO_s_mem()); OpenSsl.BIO_set_mem_eof_return(_outputBio, -1); OpenSsl.SSL_set_bio(_ssl, _inputBio, _outputBio); } ~TlsStream() { if (_ssl != IntPtr.Zero) { OpenSsl.SSL_free(_ssl); } if (_ctx != IntPtr.Zero) { // This frees the BIOs. OpenSsl.SSL_CTX_free(_ctx); } if (_protocolsHandle.IsAllocated) { _protocolsHandle.Free(); } } public override bool CanRead => true; public override bool CanWrite => true; public override bool CanSeek => false; public override long Length => throw new NotSupportedException(); public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); public override void SetLength(long value) => throw new NotSupportedException(); public override void Flush() { FlushAsync(default(CancellationToken)).GetAwaiter().GetResult(); } public override int Read(byte[] buffer, int offset, int count) { return ReadAsync(buffer, offset, count).GetAwaiter().GetResult(); } public override void Write(byte[] buffer, int offset, int count) { WriteAsync(buffer, offset, count).GetAwaiter().GetResult(); } public override async Task FlushAsync(CancellationToken cancellationToken) { var pending = OpenSsl.BIO_ctrl_pending(_outputBio); while (pending > 0) { var count = OpenSsl.BIO_read(_outputBio, _outputBuffer, 0, _outputBuffer.Length); await _innerStream.WriteAsync(_outputBuffer, 0, count, cancellationToken); pending = OpenSsl.BIO_ctrl_pending(_outputBio); } } public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { if (OpenSsl.BIO_ctrl_pending(_inputBio) == 0) { var bytesRead = await _innerStream.ReadAsync(_inputBuffer, 0, _inputBuffer.Length, cancellationToken); if (bytesRead == 0) { return 0; } OpenSsl.BIO_write(_inputBio, _inputBuffer, 0, bytesRead); } return OpenSsl.SSL_read(_ssl, buffer, offset, count); } public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { OpenSsl.SSL_write(_ssl, buffer, offset, count); return FlushAsync(cancellationToken); } public void DoHandshake() { OpenSsl.SSL_set_accept_state(_ssl); var count = 0; try { while ((count = _innerStream.Read(_inputBuffer, 0, _inputBuffer.Length)) > 0) { if (count == 0) { throw new IOException("TLS handshake failed: the inner stream was closed."); } OpenSsl.BIO_write(_inputBio, _inputBuffer, 0, count); var ret = OpenSsl.SSL_do_handshake(_ssl); if (ret != 1) { var error = OpenSsl.SSL_get_error(_ssl, ret); if (error != 2) // Typically SSL_ERROR_WANT_READ, handled non-blocking in async { throw new IOException($"TLS handshake failed: {nameof(OpenSsl.SSL_do_handshake)} error {error}."); } } Flush(); // Replace FlushAsync with its synchronous counterpart if (ret == 1) { return; // Handshake successful, return from the method. } } } finally { _protocolsHandle.Free(); } } public async Task DoHandshakeAsync(CancellationToken cancellationToken = default(CancellationToken)) { OpenSsl.SSL_set_accept_state(_ssl); var count = 0; try { while ((count = await _innerStream.ReadAsync(_inputBuffer, 0, _inputBuffer.Length, cancellationToken)) > 0) { if (count == 0) { throw new IOException("TLS handshake failed: the inner stream was closed."); } OpenSsl.BIO_write(_inputBio, _inputBuffer, 0, count); var ret = OpenSsl.SSL_do_handshake(_ssl); if (ret != 1) { var error = OpenSsl.SSL_get_error(_ssl, ret); if (error != 2) { throw new IOException($"TLS handshake failed: {nameof(OpenSsl.SSL_do_handshake)} error {error}."); } } await FlushAsync(cancellationToken); if (ret == 1) { return; } } } finally { _protocolsHandle.Free(); } } public string GetNegotiatedApplicationProtocol() { OpenSsl.SSL_get0_alpn_selected(_ssl, out var protocol); return protocol; } private static unsafe int AlpnSelectCallback(IntPtr ssl, out byte* @out, out byte outlen, byte* @in, uint inlen, IntPtr arg) { var protocols = GCHandle.FromIntPtr(arg); var server = (byte[])protocols.Target; fixed (byte* serverPtr = server) { return OpenSsl.SSL_select_next_proto(out @out, out outlen, serverPtr, (uint)server.Length, @in, (uint)inlen) == OpenSsl.OPENSSL_NPN_NEGOTIATED ? OpenSsl.SSL_TLSEXT_ERR_OK : OpenSsl.SSL_TLSEXT_ERR_NOACK; } } private static byte[] ToWireFormat(IEnumerable<string> protocols) { var buffer = new byte[protocols.Count() + protocols.Sum(protocol => protocol.Length)]; var offset = 0; foreach (var protocol in protocols) { buffer[offset++] = (byte)protocol.Length; offset += Encoding.ASCII.GetBytes(protocol, 0, protocol.Length, buffer, offset); } return buffer; } }}
Here are my OpenSSL wrappers for OpenSSL 1.1 (I had to put them elsewhere, because my question body would be to large if they are here: https://hastebin.com/share/weruzapebe.csharp
My OpenSSL Library gets loaded properly
cert.pemkey.pem
are working properly, I tried to use them on apache web server and they work fine there
I would expect firefox to show my decrypted page, but the onlything I get back is
PR_END_OF_FILE_ERROR
If I don't do TLSHandshake I get
PR_CONNECT_RESET_ERROR
I even tried to connect with OpenSSL like this:
openssl s_client -connect domain.local:443
But the onlything I get is this back
Connecting to 192.168.88.12CONNECTED(00000128)BC370000:error:0A000126:SSL routines::unexpected eof while reading:../openssl-3.3.2/ssl/record/rec_layer_s3.c:687:---no peer certificate available---No client certificate CA names sent---SSL handshake has read 0 bytes and written 334 bytesVerification: OK---New, (NONE), Cipher is (NONE)This TLS version forbids renegotiation.Compression: NONEExpansion: NONENo ALPN negotiatedEarly data was not sentVerify return code: 0 (ok)---
what I am doing wrong, I thought adding https to my server would be trivial (just pass your stream to openSSL who would do all the work, and operate on the decrypted stream)
but something is not working