diff options
author | Kenneth Skovhede <kenneth@hexad.dk> | 2017-07-04 00:23:39 +0300 |
---|---|---|
committer | Kenneth Skovhede <kenneth@hexad.dk> | 2017-07-04 00:23:39 +0300 |
commit | 36cccd9776c7a554008a94c01e941c9bcb136185 (patch) | |
tree | 4bc59c9dd8cee153eeda76a8325484d3e51c4542 | |
parent | 9394e7e3b7f6d72019a2fb647f1668505faa5fdf (diff) | |
parent | 396ea160dbb969dc8fbadd666fa45775939eb119 (diff) |
Merge branch 'feature/synology_ngix_proxy'
-rw-r--r-- | Duplicati/Server/Duplicati.Server.csproj | 1 | ||||
-rw-r--r-- | Duplicati/Server/WebServer/Server.cs | 3 | ||||
-rw-r--r-- | Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs | 288 | ||||
-rw-r--r-- | Duplicati/Server/webroot/login/login.js | 4 | ||||
-rw-r--r-- | Duplicati/Server/webroot/ngax/scripts/services/AppService.js | 2 | ||||
-rwxr-xr-x | Installer/Synology/CGIProxyHandler.exe | bin | 20992 -> 0 bytes | |||
-rwxr-xr-x | Installer/Synology/INFO | 3 | ||||
-rw-r--r-- | Installer/Synology/dsm.duplicati.conf | 12 | ||||
-rw-r--r-- | Installer/Synology/make-binary-package.sh | 72 | ||||
-rwxr-xr-x | Installer/Synology/scripts/postinst | 2 | ||||
-rwxr-xr-x | Installer/Synology/scripts/start-stop-status | 28 | ||||
-rw-r--r-- | Installer/Synology/web-extra/nph-proxy.cgi | 37 | ||||
-rw-r--r-- | Installer/Synology/web-extra/package/ngax/package.js | 7 | ||||
-rw-r--r-- | Tools/CGIProxyHandler/CGIProxyHandler.csproj | 33 | ||||
-rw-r--r-- | Tools/CGIProxyHandler/CGIProxyHandler.sln | 17 | ||||
-rw-r--r-- | Tools/CGIProxyHandler/Program.cs | 427 | ||||
-rw-r--r-- | Tools/CGIProxyHandler/Properties/AssemblyInfo.cs | 27 |
17 files changed, 404 insertions, 559 deletions
diff --git a/Duplicati/Server/Duplicati.Server.csproj b/Duplicati/Server/Duplicati.Server.csproj index 55651e573..08f901e9d 100644 --- a/Duplicati/Server/Duplicati.Server.csproj +++ b/Duplicati/Server/Duplicati.Server.csproj @@ -124,6 +124,7 @@ <Compile Include="WebServer\CaptchaUtil.cs" />
<Compile Include="WebServer\RESTMethods\Captcha.cs" />
<Compile Include="WebServer\RESTMethods\CommandLine.cs" />
+ <Compile Include="WebServer\SynologyAuthenticationHandler.cs" />
</ItemGroup>
<ItemGroup>
<None Include="app.config" />
diff --git a/Duplicati/Server/WebServer/Server.cs b/Duplicati/Server/WebServer/Server.cs index 283bc4b22..49cd201fd 100644 --- a/Duplicati/Server/WebServer/Server.cs +++ b/Duplicati/Server/WebServer/Server.cs @@ -212,6 +212,9 @@ namespace Duplicati.Server.WebServer {
HttpServer.HttpServer server = new HttpServer.HttpServer();
+ if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
+ server.Add(new SynologyAuthenticationHandler());
+
server.Add(new AuthenticationHandler());
server.Add(new RESTHandler());
diff --git a/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs b/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs new file mode 100644 index 000000000..f85f3cd75 --- /dev/null +++ b/Duplicati/Server/WebServer/SynologyAuthenticationHandler.cs @@ -0,0 +1,288 @@ +// Copyright (C) 2017, The Duplicati Team
+// http://www.duplicati.com, info@duplicati.com
+//
+// This library is free software; you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as
+// published by the Free Software Foundation; either version 2.1 of the
+// License, or (at your option) any later version.
+//
+// This library is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+using System; +using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using HttpServer.HttpModules;
+
+namespace Duplicati.Server.WebServer +{
+ /// <summary>
+ /// Helper class for enforcing the built-in authentication on Synology DSM
+ /// </summary> + public class SynologyAuthenticationHandler : HttpModule + {
+ /// <summary>
+ /// The path to the login.cgi script
+ /// </summary> + private readonly string LOGIN_CGI = GetEnvArg("SYNO_LOGIN_CGI", "/usr/syno/synoman/webman/login.cgi");
+ /// <summary>
+ /// The path to the authenticate.cgi script
+ /// </summary> + private readonly string AUTH_CGI = GetEnvArg("SYNO_AUTHENTICATE_CGI", "/usr/syno/synoman/webman/modules/authenticate.cgi");
+ /// <summary>
+ /// A flag indicating if only admins are allowed
+ /// </summary> + private readonly bool ADMIN_ONLY = !(GetEnvArg("SYNO_ALL_USERS", "0") == "1");
+ /// <summary>
+ /// A flag indicating if the XSRF token should be fetched automatically
+ /// </summary> + private readonly bool AUTO_XSRF = GetEnvArg("SYNO_AUTO_XSRF", "1") == "1"; +
+ /// <summary>
+ /// A flag indicating that the auth-module is fully disabled
+ /// </summary> + private readonly bool FULLY_DISABLED;
+
+ /// <summary>
+ /// Re-evealuate the logins periodically to ensure it is still valid
+ /// </summary>
+ private readonly TimeSpan CACHE_TIMEOUT = TimeSpan.FromMinutes(3);
+
+ /// <summary>
+ /// A cache of previously authenticated logins
+ /// </summary>
+ private readonly Dictionary<string, DateTime> m_logincache = new Dictionary<string, DateTime>();
+
+ /// <summary>
+ /// The loca guarding the login cache
+ /// </summary>
+ private object m_lock = new object(); +
+ /// <summary>
+ /// Initializes a new instance of the <see cref="T:Duplicati.Server.WebServer.SynologyAuthenticationHandler"/> class.
+ /// </summary> + public SynologyAuthenticationHandler() + {
+ Console.WriteLine("Enabling Synology integrated authentication handler");
+ var disable = false;
+ if (!File.Exists(LOGIN_CGI))
+ {
+ Console.WriteLine("Disabling webserver as the login script is not found: {0}", LOGIN_CGI);
+ disable = true;
+ } + if (!File.Exists(AUTH_CGI)) + { + Console.WriteLine("Disabling webserver as the auth script is not found: {0}", AUTH_CGI); + disable = true; + }
+
+ FULLY_DISABLED = disable; + }
+
+ /// <summary>
+ /// Processes the request
+ /// </summary>
+ /// <returns><c>true</c> if the request is handled <c>false</c> otherwise.</returns>
+ /// <param name="request">The request.</param>
+ /// <param name="response">The response.</param>
+ /// <param name="session">The session.</param>
+ public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
+ {
+ if (FULLY_DISABLED)
+ {
+ response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
+ response.Reason = "The system is incorrectly configured";
+ return true;
+ }
+
+ var limitedAccess =
+ request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.InvariantCultureIgnoreCase)
+ ||
+ request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.InvariantCultureIgnoreCase)
+ || + request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.InvariantCultureIgnoreCase);
+
+ if (!limitedAccess)
+ return false;
+ + var tmpenv = new Dictionary<string, string>();
+
+ tmpenv["REMOTE_ADDR"] = request.RemoteEndPoint.Address.ToString();
+ tmpenv["REMOTE_PORT"] = request.RemoteEndPoint.Port.ToString();
+
+ if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"]))
+ tmpenv["REMOTE_ADDR"] = request.Headers["X-Real-IP"]; + if (!string.IsNullOrWhiteSpace(request.Headers["X-Real-IP"])) + tmpenv["REMOTE_PORT"] = request.Headers["X-Real-Port"];
+ + var loginid = request.Cookies["id"]?.Value;
+ if (!string.IsNullOrWhiteSpace(loginid))
+ tmpenv["HTTP_COOKIE"] = "id=" + loginid;
+
+ var xsrftoken = request.Headers["X-Syno-Token"];
+ if (string.IsNullOrWhiteSpace(xsrftoken))
+ xsrftoken = request.QueryString["SynoToken"]?.Value;
+
+ var cachestring = BuildCacheKey(tmpenv, xsrftoken);
+
+ DateTime cacheExpires;
+ if (m_logincache.TryGetValue(cachestring, out cacheExpires) && cacheExpires > DateTime.Now)
+ {
+ // We do not refresh the cache, as we need to ask the synology auth system periodically
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(xsrftoken) && AUTO_XSRF)
+ { + var authre = new Regex(@"""SynoToken""\s?\:\s?""(?<token>[^""]+)""");
+ try
+ {
+ var resp = ShellExec(LOGIN_CGI, env: tmpenv).Result;
+
+ var m = authre.Match(resp);
+ if (m.Success)
+ xsrftoken = m.Groups["token"].Value;
+ else
+ throw new Exception("Unable to get XSRF token");
+ }
+ catch (Exception ex)
+ {
+ response.Status = System.Net.HttpStatusCode.InternalServerError; + response.Reason = "The system is incorrectly configured"; + return true; + + } + } + + if (!string.IsNullOrWhiteSpace(xsrftoken)) + tmpenv["HTTP_X_SYNO_TOKEN"] = xsrftoken; +
+ cachestring = BuildCacheKey(tmpenv, xsrftoken); +
+ var username = GetEnvArg("SYNO_USERNAME"); + if (string.IsNullOrWhiteSpace(username)) + { + try
+ {
+ username = ShellExec(AUTH_CGI, shell: false, exitcode: 0, env: tmpenv).Result;
+ }
+ catch (Exception ex)
+ { + response.Status = System.Net.HttpStatusCode.InternalServerError; + response.Reason = "The system is incorrectly configured"; + return true; + } + }
+
+ if (string.IsNullOrWhiteSpace(username))
+ { + response.Status = System.Net.HttpStatusCode.Forbidden; + response.Reason = "Permission denied, not logged in"; + return true; + } + + username = username.Trim(); + + if (ADMIN_ONLY) + { + var groups = GetEnvArg("SYNO_GROUP_IDS"); + + if (string.IsNullOrWhiteSpace(groups)) + groups = ShellExec("id", "-G '" + username.Trim().Replace("'", "\\'") + "'", exitcode: 0).Result ?? string.Empty;
+ if (!groups.Split(new char[] { ' ' }).Contains("101"))
+ {
+ response.Status = System.Net.HttpStatusCode.Forbidden; + response.Reason = "Administrator login required"; + return true; + } + }
+
+ // We are now authenticated, add to cache
+ m_logincache[cachestring] = DateTime.Now + CACHE_TIMEOUT; + return false; + }
+
+ /// <summary>
+ /// Builds a cache key from the environment data
+ /// </summary>
+ /// <returns>The cache key.</returns>
+ /// <param name="values">The environment.</param>
+ /// <param name="xsrftoken">The XSRF token.</param>
+ private static string BuildCacheKey(Dictionary<string, string> values, string xsrftoken)
+ {
+ if (!values.ContainsKey("REMOTE_ADDR") || !values.ContainsKey("REMOTE_PORT") || !values.ContainsKey("HTTP_COOKIE"))
+ return null;
+
+ return string.Format("{0}:{1}/{2}?{3}", values["REMOTE_ADDR"], values["REMOTE_PORT"], values["HTTP_COOKIE"], xsrftoken);
+ } + + /// <summary>
+ /// Runs an external command
+ /// </summary>
+ /// <returns>The stdout data.</returns>
+ /// <param name="command">The executable</param>
+ /// <param name="args">The executable and the arguments.</param>
+ /// <param name="shell">If set to <c>true</c> use the shell context for execution.</param>
+ /// <param name="exitcode">Set the value to check for a particular exitcode.</param>
+ private static async Task<string> ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary<string, string> 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 = System.Diagnostics.Process.Start(psi)) + { + if (shell && args != null) + await p.StandardInput.WriteLineAsync(args); + + var res = p.StandardOutput.ReadToEndAsync(); +
+ var tries = 10;
+ var ms = (int)TimeSpan.FromSeconds(0.5).TotalMilliseconds;
+ while (tries > 0 && !p.HasExited)
+ {
+ tries--;
+ p.WaitForExit(ms);
+ } + + if (!p.HasExited)
+ try { p.Kill(); }
+ catch { }
+
+ if (!p.HasExited || (p.ExitCode != exitcode && exitcode != -1)) + throw new Exception(string.Format("Exit code was: {0}, stdout: {1}", p.ExitCode, res)); + return await res; + } + } + + /// <summary>
+ /// Gets the environment variable argument.
+ /// </summary>
+ /// <returns>The environment variable.</returns>
+ /// <param name="key">The name of the environment variable.</param>
+ /// <param name="default">The default value.</param> + private static string GetEnvArg(string key, string @default = null) + { + var res = Environment.GetEnvironmentVariable(key); + return string.IsNullOrWhiteSpace(res) ? @default : res.Trim(); + } + } +} diff --git a/Duplicati/Server/webroot/login/login.js b/Duplicati/Server/webroot/login/login.js index 1f22ce68e..e2c2de461 100644 --- a/Duplicati/Server/webroot/login/login.js +++ b/Duplicati/Server/webroot/login/login.js @@ -10,7 +10,7 @@ $(document).ready(function() { // First we grab the nonce and salt $.ajax({ - url: '/login.cgi', + url: './login.cgi', type: 'POST', dataType: 'json', data: {'get-nonce': 1} @@ -21,7 +21,7 @@ $(document).ready(function() { var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(data.Nonce) + saltedpwd)).toString(CryptoJS.enc.Base64); $.ajax({ - url: '/login.cgi', + url: './login.cgi', type: 'POST', dataType: 'json', data: {'password': noncedpwd } diff --git a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js index 5127094f3..54ffbd112 100644 --- a/Duplicati/Server/webroot/ngax/scripts/services/AppService.js +++ b/Duplicati/Server/webroot/ngax/scripts/services/AppService.js @@ -1,5 +1,5 @@ backupApp.service('AppService', function($http, $cookies, $q, $cookies, DialogService, appConfig) { - this.apiurl = '/api/v1'; + this.apiurl = '../api/v1'; this.proxy_url = null; var self = this; diff --git a/Installer/Synology/CGIProxyHandler.exe b/Installer/Synology/CGIProxyHandler.exe Binary files differdeleted file mode 100755 index 7fba2766a..000000000 --- a/Installer/Synology/CGIProxyHandler.exe +++ /dev/null diff --git a/Installer/Synology/INFO b/Installer/Synology/INFO index 9ed7439ff..1af1ba15a 100755 --- a/Installer/Synology/INFO +++ b/Installer/Synology/INFO @@ -14,4 +14,5 @@ start_dep_services="mono" thirdparty="yes" support_conf_folder="yes" description="Duplicati is a free, open source, backup client that securely stores encrypted, incremental, compressed backups on cloud storage services and remote file servers." -firmware="5.0-4418" +firmware="6.0-7300" +startstop_restart_services="nginx" diff --git a/Installer/Synology/dsm.duplicati.conf b/Installer/Synology/dsm.duplicati.conf new file mode 100644 index 000000000..f6971d22b --- /dev/null +++ b/Installer/Synology/dsm.duplicati.conf @@ -0,0 +1,12 @@ +location ~ ^/webman/3rdparty/Duplicati/((.*)\.cgi|api/(.*))$ { + proxy_set_header X-Server-IP $server_addr; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Real-HTTPS $https; + proxy_set_header X-Server-Port $server_port; + proxy_set_header X-Real-Port $remote_port; + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-Host $http_host; + + proxy_http_version 1.1; + proxy_pass http://127.0.0.1:8200/$1$is_args$args; +}
\ No newline at end of file diff --git a/Installer/Synology/make-binary-package.sh b/Installer/Synology/make-binary-package.sh index 342ce48ae..8d9994a0a 100644 --- a/Installer/Synology/make-binary-package.sh +++ b/Installer/Synology/make-binary-package.sh @@ -10,11 +10,22 @@ DIRNAME=`echo "${FILENAME}" | cut -d "_" -f 1` VERSION=`echo "${DIRNAME}" | cut -d "-" -f 2` DATE_STAMP=`LANG=C date -R` BASE_FILE_NAME="${FILENAME%.*}" +TMPDIRNAME="${BASE_FILE_NAME}-extract" +MONO=/Library/Frameworks/Mono.framework/Commands/mono +GPG_KEYFILE="${HOME}/.config/signkeys/Duplicati/updater-gpgkey.key" + +# Sort on macOS does not have -V / --version-sort +# https://stackoverflow.com/questions/4493205/unix-sort-of-version-numbers +SORT_OPTIONS="-t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n" if [ -d "${DIRNAME}" ]; then rm -rf "${DIRNAME}" fi +if [ -d "${TMPDIRNAME}" ]; then + rm -rf "${TMPDIRNAME}" +fi + if [ -f "package.tgz" ]; then rm -rf "package.tgz" fi @@ -23,7 +34,17 @@ if [ -f "${BASE_FILE_NAME}.spk" ]; then rm -rf "${BASE_FILE_NAME}.spk" fi -unzip -d "${DIRNAME}" "$1" +if [ -f "${BASE_FILE_NAME}.spk.tmp" ]; then + rm -rf "${BASE_FILE_NAME}.spk.tmp" +fi + +if [ -f "${BASE_FILE_NAME}.signature" ]; then + rm -rf "${BASE_FILE_NAME}.spk.signature" +fi + +TIMESERVER="http://timestamp.synology.com/timestamp.php" + +unzip -q -d "${DIRNAME}" "$1" for n in "../oem" "../../oem" "../../../oem" do @@ -60,27 +81,68 @@ rm -rf ./licenses/alphavss rm -rf ./licenses/MonoMac rm -rf ./licenses/gpg -# Install extra control items for Synology +# Install extra items for Synology cp -R ../web-extra/* webroot/ -cp ../CGIProxyHandler.exe . +cp ../dsm.duplicati.conf . DIRSIZE_KB=`BLOCKSIZE=1024 du -s | cut -d '.' -f 1` let "DIRSIZE=DIRSIZE_KB*1024" -tar cvf ../package.tgz ./* +tar cf ../package.tgz ./* cd .. rm -rf "${DIRNAME}" +ICON_72=$(openssl base64 -A -in PACKAGE_ICON.PNG) +ICON_256=$(openssl base64 -A -in PACKAGE_ICON_256.PNG) + git checkout INFO echo "version=\"${VERSION}\"" >> "INFO" MD5=`md5 "package.tgz" | awk -F ' ' '{print $NF}'` echo "checksum=\"${MD5}\"" >> "INFO" echo "extractsize=\"${DIRSIZE}\"" >> "INFO" +echo "package_icon=\"${ICON_72}\"" >> "INFO" +echo "package_icon_256=\"${ICON_256}\"" >> "INFO" chmod +x scripts/* tar cf "${BASE_FILE_NAME}.spk" INFO LICENSE *.PNG package.tgz scripts conf WIZARD_UIFILES + git checkout INFO +rm package.tgz + +if [ -f "${GPG_KEYFILE}" ]; then + if [ "z${KEYFILE_PASSWORD}" == "z" ]; then + echo -n "Enter keyfile password: " + read -s KEYFILE_PASSWORD + echo + fi + + GPGDATA=`"${MONO}" "../../BuildTools/AutoUpdateBuilder/bin/Debug/SharpAESCrypt.exe" d "${KEYFILE_PASSWORD}" "${GPG_KEYFILE}"` + if [ ! $? -eq 0 ]; then + echo "Decrypting GPG keyfile failed" + exit 1 + fi + GPGID=`echo "${GPGDATA}" | head -n 1` + GPGKEY=`echo "${GPGDATA}" | head -n 2 | tail -n 1` +else + echo "No GPG keyfile found, skipping gpg signing" +fi + +if [ "z${GPGID}" != "z" ]; then + # Now codesign the spk file + mkdir "${TMPDIRNAME}" + tar xf "${BASE_FILE_NAME}.spk" -C "${TMPDIRNAME}" + cat $(find ${TMPDIRNAME} -type f | sort ${SORT_OPTIONS}) > "${BASE_FILE_NAME}.spk.tmp" + + gpg2 --ignore-time-conflict --ignore-valid-from --yes --batch --armor --detach-sign --default-key="${GPGID}" --output "${BASE_FILE_NAME}.signature" "${BASE_FILE_NAME}.spk.tmp" + rm "${BASE_FILE_NAME}.spk.tmp" + + curl --silent --form "file=@${BASE_FILE_NAME}.signature" "${TIMESERVER}" > "${TMPDIRNAME}/syno_signature.asc" + rm "${BASE_FILE_NAME}.signature" + + rm "${BASE_FILE_NAME}.spk" + tar cf "${BASE_FILE_NAME}.spk" -C "${TMPDIRNAME}" `ls -1 ${TMPDIRNAME}` -rm package.tgz
\ No newline at end of file + rm -rf "${TMPDIRNAME}" +fi
\ No newline at end of file diff --git a/Installer/Synology/scripts/postinst b/Installer/Synology/scripts/postinst index ef6208fb9..bda2ac6af 100755 --- a/Installer/Synology/scripts/postinst +++ b/Installer/Synology/scripts/postinst @@ -1,4 +1,6 @@ #!/bin/sh +mkdir -p /usr/local/bin + echo "mono ${SYNOPKG_PKGDEST}/Duplicati.CommandLine.exe $@" > "/usr/local/bin/duplicati-cli" chmod +x "/usr/local/bin/duplicati-cli" diff --git a/Installer/Synology/scripts/start-stop-status b/Installer/Synology/scripts/start-stop-status index 929a5aa0a..d23961e96 100755 --- a/Installer/Synology/scripts/start-stop-status +++ b/Installer/Synology/scripts/start-stop-status @@ -6,6 +6,23 @@ PACKAGE_NAME_SIMPLE="$(echo "$SYNOPKG_PKGNAME" | awk '{print tolower($0)}' | sed PACKAGE_DIR="${SYNOPKG_PKGDEST}" PACKAGE_UPGRADE_FLAG="/tmp/${PACKAGE_NAME_SIMPLE}.upgrade" +# We need more space than what /tmp holds +PACKAGE_TEMP_DIR="${PACKAGE_DIR}/temp" + +# These control how the authentication is integrated +SYNO_LOGIN_CGI=/usr/syno/synoman/webman/login.cgi +SYNO_AUTHENTICATE_CGI=/usr/syno/synoman/webman/modules/authenticate.cgi + +# If all users should have access, set to "1" +SYNO_ALL_USERS=0 + +# If we should disable verification of Synology's XSRF tokens, set to "1" +SYNO_SKIP_XSRF=0 + +# If we should disable Synology auth completely, set to "0" +SYNO_DSM_AUTH=1 + + # Start & Stop Varables PID_FILE="/var/run/${PACKAGE_NAME_SIMPLE}.pid" @@ -13,9 +30,14 @@ DaemonStart() { DaemonStatus if [ $? == 0 ]; then echo "Starting ${PACKAGE_NAME_SIMPLE}." - - mono "${PACKAGE_DIR}/Duplicati.Server.exe" & + + mkdir -p "${PACKAGE_TEMP_DIR}" + + TMP_DIR="${PACKAGE_TEMP_DIR}" TEMP="${PACKAGE_TEMP_DIR}" mono "${PACKAGE_DIR}/Duplicati.Server.exe" & echo $! > "$PID_FILE" + + cp -f ${PACKAGE_DIR}/dsm.duplicati.conf /usr/local/etc/nginx/conf.d/ + else echo "${PACKAGE_NAME_SIMPLE} already running." fi @@ -32,6 +54,8 @@ DaemonStop() { kill $(cat "$PID_FILE"); rm -f "$PID_FILE" + rm -f /usr/local/etc/nginx/conf.d/dsm.duplicati.conf + sleep 3 else echo "Nothing to stop for ${PACKAGE_NAME_SIMPLE}." diff --git a/Installer/Synology/web-extra/nph-proxy.cgi b/Installer/Synology/web-extra/nph-proxy.cgi deleted file mode 100644 index f57ca3b8b..000000000 --- a/Installer/Synology/web-extra/nph-proxy.cgi +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/sh - -# Basic configuration for the proxy handler -export SYNO_LOGIN_CGI=/usr/syno/synoman/webman/login.cgi -export SYNO_AUTHENTICATE_CGI=/usr/syno/synoman/webman/modules/authenticate.cgi -export SYNO_ALL_USERS=0 -export SYNO_AUTO_XSRF=1 -export SYNO_SKIP_AUTH=0 -export PROXY_HOST=localhost -export PROXY_PORT=8200 -export PROXY_DEBUG=0 -export PROXY_LOGFILE=/var/log/duplicati-proxy.log - -# It seems it is faster to set this up in the script, -# instead of letting the CGIProxyHandler do it -if [ "z$SYNO_SKIP_AUTH" != "z1" ]; then - - if [ "z$HTTP_X_SYNO_TOKEN" == "z" ]; then - if [ "z$SYNO_AUTO_XSRF" == "z1" ]; then - TOKEN=`$SYNO_LOGIN_CGI < /dev/null | grep SynoToken | cut -d '"' -f 4` - export HTTP_X_SYNO_TOKEN="$TOKEN" - fi - fi - - if [ "z$HTTP_X_SYNO_TOKEN" != "z" ]; then - USERNAME=`QUERY_STRING=SynoToken=$HTTP_X_SYNO_TOKEN $SYNO_AUTHENTICATE_CGI < /dev/null` - export SYNO_USERNAME="$USERNAME" - fi - - if [ "z$USERNAME" != "z" ]; then - GROUP_IDS=`id -G "$USERNAME" < /dev/null` - export SYNO_GROUP_IDS="$GROUP_IDS" - fi -fi - -# This line is injected by the postinst script -#mono /var/packages/Duplicati/target/CGIProxyHandler.exe
\ No newline at end of file diff --git a/Installer/Synology/web-extra/package/ngax/package.js b/Installer/Synology/web-extra/package/ngax/package.js index 72b2fad25..027f067e7 100644 --- a/Installer/Synology/web-extra/package/ngax/package.js +++ b/Installer/Synology/web-extra/package/ngax/package.js @@ -1,6 +1,4 @@ backupApp.service('ProxyService', function(AppService, $http) { - AppService.proxy_url = "../nph-proxy.cgi"; - var origconfig = AppService.proxy_config; var synotoken = null; var is_grabbing = false; @@ -39,14 +37,9 @@ backupApp.service('ProxyService', function(AppService, $http) { AppService.proxy_config = function(method, options, data, targeturl) { grab_syno_token(); - options.headers['X-Proxy-Path'] = targeturl; - if (synotoken != null) options.headers['X-Syno-Token'] = synotoken; - if (options.timeout == null || options.timeout < 30000) - options.timeout = 30000; - if (origconfig != null) origconfig(method, options, data, targeturl); }; diff --git a/Tools/CGIProxyHandler/CGIProxyHandler.csproj b/Tools/CGIProxyHandler/CGIProxyHandler.csproj deleted file mode 100644 index 1939f37ec..000000000 --- a/Tools/CGIProxyHandler/CGIProxyHandler.csproj +++ /dev/null @@ -1,33 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> - <PropertyGroup> - <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> - <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> - <ProjectGuid>{EA4505A7-3BDE-4057-BD90-94808D649570}</ProjectGuid> - <OutputType>Exe</OutputType> - <Prefer32Bit>False</Prefer32Bit> - <RootNamespace>CGIProxyHandler</RootNamespace> - <AssemblyName>CGIProxyHandler</AssemblyName> - <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> - <Optimize>false</Optimize> - <OutputPath>bin\Debug</OutputPath> - <WarningLevel>4</WarningLevel> - <DebugSymbols>true</DebugSymbols> - <DefineConstants>DEBUG</DefineConstants> - </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> - <Optimize>true</Optimize> - <OutputPath>bin\Release</OutputPath> - <WarningLevel>4</WarningLevel> - </PropertyGroup> - <ItemGroup> - <Reference Include="System" /> - </ItemGroup> - <ItemGroup> - <Compile Include="Program.cs" /> - <Compile Include="Properties\AssemblyInfo.cs" /> - </ItemGroup> - <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" /> -</Project>
\ No newline at end of file diff --git a/Tools/CGIProxyHandler/CGIProxyHandler.sln b/Tools/CGIProxyHandler/CGIProxyHandler.sln deleted file mode 100644 index dec62370f..000000000 --- a/Tools/CGIProxyHandler/CGIProxyHandler.sln +++ /dev/null @@ -1,17 +0,0 @@ -
-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 deleted file mode 100644 index dc423699b..000000000 --- a/Tools/CGIProxyHandler/Program.cs +++ /dev/null @@ -1,427 +0,0 @@ -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); - } - - } - - /// <summary> - /// Runs an external command - /// </summary> - /// <returns>The stdout data.</returns> - /// <param name="command">The executable</param> - /// <param name="args">The executable and the arguments.</param> - /// <param name="shell">If set to <c>true</c> use the shell context for execution.</param> - /// <param name="exitcode">Set the value to check for a particular exitcode.</param> - private static async Task<string> ShellExec(string command, string args = null, bool shell = false, int exitcode = -1, Dictionary<string, string> 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=(<?token>[^&+])"); - 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?""(?<token>[^""]+)"""); - 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<string, string>(); - 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=(<?url>[^&+])"); - 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<string>().Where<string>(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 deleted file mode 100644 index 4cb3fd3f2..000000000 --- a/Tools/CGIProxyHandler/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,27 +0,0 @@ -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("2.0.0.7")] - -// 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("")] - |