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("")]
+