using System; using System.Net; using System.Net.Sockets; using System.Net.Security; using System.Security.Authentication; using System.Security.Cryptography.X509Certificates; using System.Threading; namespace OSHttpServer { public class OSHttpListener: IDisposable { private readonly IPAddress m_address; private readonly X509Certificate m_certificate; private readonly IHttpContextFactory m_contextFactory; private readonly int m_port; private readonly ManualResetEvent m_shutdownEvent = new ManualResetEvent(false); private readonly SslProtocols m_sslProtocol = SslProtocols.Tls | SslProtocols.Ssl3 | SslProtocols.Ssl2; private TcpListener m_listener; private ILogWriter m_logWriter = NullLogWriter.Instance; private int m_pendingAccepts; private bool m_shutdown; protected RemoteCertificateValidationCallback m_clientCertValCallback = null; public event EventHandler Accepted; public event ExceptionHandler ExceptionThrown; public event EventHandler RequestReceived; /// /// Listen for regular HTTP connections /// /// IP Address to accept connections on /// TCP Port to listen on, default HTTP port is 80. /// Factory used to create es. /// address is null. /// Port must be a positive number. protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory) { m_address = address; m_port = port; m_contextFactory = factory; m_contextFactory.RequestReceived += OnRequestReceived; } /// /// Initializes a new instance of the class. /// /// IP Address to accept connections on /// TCP Port to listen on, default HTTPS port is 443 /// Factory used to create es. /// Certificate to use /// which HTTPS protocol to use, default is TLS. protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory, X509Certificate certificate, SslProtocols protocol) : this(address, port, factory, certificate) { m_sslProtocol = protocol; } /// /// Initializes a new instance of the class. /// /// IP Address to accept connections on /// TCP Port to listen on, default HTTPS port is 443 /// Factory used to create es. /// Certificate to use protected OSHttpListener(IPAddress address, int port, IHttpContextFactory factory, X509Certificate certificate) : this(address, port, factory) { m_certificate = certificate; } public static OSHttpListener Create(IPAddress address, int port) { RequestParserFactory requestFactory = new RequestParserFactory(); HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory); return new OSHttpListener(address, port, factory); } public static OSHttpListener Create(IPAddress address, int port, X509Certificate certificate) { RequestParserFactory requestFactory = new RequestParserFactory(); HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory); return new OSHttpListener(address, port, factory, certificate); } public static OSHttpListener Create(IPAddress address, int port, X509Certificate certificate, SslProtocols protocol) { RequestParserFactory requestFactory = new RequestParserFactory(); HttpContextFactory factory = new HttpContextFactory(NullLogWriter.Instance, requestFactory); return new OSHttpListener(address, port, factory, certificate, protocol); } private void OnRequestReceived(object sender, RequestEventArgs e) { RequestReceived?.Invoke(sender, e); } public RemoteCertificateValidationCallback CertificateValidationCallback { set { m_clientCertValCallback = value; } } /// /// Gives you a change to receive log entries for all internals of the HTTP library. /// /// /// You may not switch log writer after starting the listener. /// public ILogWriter LogWriter { get { return m_logWriter; } set { m_logWriter = value ?? NullLogWriter.Instance; if (m_certificate != null) m_logWriter.Write(this, LogPrio.Info, "HTTPS(" + m_sslProtocol + ") listening on " + m_address + ":" + m_port); else m_logWriter.Write(this, LogPrio.Info, "HTTP listening on " + m_address + ":" + m_port); } } /// /// True if we should turn on trace logs. /// public bool UseTraceLogs { get; set; } /// Exception. private void OnAccept(IAsyncResult ar) { bool beginAcceptCalled = false; try { int count = Interlocked.Decrement(ref m_pendingAccepts); if (m_shutdown) { if (count == 0) m_shutdownEvent.Set(); return; } Interlocked.Increment(ref m_pendingAccepts); m_listener.BeginAcceptSocket(OnAccept, null); beginAcceptCalled = true; Socket socket = m_listener.EndAcceptSocket(ar); if (!OnAcceptingSocket(socket)) { socket.Disconnect(true); return; } m_logWriter.Write(this, LogPrio.Debug, "Accepted connection from: " + socket.RemoteEndPoint); if (m_certificate != null) m_contextFactory.CreateSecureContext(socket, m_certificate, m_sslProtocol, m_clientCertValCallback); else m_contextFactory.CreateContext(socket); } catch (Exception err) { m_logWriter.Write(this, LogPrio.Debug, err.Message); ExceptionThrown?.Invoke(this, err); if (!beginAcceptCalled) RetryBeginAccept(); } } /// /// Will try to accept connections one more time. /// /// If any exceptions is thrown. private void RetryBeginAccept() { try { m_logWriter.Write(this, LogPrio.Error, "Trying to accept connections again."); m_listener.BeginAcceptSocket(OnAccept, null); } catch (Exception err) { m_logWriter.Write(this, LogPrio.Fatal, err.Message); ExceptionThrown?.Invoke(this, err); } } /// /// Can be used to create filtering of new connections. /// /// Accepted socket /// true if connection can be accepted; otherwise false. protected bool OnAcceptingSocket(Socket socket) { ClientAcceptedEventArgs args = new ClientAcceptedEventArgs(socket); Accepted?.Invoke(this, args); return !args.Revoked; } /// /// Start listen for new connections /// /// Number of connections that can stand in a queue to be accepted. /// Listener have already been started. public void Start(int backlog) { if (m_listener != null) throw new InvalidOperationException("Listener have already been started."); m_listener = new TcpListener(m_address, m_port); m_listener.Start(backlog); Interlocked.Increment(ref m_pendingAccepts); m_listener.BeginAcceptSocket(OnAccept, null); } /// /// Stop the listener /// /// public void Stop() { m_shutdown = true; m_contextFactory.Shutdown(); m_listener.Stop(); if (!m_shutdownEvent.WaitOne()) m_logWriter.Write(this, LogPrio.Error, "Failed to shutdown listener properly."); m_listener = null; Dispose(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Dispose(bool disposing) { if (m_shutdownEvent != null) { m_shutdownEvent.Dispose(); } } } }