Files
duplicati/Duplicati/Server/WebServer/Server.cs
T

336 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using HttpServer.HttpModules;
using System.IO;
using Duplicati.Server.Serialization;
using System.Security.Cryptography.X509Certificates;
namespace Duplicati.Server.WebServer
{
public class 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 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>
/// Writes a log message to Console, Service-hook and normal log
/// </summary>
/// <param name="message">The message to write.</param>
/// <param name="type">The message type.</param>
/// <param name="ex">The exception, if any.</param>
public static void WriteLogMessage(string message, Library.Logging.LogMessageType type, Exception ex)
{
System.Console.WriteLine(message);
Library.Logging.Log.WriteMessage(message, type, ex);
Program.LogHandler.WriteMessage(message, type, ex);
}
/// <summary>
/// Sets up the webserver and starts it
/// </summary>
/// <param name="options">A set of options</param>
public Server(IDictionary<string, string> options)
{
int port;
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 port)
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 = Program.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 = Program.DataConnection.ApplicationSettings.ServerSSLCertificate;
if (cert != null)
certValid = cert.HasPrivateKey;
}
catch (Exception ex)
{
WriteLogMessage(Strings.Server.DefectSSLCertInDatabase, Duplicati.Library.Logging.LogMessageType.Warning, ex);
}
}
else if (certificateFile.Length == 0)
{
Program.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
server.Start(listenInterface, p, cert, System.Security.Authentication.SslProtocols.Tls11 | System.Security.Authentication.SslProtocols.Tls12, 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 != Program.DataConnection.ApplicationSettings.ServerListenInterface)
Program.DataConnection.ApplicationSettings.ServerListenInterface = interfacestring;
if (certValid && !cert.Equals(Program.DataConnection.ApplicationSettings.ServerSSLCertificate))
Program.DataConnection.ApplicationSettings.ServerSSLCertificate = cert;
WriteLogMessage(Strings.Server.StartedServer(listenInterface.ToString(), p), Library.Logging.LogMessageType.Information, null);
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();
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(Library.Utility.Utility.AppendDirSeparator(System.Reflection.Assembly.GetExecutingAssembly().Location), Library.Utility.Utility.ClientFilenameStringComparision)
||
userroot.StartsWith(Library.Utility.Utility.AppendDirSeparator(Program.StartupPath), Library.Utility.Utility.ClientFilenameStringComparision)
)
)
#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);
}
}
}
}