First pass at introducing kestrel and refactoring to make the RESTAPI be its own package.
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
// Copyright (C) 2015, 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.Concurrent;
|
||||
using System.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Collections.Generic;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
internal class AuthenticationHandler : HttpModule
|
||||
{
|
||||
private const string AUTH_COOKIE_NAME = "session-auth";
|
||||
private const string NONCE_COOKIE_NAME = "session-nonce";
|
||||
|
||||
private const string XSRF_COOKIE_NAME = "xsrf-token";
|
||||
private const string XSRF_HEADER_NAME = "X-XSRF-Token";
|
||||
|
||||
private const string TRAYICONPASSWORDSOURCE_HEADER = "X-TrayIcon-PasswordSource";
|
||||
|
||||
public const string LOGIN_SCRIPT_URI = "/login.cgi";
|
||||
public const string LOGOUT_SCRIPT_URI = "/logout.cgi";
|
||||
public const string CAPTCHA_IMAGE_URI = RESTHandler.API_URI_PATH + "/captcha/";
|
||||
|
||||
private const int XSRF_TIMEOUT_MINUTES = 10;
|
||||
private const int AUTH_TIMEOUT_MINUTES = 10;
|
||||
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activeTokens = new ConcurrentDictionary<string, DateTime>();
|
||||
private readonly ConcurrentDictionary<string, Tuple<DateTime, string>> m_activeNonces = new ConcurrentDictionary<string, Tuple<DateTime, string>>();
|
||||
private readonly ConcurrentDictionary<string, DateTime> m_activexsrf = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
readonly System.Security.Cryptography.RandomNumberGenerator m_prng = System.Security.Cryptography.RNGCryptoServiceProvider.Create();
|
||||
|
||||
private string FindXSRFToken(HttpServer.IHttpRequest request)
|
||||
{
|
||||
string xsrftoken = request.Headers[XSRF_HEADER_NAME] ?? "";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.Form[XSRF_HEADER_NAME] ?? request.Form[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(xsrftoken))
|
||||
{
|
||||
var xsrfq = request.QueryString[XSRF_HEADER_NAME] ?? request.QueryString[Duplicati.Library.Utility.Uri.UrlEncode(XSRF_HEADER_NAME)];
|
||||
xsrftoken = (xsrfq == null || string.IsNullOrWhiteSpace(xsrfq.Value)) ? "" : xsrfq.Value;
|
||||
}
|
||||
|
||||
return xsrftoken;
|
||||
}
|
||||
|
||||
private bool AddXSRFTokenToRespone(HttpServer.IHttpResponse response)
|
||||
{
|
||||
if (m_activexsrf.Count > 500)
|
||||
return false;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Convert.ToBase64String(buf);
|
||||
|
||||
m_activexsrf.AddOrUpdate(token, key => expires, (key, existingExpires) =>
|
||||
{
|
||||
// Simulate the original behavior => if the random token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(XSRF_COOKIE_NAME, token, expires));
|
||||
return true;
|
||||
}
|
||||
|
||||
private string FindAuthCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
var authcookie = request.Cookies[AUTH_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(AUTH_COOKIE_NAME)];
|
||||
var authform = request.Form["auth-token"] ?? request.Form[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
var authquery = request.QueryString["auth-token"] ?? request.QueryString[Library.Utility.Uri.UrlEncode("auth-token")];
|
||||
|
||||
var auth_token = string.IsNullOrWhiteSpace(authcookie?.Value) ? null : authcookie.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authquery?.Value))
|
||||
auth_token = authquery.Value;
|
||||
if (!string.IsNullOrWhiteSpace(authform?.Value))
|
||||
auth_token = authform.Value;
|
||||
|
||||
return auth_token;
|
||||
}
|
||||
|
||||
private bool HasXSRFCookie(HttpServer.IHttpRequest request)
|
||||
{
|
||||
// Clean up expired XSRF cookies
|
||||
foreach (var k in (from n in m_activexsrf where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activexsrf.TryRemove(k, out _);
|
||||
|
||||
var xsrfcookie = request.Cookies[XSRF_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(XSRF_COOKIE_NAME)];
|
||||
var value = xsrfcookie == null ? null : xsrfcookie.Value;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
return false;
|
||||
|
||||
if (m_activexsrf.ContainsKey(value))
|
||||
{
|
||||
m_activexsrf[value] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
else if (m_activexsrf.ContainsKey(Library.Utility.Uri.UrlDecode(value)))
|
||||
{
|
||||
m_activexsrf[Library.Utility.Uri.UrlDecode(value)] = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
HttpServer.HttpInput input = String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString;
|
||||
|
||||
var auth_token = FindAuthCookie(request);
|
||||
var xsrf_token = FindXSRFToken(request);
|
||||
|
||||
if (!HasXSRFCookie(request))
|
||||
{
|
||||
var cookieAdded = AddXSRFTokenToRespone(response);
|
||||
|
||||
if (!cookieAdded)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too Many Concurrent Request, try again later";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (LOGOUT_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
// Remove the active auth token
|
||||
m_activeTokens.TryRemove(auth_token, out _);
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.NoContent;
|
||||
response.Reason = "OK";
|
||||
|
||||
return true;
|
||||
}
|
||||
else if (LOGIN_SCRIPT_URI.Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Remove expired nonces
|
||||
foreach(var k in (from n in m_activeNonces where DateTime.UtcNow > n.Value.Item1 select n.Key))
|
||||
m_activeNonces.TryRemove(k, out _);
|
||||
|
||||
if (input["get-nonce"] != null && !string.IsNullOrWhiteSpace(input["get-nonce"].Value))
|
||||
{
|
||||
if (m_activeNonces.Count > 50)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.ServiceUnavailable;
|
||||
response.Reason = "Too many active login attempts";
|
||||
return true;
|
||||
}
|
||||
|
||||
var password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword;
|
||||
|
||||
if (request.Headers[TRAYICONPASSWORDSOURCE_HEADER] == "database")
|
||||
password = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordTrayIconHash;
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddMinutes(AUTH_TIMEOUT_MINUTES);
|
||||
m_prng.GetBytes(buf);
|
||||
var nonce = Convert.ToBase64String(buf);
|
||||
|
||||
var sha256 = System.Security.Cryptography.SHA256.Create();
|
||||
sha256.TransformBlock(buf, 0, buf.Length, buf, 0);
|
||||
buf = Convert.FromBase64String(password);
|
||||
sha256.TransformFinalBlock(buf, 0, buf.Length);
|
||||
var pwd = Convert.ToBase64String(sha256.Hash);
|
||||
|
||||
m_activeNonces.AddOrUpdate(nonce, key => new Tuple<DateTime, string>(expires, pwd), (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the nonce, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(NONCE_COOKIE_NAME, nonce, expires));
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
{
|
||||
bw.OutputOK(new {
|
||||
Status = "OK",
|
||||
Nonce = nonce,
|
||||
Salt = FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPasswordSalt
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (input["password"] != null && !string.IsNullOrWhiteSpace(input["password"].Value))
|
||||
{
|
||||
var nonce_el = request.Cookies[NONCE_COOKIE_NAME] ?? request.Cookies[Library.Utility.Uri.UrlEncode(NONCE_COOKIE_NAME)];
|
||||
var nonce = nonce_el == null || string.IsNullOrWhiteSpace(nonce_el.Value) ? "" : nonce_el.Value;
|
||||
var urldecoded = nonce == null ? "" : Duplicati.Library.Utility.Uri.UrlDecode(nonce);
|
||||
if (m_activeNonces.ContainsKey(urldecoded))
|
||||
nonce = urldecoded;
|
||||
|
||||
if (!m_activeNonces.ContainsKey(nonce))
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var pwd = m_activeNonces[nonce].Item2;
|
||||
|
||||
// Remove the nonce
|
||||
m_activeNonces.TryRemove(nonce, out _);
|
||||
|
||||
if (pwd != input["password"].Value)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Unauthorized";
|
||||
response.ContentType = "application/json";
|
||||
return true;
|
||||
}
|
||||
|
||||
var buf = new byte[32];
|
||||
var expires = DateTime.UtcNow.AddHours(1);
|
||||
m_prng.GetBytes(buf);
|
||||
var token = Duplicati.Library.Utility.Utility.Base64UrlEncode(buf);
|
||||
while (token.Length > 0 && token.EndsWith("=", StringComparison.Ordinal))
|
||||
token = token.Substring(0, token.Length - 1);
|
||||
|
||||
m_activeTokens.AddOrUpdate(token, key => expires, (key, existingValue) =>
|
||||
{
|
||||
// Simulate the original behavior => if the token, against all odds, is already used
|
||||
// we throw an ArgumentException
|
||||
throw new ArgumentException("An element with the same key already exists in the dictionary.");
|
||||
});
|
||||
|
||||
response.Cookies.Add(new HttpServer.ResponseCookie(AUTH_COOKIE_NAME, token, expires));
|
||||
|
||||
using(var bw = new BodyWriter(response, request))
|
||||
bw.OutputOK();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var limitedAccess =
|
||||
request.Uri.AbsolutePath.StartsWith(RESTHandler.API_URI_PATH, StringComparison.OrdinalIgnoreCase)
|
||||
;
|
||||
|
||||
// Override to allow the CAPTCHA call to go through
|
||||
if (request.Uri.AbsolutePath.StartsWith(CAPTCHA_IMAGE_URI, StringComparison.OrdinalIgnoreCase) && request.Method == "GET")
|
||||
limitedAccess = false;
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
if (xsrf_token != null && m_activexsrf.ContainsKey(xsrf_token))
|
||||
{
|
||||
var expires = DateTime.UtcNow.AddMinutes(XSRF_TIMEOUT_MINUTES);
|
||||
m_activexsrf[xsrf_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(XSRF_COOKIE_NAME, xsrf_token, expires));
|
||||
}
|
||||
else
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.BadRequest;
|
||||
response.Reason = "Missing XSRF Token. Please reload the page";
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(FIXMEGlobal.DataConnection.ApplicationSettings.WebserverPassword))
|
||||
return false;
|
||||
|
||||
foreach(var k in (from n in m_activeTokens where DateTime.UtcNow > n.Value select n.Key))
|
||||
m_activeTokens.TryRemove(k, out _);
|
||||
|
||||
|
||||
// If we have a valid token, proceed
|
||||
if (!string.IsNullOrWhiteSpace(auth_token))
|
||||
{
|
||||
DateTime expires;
|
||||
var found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
if (!found)
|
||||
{
|
||||
auth_token = Duplicati.Library.Utility.Uri.UrlDecode(auth_token);
|
||||
found = m_activeTokens.TryGetValue(auth_token, out expires);
|
||||
}
|
||||
|
||||
if (found && DateTime.UtcNow < expires)
|
||||
{
|
||||
expires = DateTime.UtcNow.AddHours(1);
|
||||
|
||||
m_activeTokens[auth_token] = expires;
|
||||
response.Cookies.Add(new ResponseCookie(AUTH_COOKIE_NAME, auth_token, expires));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ("/".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase) || "/index.html".Equals(request.Uri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
response.Redirect("/login.html");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (limitedAccess)
|
||||
{
|
||||
response.Status = System.Net.HttpStatusCode.Unauthorized;
|
||||
response.Reason = "Not logged in";
|
||||
response.AddHeader("Location", "login.html");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2015, 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 Duplicati.Server.Serialization;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class BodyWriter : System.IO.StreamWriter, IDisposable
|
||||
{
|
||||
private readonly HttpServer.IHttpResponse m_resp;
|
||||
private readonly string m_jsonp;
|
||||
private static readonly object SUCCESS_RESPONSE = new { Status = "OK" };
|
||||
|
||||
// We override the format provider so all JSON output uses US format
|
||||
public override IFormatProvider FormatProvider
|
||||
{
|
||||
get { return System.Globalization.CultureInfo.InvariantCulture; }
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, HttpServer.IHttpRequest request)
|
||||
: this(resp, request.QueryString["jsonp"].Value)
|
||||
{
|
||||
}
|
||||
|
||||
public BodyWriter(HttpServer.IHttpResponse resp, string jsonp)
|
||||
: base(resp.Body, resp.Encoding)
|
||||
{
|
||||
m_resp = resp;
|
||||
m_jsonp = jsonp;
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
}
|
||||
|
||||
protected override void Dispose (bool disposing)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
{
|
||||
base.Flush();
|
||||
m_resp.ContentLength = base.BaseStream.Length;
|
||||
m_resp.Send();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
public void SetOK()
|
||||
{
|
||||
m_resp.Reason = "OK";
|
||||
m_resp.Status = System.Net.HttpStatusCode.OK;
|
||||
}
|
||||
|
||||
public void OutputOK(object result = null)
|
||||
{
|
||||
SetOK();
|
||||
WriteJsonObject(result ?? SUCCESS_RESPONSE);
|
||||
}
|
||||
|
||||
public void WriteJsonObject(object o)
|
||||
{
|
||||
if (!m_resp.HeadersSent)
|
||||
m_resp.ContentType = "application/json";
|
||||
|
||||
using(this)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
this.Write(m_jsonp);
|
||||
this.Write('(');
|
||||
}
|
||||
|
||||
Serializer.SerializeJson(this, o, true);
|
||||
|
||||
if (!string.IsNullOrEmpty(m_jsonp))
|
||||
{
|
||||
this.Write(')');
|
||||
this.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (C) 2016, 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.Linq;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Text;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for creating Captcha images
|
||||
/// </summary>
|
||||
public static class CaptchaUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// A lookup string with characters to use
|
||||
/// </summary>
|
||||
private static readonly string DEFAULT_CHARS = "ACDEFGHJKLMNPQRTUVWXY34679";
|
||||
|
||||
/// <summary>
|
||||
/// A range of possible brush colors
|
||||
/// </summary>
|
||||
private static readonly Brush[] BRUSH_COLORS =
|
||||
typeof(Brushes)
|
||||
.GetProperties(System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public)
|
||||
.Where(x => x.PropertyType == typeof(Brush))
|
||||
.Select(x => x.GetValue(null, null) as Brush)
|
||||
.Where(x => x != null)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Approximate the size in pixels of text drawn at the given fontsize
|
||||
/// </summary>
|
||||
private static int ApproxTextWidth(string text, FontFamily fontfamily, int fontsize)
|
||||
{
|
||||
using (var font = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var graphics = Graphics.FromImage(new Bitmap(1, 1))) {
|
||||
return (int) graphics.MeasureString(text, font).Width;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a random answer.
|
||||
/// </summary>
|
||||
/// <returns>The random answer.</returns>
|
||||
/// <param name="allowedchars">The list of allowed chars, supply a character multiple times to change frequency.</param>
|
||||
/// <param name="minlength">The minimum answer length.</param>
|
||||
/// <param name="maxlength">The maximum answer length.</param>
|
||||
public static string CreateRandomAnswer(string allowedchars = null, int minlength = 10, int maxlength = 12)
|
||||
{
|
||||
allowedchars = allowedchars ?? DEFAULT_CHARS;
|
||||
var rnd = new Random();
|
||||
var len = rnd.Next(Math.Min(minlength, maxlength), Math.Max(minlength, maxlength) + 1);
|
||||
if (len <= 0)
|
||||
throw new ArgumentException($"The values ${minlength} and ${maxlength} gave a final length of {len} and it must be greater than 0");
|
||||
|
||||
return new string(Enumerable.Range(0, len).Select(x => allowedchars[rnd.Next(0, allowedchars.Length)]).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a captcha image.
|
||||
/// </summary>
|
||||
/// <returns>The captcha image.</returns>
|
||||
/// <param name="answer">The captcha solution string.</param>
|
||||
/// <param name="size">The size of the image, omit to get a size based on the string.</param>
|
||||
/// <param name="fontsize">The size of the font used to create the captcha, in pixels.</param>
|
||||
public static Bitmap CreateCaptcha(string answer, Size size = default(Size), int fontsize = 40)
|
||||
{
|
||||
var fontfamily = FontFamily.GenericSansSerif;
|
||||
var text_width = ApproxTextWidth(answer, fontfamily, fontsize);
|
||||
if (size.Width == 0 || size.Height == 0)
|
||||
size = new Size((int) (text_width * 1.2), (int) (fontsize * 1.2));
|
||||
|
||||
var bmp = new Bitmap(size.Width, size.Height);
|
||||
var rnd = new Random();
|
||||
var stray_x = fontsize / 2;
|
||||
var stray_y = size.Height / 4;
|
||||
var ans_stray_x = fontsize / 3;
|
||||
var ans_stray_y = size.Height / 6;
|
||||
using (var graphics = Graphics.FromImage(bmp))
|
||||
using (var font1 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var font2 = new Font(fontfamily, fontsize, GraphicsUnit.Pixel))
|
||||
using (var font3 = new HatchBrush(HatchStyle.Shingle, Color.GhostWhite, Color.DarkBlue))
|
||||
{
|
||||
graphics.Clear(Color.White);
|
||||
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
|
||||
// Apply a some background string to make it hard to do OCR
|
||||
foreach (var color in new[] { Color.Yellow, Color.LightGreen, Color.GreenYellow })
|
||||
using (var brush = new SolidBrush(color))
|
||||
graphics.DrawString(CreateRandomAnswer(minlength: answer.Length, maxlength: answer.Length), font2, brush, rnd.Next(-stray_x, stray_x), rnd.Next(-stray_y, stray_y));
|
||||
|
||||
|
||||
var spacing = (size.Width / fontsize) + rnd.Next(0, stray_x);
|
||||
|
||||
// Create a vertical background lines
|
||||
for (var i = rnd.Next(0, stray_x); i < size.Width; i += spacing)
|
||||
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
|
||||
graphics.DrawLine(pen, i + rnd.Next(-stray_x, stray_x), rnd.Next(0, stray_y), i + rnd.Next(-stray_x, stray_x), size.Height - rnd.Next(0, stray_y));
|
||||
|
||||
spacing = (size.Height / fontsize) + rnd.Next(0, stray_y);
|
||||
// Create a horizontal background lines
|
||||
for (var i = rnd.Next(0, stray_y); i < size.Height; i += spacing)
|
||||
using (var pen = new Pen(BRUSH_COLORS[rnd.Next(0, BRUSH_COLORS.Length)]))
|
||||
graphics.DrawLine(pen, rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y), size.Width - rnd.Next(0, stray_x), i + rnd.Next(-stray_y, stray_y));
|
||||
|
||||
// Draw the actual answer
|
||||
graphics.DrawString(answer, font1, font3, ((size.Width - text_width) / 2) + rnd.Next(-ans_stray_x, ans_stray_x), ((size.Height - fontsize) / 2) + rnd.Next(-ans_stray_y, ans_stray_y));
|
||||
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2015, 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.Linq;
|
||||
using HttpServer;
|
||||
using HttpServer.HttpModules;
|
||||
using HttpServer.Exceptions;
|
||||
using Duplicati.Library.Common.IO;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
internal class IndexHtmlHandler : HttpModule
|
||||
{
|
||||
private readonly string m_webroot;
|
||||
|
||||
private static readonly string[] ForbiddenChars = new string[] {"\\", "..", ":"}.Union(from n in System.IO.Path.GetInvalidPathChars() select n.ToString()).Distinct().ToArray();
|
||||
private static readonly string DirSep = Util.DirectorySeparatorString;
|
||||
|
||||
public IndexHtmlHandler(string webroot) { m_webroot = webroot; }
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
var path = this.GetPath(request.Uri);
|
||||
var html = System.IO.Path.Combine(path, "index.html");
|
||||
var htm = System.IO.Path.Combine(path, "index.htm");
|
||||
|
||||
if (System.IO.Directory.Exists(path) && (System.IO.File.Exists(html) || System.IO.File.Exists(htm)))
|
||||
{
|
||||
if (!request.Uri.AbsolutePath.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
response.Redirect(request.Uri.AbsolutePath + "/");
|
||||
return true;
|
||||
}
|
||||
|
||||
response.Status = System.Net.HttpStatusCode.OK;
|
||||
response.Reason = "OK";
|
||||
response.ContentType = "text/html; charset=utf-8";
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
|
||||
using (var fs = System.IO.File.OpenRead(System.IO.File.Exists(html) ? html : htm))
|
||||
{
|
||||
response.ContentLength = fs.Length;
|
||||
response.Body = fs;
|
||||
response.Send();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetPath(Uri uri)
|
||||
{
|
||||
if (ForbiddenChars.Any(x => uri.AbsolutePath.Contains(x)))
|
||||
throw new BadRequestException("Illegal path");
|
||||
var uripath = Uri.UnescapeDataString(uri.AbsolutePath);
|
||||
while(uripath.Length > 0 && (uripath.StartsWith("/", StringComparison.Ordinal) || uripath.StartsWith(DirSep, StringComparison.Ordinal)))
|
||||
uripath = uripath.Substring(1);
|
||||
return System.IO.Path.Combine(m_webroot, uripath.Replace('/', System.IO.Path.DirectorySeparatorChar));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// Copyright (C) 2015, 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.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
|
||||
using Duplicati.Server.WebServer.RESTMethods;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class RESTHandler : HttpModule
|
||||
{
|
||||
public const string API_URI_PATH = "/api/v1";
|
||||
public static readonly int API_URI_SEGMENTS = API_URI_PATH.Split(new char[] {'/'}).Length;
|
||||
|
||||
private static readonly Dictionary<string, IRESTMethod> _modules = new Dictionary<string, IRESTMethod>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static IDictionary<string, IRESTMethod> Modules { get { return _modules; } }
|
||||
|
||||
/// <summary>
|
||||
/// Loads all REST modules in the Duplicati.Server.WebServer.RESTMethods namespace
|
||||
/// </summary>
|
||||
static RESTHandler()
|
||||
{
|
||||
var lst =
|
||||
from n in typeof(IRESTMethod).Assembly.GetTypes()
|
||||
where
|
||||
n.Namespace == typeof(IRESTMethod).Namespace
|
||||
&&
|
||||
typeof(IRESTMethod).IsAssignableFrom(n)
|
||||
&&
|
||||
!n.IsAbstract
|
||||
&&
|
||||
!n.IsInterface
|
||||
select n;
|
||||
|
||||
foreach(var t in lst)
|
||||
{
|
||||
var m = (IRESTMethod)Activator.CreateInstance(t);
|
||||
_modules.Add(t.Name.ToLowerInvariant(), m);
|
||||
}
|
||||
}
|
||||
|
||||
public static void HandleControlCGI(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, Type module)
|
||||
{
|
||||
var method = request.Method;
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
|
||||
method = request.Headers["X-HTTP-Method-Override"];
|
||||
|
||||
DoProcess(request, response, session, method, module.Name.ToLowerInvariant(), (String.Equals(request.Method, "POST", StringComparison.OrdinalIgnoreCase) ? request.Form : request.QueryString)["id"].Value);
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<string, System.Globalization.CultureInfo> _cultureCache = new ConcurrentDictionary<string, System.Globalization.CultureInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static System.Globalization.CultureInfo ParseRequestCulture(RequestInfo info)
|
||||
{
|
||||
// Inject the override
|
||||
return ParseRequestCulture(string.Format("{0},{1}", info.Request.Headers["X-UI-Language"], info.Request.Headers["Accept-Language"]));
|
||||
}
|
||||
|
||||
public static System.Globalization.CultureInfo ParseDefaultRequestCulture(RequestInfo info)
|
||||
{
|
||||
if (info == null)
|
||||
return null;
|
||||
return ParseRequestCulture(info.Request.Headers["Accept-Language"]);
|
||||
}
|
||||
|
||||
private static System.Globalization.CultureInfo ParseRequestCulture(string acceptheader)
|
||||
{
|
||||
acceptheader = acceptheader ?? string.Empty;
|
||||
|
||||
// Lock-free read
|
||||
System.Globalization.CultureInfo ci;
|
||||
if (_cultureCache.TryGetValue(acceptheader, out ci))
|
||||
return ci;
|
||||
|
||||
// Lock-free assignment, we might compute the value twice
|
||||
return _cultureCache[acceptheader] =
|
||||
// Parse headers like "Accept-Language: da, en-gb;q=0.8, en;q=0.7"
|
||||
acceptheader
|
||||
.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x =>
|
||||
{
|
||||
var opts = x.Split(new[] { ";" }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var lang = opts.FirstOrDefault();
|
||||
var weight =
|
||||
opts.Where(y => y.StartsWith("q=", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(y =>
|
||||
{
|
||||
float f;
|
||||
float.TryParse(y.Substring(2), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out f);
|
||||
return f;
|
||||
}).FirstOrDefault();
|
||||
|
||||
// Set the default weight=1
|
||||
if (weight <= 0.001 && weight >= 0)
|
||||
weight = 1;
|
||||
|
||||
return new KeyValuePair<string, float>(lang, weight);
|
||||
})
|
||||
// Handle priority
|
||||
.OrderByDescending(x => x.Value)
|
||||
.Select(x => x.Key)
|
||||
.Distinct()
|
||||
// Filter invalid/unsupported items
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x) && Library.Localization.LocalizationService.ParseCulture(x) != null)
|
||||
.Select(x => Library.Localization.LocalizationService.ParseCulture(x))
|
||||
// And get the first that works
|
||||
.FirstOrDefault();
|
||||
|
||||
}
|
||||
|
||||
public static void DoProcess(RequestInfo info, string method, string module, string key)
|
||||
{
|
||||
var ci = ParseRequestCulture(info);
|
||||
|
||||
using (Library.Localization.LocalizationService.TemporaryContext(ci))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ci != null)
|
||||
info.Response.AddHeader("Content-Language", ci.Name);
|
||||
|
||||
IRESTMethod mod;
|
||||
_modules.TryGetValue(module, out mod);
|
||||
|
||||
if (mod == null)
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.NotFound;
|
||||
info.Response.Reason = "No such module";
|
||||
}
|
||||
else if (method == HttpServer.Method.Get && mod is IRESTMethodGET get)
|
||||
{
|
||||
if (info.Request.Form != HttpServer.HttpForm.EmptyForm)
|
||||
{
|
||||
if (info.Request.QueryString == HttpServer.HttpInput.Empty)
|
||||
{
|
||||
var r = info.Request.GetType().GetField("_queryString", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
r.SetValue(info.Request, new HttpServer.HttpInput("formdata"));
|
||||
}
|
||||
|
||||
foreach (HttpServer.HttpInputItem v in info.Request.Form)
|
||||
if (!info.Request.QueryString.Contains(v.Name))
|
||||
info.Request.QueryString.Add(v.Name, v.Value);
|
||||
}
|
||||
|
||||
get.GET(key, info);
|
||||
}
|
||||
else if (method == HttpServer.Method.Put && mod is IRESTMethodPUT put)
|
||||
put.PUT(key, info);
|
||||
else if (method == HttpServer.Method.Post && mod is IRESTMethodPOST post)
|
||||
{
|
||||
if (info.Request.Form == HttpServer.HttpForm.EmptyForm || info.Request.Form == HttpServer.HttpInput.Empty)
|
||||
{
|
||||
var r = info.Request.GetType().GetMethod("AssignForm", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance, null, new Type[] {typeof(HttpServer.HttpForm)}, null);
|
||||
r.Invoke(info.Request, new object[] {new HttpServer.HttpForm(info.Request.QueryString)});
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (HttpServer.HttpInputItem v in info.Request.QueryString)
|
||||
if (!info.Request.Form.Contains(v.Name))
|
||||
info.Request.Form.Add(v.Name, v.Value);
|
||||
}
|
||||
|
||||
post.POST(key, info);
|
||||
}
|
||||
else if (method == HttpServer.Method.Delete && mod is IRESTMethodDELETE delete)
|
||||
delete.DELETE(key, info);
|
||||
else if (method == "PATCH" && mod is IRESTMethodPATCH patch)
|
||||
patch.PATCH(key, info);
|
||||
else
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.MethodNotAllowed;
|
||||
info.Response.Reason = "Method is not allowed";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", string.Format("Request for {0} gave error", info.Request.Uri), ex);
|
||||
Console.WriteLine(ex);
|
||||
|
||||
try
|
||||
{
|
||||
if (!info.Response.HeadersSent)
|
||||
{
|
||||
info.Response.Status = System.Net.HttpStatusCode.InternalServerError;
|
||||
info.Response.Reason = "Error";
|
||||
info.Response.ContentType = "text/plain";
|
||||
|
||||
var wex = ex;
|
||||
while (wex is System.Reflection.TargetInvocationException && wex.InnerException != wex)
|
||||
wex = wex.InnerException;
|
||||
|
||||
info.BodyWriter.WriteJsonObject(new
|
||||
{
|
||||
Message = wex.Message,
|
||||
Type = wex.GetType().Name,
|
||||
#if DEBUG
|
||||
Stacktrace = wex.ToString()
|
||||
#endif
|
||||
});
|
||||
info.BodyWriter.Flush();
|
||||
}
|
||||
}
|
||||
catch (Exception flex)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.LogError("", "Reporting error gave error", flex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DoProcess(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session, string method, string module, string key)
|
||||
{
|
||||
using(var reqinfo = new RequestInfo(request, response, session))
|
||||
DoProcess(reqinfo, method, module, key);
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!request.Uri.AbsolutePath.StartsWith(API_URI_PATH, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
var module = request.Uri.Segments.Skip(API_URI_SEGMENTS).FirstOrDefault();
|
||||
if (string.IsNullOrWhiteSpace(module))
|
||||
module = "help";
|
||||
|
||||
module = module.Trim('/');
|
||||
|
||||
var key = string.Join("", request.Uri.Segments.Skip(API_URI_SEGMENTS + 1)).Trim('/');
|
||||
|
||||
var method = request.Method;
|
||||
if (!string.IsNullOrWhiteSpace(request.Headers["X-HTTP-Method-Override"]))
|
||||
method = request.Headers["X-HTTP-Method-Override"];
|
||||
|
||||
DoProcess(request, response, session, method, module, key);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using HttpServer.HttpModules;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Duplicati.Library.Common.IO;
|
||||
using Duplicati.Library.RestAPI;
|
||||
|
||||
namespace Duplicati.Server.WebServer
|
||||
{
|
||||
public class Server
|
||||
{
|
||||
/// <summary>
|
||||
/// The tag used for logging
|
||||
/// </summary>
|
||||
private static readonly string LOGTAG = Duplicati.Library.Logging.Log.LogTagFromType<Server>();
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webroot folder
|
||||
/// </summary>
|
||||
public const string OPTION_WEBROOT = "webservice-webroot";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen port
|
||||
/// </summary>
|
||||
public const string OPTION_PORT = "webservice-port";
|
||||
|
||||
/// <summary>
|
||||
/// Option for changing the webservice listen interface
|
||||
/// </summary>
|
||||
public const string OPTION_INTERFACE = "webservice-interface";
|
||||
|
||||
/// <summary>
|
||||
/// The default path to the web root
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_WEBROOT = "webroot";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening port
|
||||
/// </summary>
|
||||
public const int DEFAULT_OPTION_PORT = 8200;
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILE = "webservice-sslcertificatefile";
|
||||
|
||||
/// <summary>
|
||||
/// Option for setting the webservice SSL certificate key
|
||||
/// </summary>
|
||||
public const string OPTION_SSLCERTIFICATEFILEPASSWORD = "webservice-sslcertificatepassword";
|
||||
|
||||
/// <summary>
|
||||
/// The default listening interface
|
||||
/// </summary>
|
||||
public const string DEFAULT_OPTION_INTERFACE = "loopback";
|
||||
|
||||
/// <summary>
|
||||
/// The single webserver instance
|
||||
/// </summary>
|
||||
private readonly HttpServer.HttpServer m_server;
|
||||
|
||||
/// <summary>
|
||||
/// The webserver listening port
|
||||
/// </summary>
|
||||
public readonly int Port;
|
||||
|
||||
/// <summary>
|
||||
/// A string that is sent out instead of password values
|
||||
/// </summary>
|
||||
public const string PASSWORD_PLACEHOLDER = "**********";
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the webserver and starts it
|
||||
/// </summary>
|
||||
/// <param name="options">A set of options</param>
|
||||
public Server(IDictionary<string, string> options)
|
||||
{
|
||||
string portstring;
|
||||
IEnumerable<int> ports = null;
|
||||
options.TryGetValue(OPTION_PORT, out portstring);
|
||||
if (!string.IsNullOrEmpty(portstring))
|
||||
ports =
|
||||
from n in portstring.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
where int.TryParse(n, out _)
|
||||
select int.Parse(n);
|
||||
|
||||
if (ports == null || !ports.Any())
|
||||
ports = new int[] { DEFAULT_OPTION_PORT };
|
||||
|
||||
string interfacestring;
|
||||
System.Net.IPAddress listenInterface;
|
||||
options.TryGetValue(OPTION_INTERFACE, out interfacestring);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface;
|
||||
if (string.IsNullOrWhiteSpace(interfacestring))
|
||||
interfacestring = DEFAULT_OPTION_INTERFACE;
|
||||
|
||||
if (interfacestring.Trim() == "*" || interfacestring.Trim().Equals("any", StringComparison.OrdinalIgnoreCase) || interfacestring.Trim().Equals("all", StringComparison.OrdinalIgnoreCase))
|
||||
listenInterface = System.Net.IPAddress.Any;
|
||||
else if (interfacestring.Trim() == "loopback")
|
||||
listenInterface = System.Net.IPAddress.Loopback;
|
||||
else
|
||||
listenInterface = System.Net.IPAddress.Parse(interfacestring);
|
||||
|
||||
string certificateFile;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILE, out certificateFile);
|
||||
|
||||
string certificateFilePassword;
|
||||
options.TryGetValue(OPTION_SSLCERTIFICATEFILEPASSWORD, out certificateFilePassword);
|
||||
|
||||
X509Certificate2 cert = null;
|
||||
bool certValid = false;
|
||||
|
||||
if (certificateFile == null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cert = FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate;
|
||||
|
||||
if (cert != null)
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Duplicati.Library.Logging.Log.WriteWarningMessage(LOGTAG, "DefectStoredSSLCert", ex, Strings.Server.DefectSSLCertInDatabase);
|
||||
}
|
||||
}
|
||||
else if (certificateFile.Length == 0)
|
||||
{
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(certificateFilePassword))
|
||||
cert = new X509Certificate2(certificateFile, "", X509KeyStorageFlags.Exportable);
|
||||
else
|
||||
cert = new X509Certificate2(certificateFile, certificateFilePassword, X509KeyStorageFlags.Exportable);
|
||||
|
||||
certValid = cert.HasPrivateKey;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(Strings.Server.SSLCertificateFailure(ex.Message), ex);
|
||||
}
|
||||
}
|
||||
|
||||
// If we are in hosted mode with no specified port,
|
||||
// then try different ports
|
||||
foreach (var p in ports)
|
||||
try
|
||||
{
|
||||
// Due to the way the server is initialized,
|
||||
// we cannot try to start it again on another port,
|
||||
// so we create a new server for each attempt
|
||||
|
||||
var server = CreateServer(options);
|
||||
|
||||
if (!certValid)
|
||||
server.Start(listenInterface, p);
|
||||
else
|
||||
{
|
||||
var secProtocols = System.Security.Authentication.SslProtocols.Tls12;
|
||||
|
||||
try
|
||||
{
|
||||
//try TLS 1.3 (type not available on .NET < 4.8)
|
||||
secProtocols = System.Security.Authentication.SslProtocols.Tls12 | (System.Security.Authentication.SslProtocols)12288;
|
||||
}
|
||||
catch (NotSupportedException)
|
||||
{
|
||||
}
|
||||
server.Start(listenInterface, p, cert, secProtocols, null, false);
|
||||
}
|
||||
|
||||
m_server = server;
|
||||
m_server.ServerName = string.Format("{0} v{1}", Library.AutoUpdater.AutoUpdateSettings.AppName, System.Reflection.Assembly.GetExecutingAssembly().GetName().Version);
|
||||
this.Port = p;
|
||||
|
||||
if (interfacestring != FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface)
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
|
||||
|
||||
if (certValid && !cert.Equals(FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate))
|
||||
FIXMEGlobal.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
|
||||
|
||||
Duplicati.Library.Logging.Log.WriteInformationMessage(LOGTAG, "ServerListening", Strings.Server.StartedServer(listenInterface.ToString(), p));
|
||||
|
||||
return;
|
||||
}
|
||||
catch (System.Net.Sockets.SocketException)
|
||||
{
|
||||
}
|
||||
|
||||
throw new Exception(Strings.Server.ServerStartFailure(ports));
|
||||
}
|
||||
|
||||
private static void AddMimeTypes(FileModule fm)
|
||||
{
|
||||
fm.AddDefaultMimeTypes();
|
||||
fm.MimeTypes["htc"] = "text/x-component";
|
||||
fm.MimeTypes["json"] = "application/json";
|
||||
fm.MimeTypes["map"] = "application/json";
|
||||
fm.MimeTypes["htm"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["html"] = "text/html; charset=utf-8";
|
||||
fm.MimeTypes["hbs"] = "application/x-handlebars-template";
|
||||
fm.MimeTypes["woff"] = "application/font-woff";
|
||||
fm.MimeTypes["woff2"] = "application/font-woff";
|
||||
}
|
||||
|
||||
private static HttpServer.HttpServer CreateServer(IDictionary<string, string> options)
|
||||
{
|
||||
HttpServer.HttpServer server = new HttpServer.HttpServer();
|
||||
|
||||
server.Add(new HostHeaderChecker());
|
||||
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SYNO_DSM_AUTH") ?? string.Empty, "1"))
|
||||
server.Add(new SynologyAuthenticationHandler());
|
||||
|
||||
server.Add(new AuthenticationHandler());
|
||||
|
||||
server.Add(new RESTHandler());
|
||||
|
||||
string webroot = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
|
||||
string install_webroot = System.IO.Path.Combine(Library.AutoUpdater.UpdaterManager.InstalledBaseDir, "webroot");
|
||||
|
||||
#if DEBUG
|
||||
// Easy test for extensions while debugging
|
||||
install_webroot = Library.AutoUpdater.UpdaterManager.InstalledBaseDir;
|
||||
|
||||
if (!System.IO.Directory.Exists(System.IO.Path.Combine(webroot, "webroot")))
|
||||
{
|
||||
//For debug we go "../../../.." to get out of "GUI/Duplicati.GUI.TrayIcon/bin/debug"
|
||||
string tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", "..", "..", ".."));
|
||||
tmpwebroot = System.IO.Path.Combine(tmpwebroot, "Server");
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
else
|
||||
{
|
||||
//If we are running the server standalone, we only need to exit "bin/Debug"
|
||||
tmpwebroot = System.IO.Path.GetFullPath(System.IO.Path.Combine(webroot, "..", ".."));
|
||||
if (System.IO.Directory.Exists(System.IO.Path.Combine(tmpwebroot, "webroot")))
|
||||
webroot = tmpwebroot;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
webroot = System.IO.Path.Combine(webroot, "webroot");
|
||||
|
||||
if (options.ContainsKey(OPTION_WEBROOT))
|
||||
{
|
||||
string userroot = options[OPTION_WEBROOT];
|
||||
#if DEBUG
|
||||
//In debug mode we do not care where the path points
|
||||
#else
|
||||
//In release mode we check that the user supplied path is located
|
||||
// in the same folders as the running application, to avoid users
|
||||
// that inadvertently expose top level folders
|
||||
if (!string.IsNullOrWhiteSpace(userroot)
|
||||
&&
|
||||
(
|
||||
userroot.StartsWith(Util.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparison)
|
||||
||
|
||||
userroot.StartsWith(Util.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparison)
|
||||
)
|
||||
)
|
||||
#endif
|
||||
{
|
||||
webroot = userroot;
|
||||
install_webroot = webroot;
|
||||
}
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "customized")))
|
||||
{
|
||||
var customized_files = new CacheControlFileHandler("/customized/", System.IO.Path.Combine(install_webroot, "customized"));
|
||||
AddMimeTypes(customized_files);
|
||||
server.Add(customized_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "oem")))
|
||||
{
|
||||
var oem_files = new CacheControlFileHandler("/oem/", System.IO.Path.Combine(install_webroot, "oem"));
|
||||
AddMimeTypes(oem_files);
|
||||
server.Add(oem_files);
|
||||
}
|
||||
|
||||
if (install_webroot != webroot && System.IO.Directory.Exists(System.IO.Path.Combine(install_webroot, "package")))
|
||||
{
|
||||
var proxy_files = new CacheControlFileHandler("/proxy/", System.IO.Path.Combine(install_webroot, "package"));
|
||||
AddMimeTypes(proxy_files);
|
||||
server.Add(proxy_files);
|
||||
}
|
||||
|
||||
var fh = new CacheControlFileHandler("/", webroot, true);
|
||||
AddMimeTypes(fh);
|
||||
server.Add(fh);
|
||||
|
||||
server.Add(new IndexHtmlHandler(webroot));
|
||||
#if DEBUG
|
||||
//For debugging, it is nice to know when we get a 404
|
||||
server.Add(new DebugReportHandler());
|
||||
#endif
|
||||
return server;
|
||||
}
|
||||
|
||||
private class DebugReportHandler : HttpModule
|
||||
{
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
System.Diagnostics.Trace.WriteLine(string.Format("Rejecting request for {0}", request.Uri));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class CacheControlFileHandler : FileModule
|
||||
{
|
||||
public CacheControlFileHandler(string baseUri, string basePath, bool useLastModifiedHeader = false)
|
||||
: base(baseUri, basePath, useLastModifiedHeader)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
if (!this.CanHandle(request.Uri))
|
||||
return false;
|
||||
|
||||
if (request.Uri.AbsolutePath.EndsWith("index.html", StringComparison.Ordinal) || request.Uri.AbsolutePath.EndsWith("index.htm", StringComparison.Ordinal))
|
||||
response.AddHeader("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0");
|
||||
else
|
||||
response.AddHeader("Cache-Control", "max-age=" + (60 * 60 * 24));
|
||||
return base.Process(request, response, session);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Module for injecting host header verification
|
||||
/// </summary>
|
||||
private class HostHeaderChecker : HttpModule
|
||||
{
|
||||
/// <summary>
|
||||
/// The hostnames that we allow
|
||||
/// </summary>
|
||||
private string[] m_lastSplitNames;
|
||||
|
||||
/// <summary>
|
||||
/// The string used to generate m_lastSplitNames;
|
||||
/// </summary>
|
||||
private string m_lastAllowed;
|
||||
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv4 addresses.
|
||||
/// Note that this also detects things that are not valid IPv4.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV4 = new System.Text.RegularExpressions.Regex(@"((\d){1,3}\.){3}(\d){1,3}");
|
||||
/// <summary>
|
||||
/// A regex to detect potential IPv6 addresses.
|
||||
/// Note that this also detects things that are not valid IPv6.
|
||||
/// </summary>
|
||||
private static readonly System.Text.RegularExpressions.Regex IPV6 = new System.Text.RegularExpressions.Regex(@"(\:)?(\:?[A-Fa-f0-9]{1,4}\:?){1,8}(\:)?");
|
||||
|
||||
/// <summary>
|
||||
/// The hostnames that are always allowed
|
||||
/// </summary>
|
||||
private static readonly string[] DEFAULT_ALLOWED = new string[] { "localhost", "127.0.0.1", "::1", "localhost.localdomain" };
|
||||
|
||||
/// <summary>
|
||||
/// Process the received request
|
||||
/// </summary>
|
||||
/// <returns>A flag indicating if the request is handled.</returns>
|
||||
/// <param name="request">The received request.</param>
|
||||
/// <param name="response">The response object.</param>
|
||||
/// <param name="session">The session state.</param>
|
||||
public override bool Process(HttpServer.IHttpRequest request, HttpServer.IHttpResponse response, HttpServer.Sessions.IHttpSession session)
|
||||
{
|
||||
string[] h = null;
|
||||
var hstring = FIXMEGlobal.DataConnection.ApplicationSettings.AllowedHostnames;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(hstring))
|
||||
{
|
||||
h = m_lastSplitNames;
|
||||
if (hstring != m_lastAllowed)
|
||||
{
|
||||
m_lastAllowed = hstring;
|
||||
h = m_lastSplitNames = (hstring ?? string.Empty).Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
if (h == null || h.Length == 0)
|
||||
h = null;
|
||||
}
|
||||
|
||||
// For some reason, the web server strips out the host header
|
||||
var host = request.Headers["Host"];
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
host = request.Uri.Host;
|
||||
|
||||
// This should not happen
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
response.Reason = "Invalid request, missing host header";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var msg = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = msg.Length;
|
||||
response.Body.Write(msg, 0, msg.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check the hostnames we always allow
|
||||
if (DEFAULT_ALLOWED.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Then the user specified ones
|
||||
if (h != null && h.Contains(host, StringComparer.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
// Disable checks if we have an asterisk
|
||||
if (h != null && Array.IndexOf(h, "*") >= 0)
|
||||
return false;
|
||||
|
||||
// Finally, check if we have a potential IP address
|
||||
var v4 = IPV4.Match(host);
|
||||
var v6 = IPV6.Match(host);
|
||||
|
||||
if ((v4.Success && v4.Length == host.Length) || (v6.Success && v6.Length == host.Length))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Verify that the hostname is indeed a valid IP address
|
||||
System.Net.IPAddress.Parse(host);
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{ }
|
||||
}
|
||||
|
||||
// Failed to find a valid header
|
||||
response.Reason = $"The host header sent by the client is not allowed";
|
||||
response.Status = System.Net.HttpStatusCode.Forbidden;
|
||||
var txt = System.Text.Encoding.ASCII.GetBytes(response.Reason);
|
||||
response.ContentType = "text/plain";
|
||||
response.ContentLength = txt.Length;
|
||||
response.Body.Write(txt, 0, txt.Length);
|
||||
response.Send();
|
||||
return true;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
// 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.Concurrent;
|
||||
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-evaluate 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 ConcurrentDictionary<string, DateTime> m_logincache = new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
/// <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.OrdinalIgnoreCase)
|
||||
||
|
||||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGIN_SCRIPT_URI, StringComparison.OrdinalIgnoreCase)
|
||||
||
|
||||
request.Uri.AbsolutePath.StartsWith(AuthenticationHandler.LOGOUT_SCRIPT_URI, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
groups = groups.Replace(Environment.NewLine, 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,
|
||||
RedirectStandardError = false
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user