From 5a674e8d8da705aa21e8a3c26447f26b8092b08d Mon Sep 17 00:00:00 2001 From: Kenneth Skovhede Date: Fri, 13 May 2016 00:06:12 +0200 Subject: [PATCH] Added source for the CGIProxyHandler --- Tools/CGIProxyHandler/CGIProxyHandler.csproj | 32 ++ Tools/CGIProxyHandler/CGIProxyHandler.sln | 17 + Tools/CGIProxyHandler/Program.cs | 427 ++++++++++++++++++ .../Properties/AssemblyInfo.cs | 27 ++ 4 files changed, 503 insertions(+) create mode 100644 Tools/CGIProxyHandler/CGIProxyHandler.csproj create mode 100644 Tools/CGIProxyHandler/CGIProxyHandler.sln create mode 100644 Tools/CGIProxyHandler/Program.cs create mode 100644 Tools/CGIProxyHandler/Properties/AssemblyInfo.cs diff --git a/Tools/CGIProxyHandler/CGIProxyHandler.csproj b/Tools/CGIProxyHandler/CGIProxyHandler.csproj new file mode 100644 index 000000000..40d9ab562 --- /dev/null +++ b/Tools/CGIProxyHandler/CGIProxyHandler.csproj @@ -0,0 +1,32 @@ + + + + Debug + AnyCPU + {EA4505A7-3BDE-4057-BD90-94808D649570} + Exe + CGIProxyHandler + CGIProxyHandler + v4.5 + + + false + bin\Debug + 4 + true + DEBUG + + + true + bin\Release + 4 + + + + + + + + + + \ No newline at end of file diff --git a/Tools/CGIProxyHandler/CGIProxyHandler.sln b/Tools/CGIProxyHandler/CGIProxyHandler.sln new file mode 100644 index 000000000..dec62370f --- /dev/null +++ b/Tools/CGIProxyHandler/CGIProxyHandler.sln @@ -0,0 +1,17 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CGIProxyHandler", "CGIProxyHandler.csproj", "{EA4505A7-3BDE-4057-BD90-94808D649570}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EA4505A7-3BDE-4057-BD90-94808D649570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA4505A7-3BDE-4057-BD90-94808D649570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA4505A7-3BDE-4057-BD90-94808D649570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA4505A7-3BDE-4057-BD90-94808D649570}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tools/CGIProxyHandler/Program.cs b/Tools/CGIProxyHandler/Program.cs new file mode 100644 index 000000000..dc423699b --- /dev/null +++ b/Tools/CGIProxyHandler/Program.cs @@ -0,0 +1,427 @@ +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Text.RegularExpressions; +using System.Linq; +using System.IO; +using System.Collections.Generic; +using System.Threading; +using System.Runtime.InteropServices; + +namespace CGIProxyHandler +{ + class MainClass + { + private class LogHandler + { + private object m_lock = new object(); + private StreamWriter m_stream; + private bool m_debug; + + public LogHandler(StreamWriter writer, bool debug) + { + m_stream = writer; + m_debug = debug; + } + + public void WriteMessage(string msg) + { + if (m_stream != null) + lock(m_lock) + m_stream.WriteLine(msg); + } + + public void WriteDebugMessage(string msg) + { + if (m_stream != null && m_debug) + lock(m_lock) + m_stream.WriteLine(msg); + } + + } + + /// + /// Runs an external command + /// + /// The stdout data. + /// The executable + /// The executable and the arguments. + /// If set to true use the shell context for execution. + /// Set the value to check for a particular exitcode. + private static async Task ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary env = null) + { + var psi = new ProcessStartInfo() { + FileName = command, + Arguments = shell ? null : args, + UseShellExecute = false, + RedirectStandardInput = shell, + RedirectStandardOutput = true + }; + + if (env != null) + foreach (var pk in env) + psi.EnvironmentVariables[pk.Key] = pk.Value; + + using (var p = Process.Start(psi)) + { + if (shell && args != null) + await p.StandardInput.WriteLineAsync(args); + + var res = await p.StandardOutput.ReadToEndAsync(); + + p.WaitForExit((int)TimeSpan.FromSeconds(5).TotalMilliseconds); + + if (p.ExitCode != exitcode && exitcode != -1) + throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res)); + return res; + } + } + + private static string GetEnvArg(string key, string @default = null) + { + var res = Environment.GetEnvironmentVariable(key); + return string.IsNullOrWhiteSpace(res) ? @default : res.Trim(); + } + + private const string CRLF = "\r\n"; + private static readonly byte[] ERROR_MESSAGE = System.Text.Encoding.ASCII.GetBytes("Status: 500 Server error" + CRLF + CRLF); + + public static void Main(string[] args) + { + var debug = GetEnvArg("PROXY_DEBUG", "0") == "1"; + var logfile = GetEnvArg("PROXY_LOGFILE", "/var/log/duplicat-proxy.log"); + + using (var logout = new StreamWriter(File.Open(logfile, System.IO.FileMode.Append, FileAccess.Write, FileShare.ReadWrite))) + using(var stdout = Console.OpenStandardOutput()) + { + if (logout != null) + logout.AutoFlush = true; + + + var logger = new LogHandler(logout, debug); + + try + { + //stdout.WriteTimeout = (int)ACTIVITY_TIMEOUT.TotalMilliseconds; + logger.WriteDebugMessage("Started!"); + logger.WriteDebugMessage(string.Format("Processing request for target url: {0}", GetEnvArg("HTTP_X_PROXY_PATH"))); + + logger.WriteDebugMessage(string.Format("Redirects: {0},{1},{2}", Console.IsInputRedirected, Console.IsOutputRedirected, Console.IsErrorRedirected)); + + if (args == null) + args = new string[0]; + + foreach(var a in args) + logger.WriteDebugMessage(string.Format("arg: {0}", a)); + + Run(args, logger, stdout).Wait(); + } + catch (Exception ex) + { + var rex = ex; + if (rex is AggregateException && (rex as AggregateException).Flatten().InnerExceptions.Count == 1) + rex = (rex as AggregateException).Flatten().InnerExceptions.First(); + + if (debug) + logger.WriteMessage(string.Format("Failed: {0}", rex)); + else + logger.WriteMessage(string.Format("Failed: {0}", rex.Message)); + + try + { + stdout.Write(ERROR_MESSAGE, 0, ERROR_MESSAGE.Length); + } + catch (Exception ex2) + { + logger.WriteDebugMessage(string.Format("Failed to set error status: {0}", ex2)); + } + } + } + } + + + private static async Task Run(string[] args, LogHandler logger, Stream stdout) + { + var login_cgi = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi"); + var auth_cgi = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi"); + var admin_only = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1"); + var auto_xsrf = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1"; + var skip_auth = GetEnvArg("SYNO_SKIP_AUTH", "0") == "1"; + var query_string = GetEnvArg("QUERY_STRING", ""); + var proxy_host = GetEnvArg("PROXY_HOST", "localhost"); + var proxy_port = GetEnvArg("PROXY_PORT", "8200"); + + var xsrftoken = GetEnvArg("HTTP_X_SYNO_TOKEN"); + if (string.IsNullOrWhiteSpace(xsrftoken) && !string.IsNullOrWhiteSpace(query_string)) + { + // Avoid loading a library just for parsing the token + var tkre = new Regex(@"SynoToken=([^&+])"); + var m = tkre.Match(query_string); + if (m.Success) + xsrftoken = m.Groups["token"].Value; + } + + if (!skip_auth) + { + if (string.IsNullOrWhiteSpace(xsrftoken) && auto_xsrf) + { + var authre = new Regex(@"""SynoToken""\s?\:\s?""(?[^""]+)"""); + var resp = await ShellExec(login_cgi); + + logger.WriteDebugMessage(string.Format("xsrf response is: {0}", resp)); + + var m = authre.Match(resp); + if (m.Success) + xsrftoken = m.Groups["token"].Value; + else + throw new Exception("Unable to get XSRF token"); + } + + var tmpenv = new Dictionary(); + tmpenv["QUERY_STRING"] = "SynoToken=" + xsrftoken; + + var username = GetEnvArg("SYNO_USERNAME"); + + if (string.IsNullOrWhiteSpace(username)) + { + username = await ShellExec(auth_cgi, shell: false, exitcode: 0, env: tmpenv); + logger.WriteDebugMessage(string.Format("Username: {0}", username)); + } + + if (string.IsNullOrWhiteSpace(username)) + throw new Exception("Not logged in"); + + username = username.Trim(); + + if (admin_only) + { + var groups = GetEnvArg("SYNO_GROUP_IDS"); + + if (string.IsNullOrWhiteSpace(groups)) + groups = await ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0) ?? ""; + if (!groups.Split(new char[] { ' ' }).Contains("101")) + throw new Exception(string.Format("User {0} is not an admin", username)); + + logger.WriteDebugMessage("User is admin"); + } + } + + var path = GetEnvArg("HTTP_X_PROXY_PATH"); + if (string.IsNullOrWhiteSpace(path)) + { + var xpre = new Regex(@"x-proxy-path=([^&+])"); + var m = xpre.Match(query_string); + if (m.Success) + path = Uri.UnescapeDataString(m.Groups["url"].Value); + } + + logger.WriteDebugMessage(string.Format("Path is {0} and query string is {1}", path, query_string)); + + if (string.IsNullOrWhiteSpace(path) || !path.StartsWith("/")) + throw new Exception("Invalid path requested"); + + if (!string.IsNullOrWhiteSpace(query_string)) + path += (query_string.StartsWith("?") ? "" : "?") + Uri.EscapeUriString(query_string); + + int port; + if (!int.TryParse(proxy_port, out port)) + port = 8200; + + logger.WriteDebugMessage(string.Format("About to connect to {0}:{1}", proxy_host, port)); + + using (var client = new System.Net.Sockets.TcpClient()) + { + logger.WriteDebugMessage(string.Format("Connecting to {0}:{1}", proxy_host, port)); + client.Connect(proxy_host, port); + logger.WriteDebugMessage("Connected"); + + using (var ns = client.GetStream()) + { + logger.WriteDebugMessage("Opened TCP stream"); + + using (var sw = new StreamWriter(ns)) + { + logger.WriteDebugMessage("Created StreamWriter"); + + //await ForwardRequest(sw, path, logout); + //await ForwardResponse(ns, stdout, logout); + + await Task.WhenAll( + ForwardRequest(sw, path, logger), + ForwardResponse(ns, stdout, logger) + ); + + logger.WriteDebugMessage("Done processing"); + } + } + } + } + + private static readonly byte[] STATUS_PREFIX = System.Text.Encoding.ASCII.GetBytes("Status: "); + private static readonly int HTTP_HEAD_LEN = "HTTP/1.1 ".Length; + + + private static async Task ForwardResponse(Stream source, Stream target, LogHandler logger) + { + var buf = new byte[8 * 1024]; + int r = 0; + int offset = 0; + var lastmatch = 0; + var status = false; + long contentlength = -1; + var canceltoken = new CancellationTokenSource(); + + logger.WriteDebugMessage("Forward response"); + + while ((r = await source.ReadAsync(buf, offset, buf.Length - offset, canceltoken.Token)) != 0) + { + logger.WriteDebugMessage(string.Format("Read {0} bytes", r)); + + offset += r; + var ix = Array.IndexOf(buf, (byte)13, 0, offset); + + while (ix >= 0 && ix < offset - 1) + { + if (buf[ix + 1] == 10) + { + if (!status) + { + status = true; + logger.WriteDebugMessage("Writing: Status: " + System.Text.Encoding.ASCII.GetString(buf, lastmatch + HTTP_HEAD_LEN, ix - lastmatch - HTTP_HEAD_LEN)); + + await target.WriteAsync(STATUS_PREFIX, 0, STATUS_PREFIX.Length, canceltoken.Token); + await target.WriteAsync(buf, lastmatch + HTTP_HEAD_LEN, (ix - lastmatch - HTTP_HEAD_LEN) + 2, canceltoken.Token); + + logger.WriteDebugMessage("Wrote status line"); + } + else + { + // Blank line and we are done + if (ix - lastmatch == 0) + { + logger.WriteDebugMessage(string.Format("Completed header, writing remaining {0} bytes", offset - lastmatch)); + + await target.WriteAsync(buf, lastmatch, offset - lastmatch, canceltoken.Token); + + // Adjust remaining data length + if (contentlength > 0) + contentlength -= offset - lastmatch - 2; + + + logger.WriteDebugMessage(string.Format("Body has remaining {0} bytes", contentlength)); + + while(contentlength > 0) + { + r = await source.ReadAsync(buf, 0, (int)Math.Min(buf.Length, contentlength), canceltoken.Token); + if (r == 0) + break; + + contentlength -= r; + + + await target.WriteAsync(buf, 0, r, canceltoken.Token); + + logger.WriteDebugMessage(string.Format("Body has remaining {0} bytes", contentlength)); + } + + await target.FlushAsync(canceltoken.Token); + + //await logout.WriteDebugMessageAsync(string.Format("Last body chunck: {0}", System.Text.Encoding.ASCII.GetString(buf, 0, r))); + logger.WriteDebugMessage(string.Format("Completed response forward")); + + target.Close(); + + return; + } + else + { + + var header = System.Text.Encoding.ASCII.GetString(buf, lastmatch, ix - lastmatch) ?? string.Empty; + if (header.StartsWith("Content-Length: ", StringComparison.OrdinalIgnoreCase)) + if (!long.TryParse(header.Substring("Content-Length: ".Length), out contentlength)) + contentlength = -1; + + logger.WriteDebugMessage("Writing: " + header); + + await target.WriteAsync(buf, lastmatch, (ix - lastmatch) + 2, canceltoken.Token); + } + + } + + lastmatch = ix + 2; + } + + //await logger.WriteDebugMessageAsync(string.Format("Buf stats: {0},{1},{2},{3}", buf.Length, ix, offset, lastmatch)); + + ix = Array.IndexOf(buf, (byte)13, ix + 1, offset - ix - 1); + } + } + + } + + private static async Task ForwardRequest(StreamWriter sw, string path, LogHandler logger) + { + var canceltoken = new CancellationTokenSource(); + var env = Environment.GetEnvironmentVariables(); + + /*foreach (var k in env.Keys) + logger.WriteDebugMessage(string.Format("{0}: {1}", k, env[k]));*/ + + await sw.WriteAsync(string.Format("{0} {1} HTTP/1.1{2}", GetEnvArg("REQUEST_METHOD", "").Trim(), path, CRLF)); + + logger.WriteDebugMessage("Wrote request header line"); + + foreach (var key in env.Keys.Cast().Where(x => x.StartsWith("HTTP_"))) + await sw.WriteAsync(string.Format("{0}: {1}{2}", key.Substring("HTTP_".Length).Replace("_", "-"), env[key], CRLF)); + + if (!string.IsNullOrWhiteSpace(GetEnvArg("CONTENT_TYPE"))) + await sw.WriteAsync(string.Format("{0}: {1}{2}", "Content-Type", GetEnvArg("CONTENT_TYPE"), CRLF)); + if (!string.IsNullOrWhiteSpace(GetEnvArg("CONTENT_LENGTH"))) + await sw.WriteAsync(string.Format("{0}: {1}{2}", "Content-Length", GetEnvArg("CONTENT_LENGTH"), CRLF)); + + await sw.WriteAsync(string.Format("{0}: {1}{2}", "Connection", "close", CRLF)); + + await sw.WriteAsync(CRLF); + await sw.FlushAsync(); + + logger.WriteDebugMessage("Wrote all header lines"); + + if (new string[] { "POST", "PUT", "PATCH" }.Contains(GetEnvArg("REQUEST_METHOD", "").Trim().ToUpper())) + { + logger.WriteDebugMessage(string.Format("Copying StdIn")); + + using(var stdin = Console.OpenStandardInput()) + { + logger.WriteDebugMessage("Opened StdIn"); + + long reqsize; + if (!long.TryParse(GetEnvArg("CONTENT_LENGTH"), out reqsize)) + reqsize = long.MaxValue; + + var buf = new byte[4 * 1024 * 1024]; + var r = 0; + while(reqsize > 0) + { + logger.WriteDebugMessage(string.Format("Remaining {0} bytes from stdin", reqsize)); + + r = await stdin.ReadAsync(buf, 0, buf.Length, canceltoken.Token); + logger.WriteDebugMessage(string.Format("Got {0} bytes from stdin", r)); + + if (r == 0) + break; + + reqsize -= r; + await sw.BaseStream.WriteAsync(buf, 0, r, canceltoken.Token); + } + } + + logger.WriteDebugMessage("Copy stdin done"); + + } + + logger.WriteDebugMessage("Completed writing request"); + + } + + } +} diff --git a/Tools/CGIProxyHandler/Properties/AssemblyInfo.cs b/Tools/CGIProxyHandler/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..29d0b7e2a --- /dev/null +++ b/Tools/CGIProxyHandler/Properties/AssemblyInfo.cs @@ -0,0 +1,27 @@ +using System.Reflection; +using System.Runtime.CompilerServices; + +// Information about this assembly is defined by the following attributes. +// Change them to the values specific to your project. + +[assembly: AssemblyTitle("CGIProxyHandler")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("")] +[assembly: AssemblyCopyright("kenneth")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". +// The form "{Major}.{Minor}.*" will automatically update the build and revision, +// and "{Major}.{Minor}.{Build}.*" will update just the revision. + +[assembly: AssemblyVersion("1.0.*")] + +// The following attributes are used to specify the signing key for the assembly, +// if desired. See the Mono documentation for more information about signing. + +//[assembly: AssemblyDelaySign(false)] +//[assembly: AssemblyKeyFile("")] +