// -------------------------------------------------------------------------------------------------------------------- // // This file is part of the HandBrake source code - It may be used under the terms of the GNU General Public License. // // // This is a service worker for the HandBrake app. It allows us to run encodes / scans in a separate process easily. // All API's expose the ApplicationServices models as JSON. // // -------------------------------------------------------------------------------------------------------------------- namespace HandBrake.Worker { using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Text; using HandBrake.Worker.Logging; using HandBrake.Worker.Services.Interfaces; public class HttpServer { private readonly int port; private ITokenService tokenService; private HttpListener httpListener; private Dictionary> apiHandlers; private int failedStart = 0; private int count = 0; public HttpServer(Dictionary> apiCalls, int port, ITokenService tokenService) { this.port = port; try { Init(apiCalls, port, tokenService); } catch (Exception e) { ConsoleOutput.WriteLine("HandBrake Worker: Failed to Initialise: " + Environment.NewLine + e, ConsoleColor.Red); } } public int Run() { if (this.failedStart > 0) { return failedStart; } httpListener.BeginGetContext(new AsyncCallback(ListenerCallback), this.httpListener); return 0; } public void ListenerCallback(IAsyncResult result) { if (this.failedStart > 0) { return; } try { if (this.httpListener.IsListening) { var context = this.httpListener.EndGetContext(result); lock (this.httpListener) { this.HandleRequest(context); } } else { // This shouldn't happen, but if it does, try re-initialising the server. StartServer(this.apiHandlers, port, this.tokenService); } } catch (Exception e) { if (e is HttpListenerException) { Console.WriteLine("Worker: " + e); return; } } this.Run(); } public void Stop() { try { if (this.httpListener != null) { this.httpListener.Stop(); this.httpListener.Close(); } } catch (Exception e) { ConsoleOutput.WriteLine("Worker: Not able to close HttpListener: " + e); } finally { this.httpListener = null; } } private void Init(Dictionary> apiCalls, int port, ITokenService tokenService) { if (!HttpListener.IsSupported) { string rstr = string.Format( "Worker: Failed to Initialise: HttpListener is not supported on this computer.{0}" + "\r\nProcess Isolation can be disabled in \"Tools Menu -> Preferences -> Advanced -> Process Isolation\".{0}", Environment.NewLine); ConsoleOutput.WriteLine(rstr, ConsoleColor.Red); throw new NotSupportedException("HttpListener not supported on this computer."); } this.tokenService = tokenService; // Store the Handlers this.apiHandlers = new Dictionary>(apiCalls); if (!tokenService.IsTokenSet()) { ConsoleOutput.WriteLine("HandBrake Worker: Token has not been initialised. API Access is limited!", ConsoleColor.Red); Console.WriteLine(Environment.NewLine); ConsoleOutput.WriteLine("API Information: ", ConsoleColor.Cyan); Console.WriteLine("All calls require a 'token' in the HTTP header "); } StartServer(apiCalls, port, tokenService); } private void StartServer(Dictionary> apiCalls, int port, ITokenService tokenService) { this.count += 1; ConsoleOutput.WriteLine("Worker: Starting Listener: " + count, ConsoleColor.White, true); this.Stop(); // In the case that we are re-initialising. this.httpListener = new HttpListener(); // Base URL string url = string.Format("http://127.0.0.1:{0}/", port); this.httpListener.Prefixes.Add(url); // API URLS foreach (KeyValuePair> api in apiCalls) { url = string.Format("http://127.0.0.1:{0}/{1}/", port, api.Key); this.httpListener.Prefixes.Add(url); if (!tokenService.IsTokenSet()) { Console.WriteLine(" - " + url); } } if (!tokenService.IsTokenSet()) { Console.WriteLine(); } try { this.httpListener.Start(); } catch (Exception e) { this.failedStart = 2; Console.WriteLine("Worker: Unable to start HTTP Server. Maybe the port {0} is in use?", port); Console.WriteLine("Worker Exception: " + e); } } private void HandleRequest(HttpListenerContext context) { try { if (context == null) { return; } Stopwatch watch = Stopwatch.StartNew(); string path = context.Request.RawUrl.TrimStart('/').TrimEnd('/'); string token = context.Request.Headers.Get("token"); if (path == string.Empty) { string rstr = string.Format("HandBrake Worker {0}{0}" + "This worker runs on localhost loopback only and is not accessible to the wider network.{0}" + "\r\nThis feature allows processing of HandBrake jobs on a background process.{0}" + "\r\nThis can be enabled and disabled in \"Tools Menu -> Preferences -> Advanced -> Process Isolation\".{0}", Environment.NewLine); byte[] buf = Encoding.UTF8.GetBytes(rstr); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; context.Response.ContentLength64 = buf.Length; context.Response.OutputStream.Write(buf, 0, buf.Length); watch.Stop(); Debug.WriteLine(string.Format(" - Processed call to: '/{0}', Took {1}ms", path, watch.ElapsedMilliseconds), ConsoleColor.White, true); return; } if (!path.Equals("Version") && !tokenService.IsAuthenticated(token)) { string rstr = string.Format("HandBrake Worker: Access Denied to '/{0}'. The token provided in the HTTP header was not valid.", path); ConsoleOutput.WriteLine(rstr, ConsoleColor.Red, true); byte[] buf = Encoding.UTF8.GetBytes(rstr); context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; context.Response.ContentLength64 = buf.Length; context.Response.OutputStream.Write(buf, 0, buf.Length); watch.Stop(); Debug.WriteLine(string.Format(" - Processed call to: '/{0}', Took {1}ms", path, watch.ElapsedMilliseconds), ConsoleColor.White, true); return; } if (this.apiHandlers.TryGetValue(path, out var actionToPerform)) { string rstr = actionToPerform(context.Request); if (!string.IsNullOrEmpty(rstr)) { byte[] buf = Encoding.UTF8.GetBytes(rstr); context.Response.ContentLength64 = buf.Length; context.Response.OutputStream.Write(buf, 0, buf.Length); } } else { string rstr = "HandBrake Worker: The API endpoint is unknown."; byte[] buf = Encoding.UTF8.GetBytes(rstr); context.Response.ContentLength64 = buf.Length; context.Response.OutputStream.Write(buf, 0, buf.Length); } watch.Stop(); Debug.WriteLine(string.Format(" - Processed call to: '/{0}', Took {1}ms", path, watch.ElapsedMilliseconds), ConsoleColor.White, true); } catch (Exception exc) { ConsoleOutput.WriteLine("Worker: Listener Thread: " + exc); } finally { context?.Response.OutputStream.Close(); } } } }