2014-06-27 13:23:05 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Text ;
using HttpServer.HttpModules ;
using System.IO ;
using Duplicati.Server.Serialization ;
2016-06-26 13:47:24 +02:00
using System.Security.Cryptography.X509Certificates ;
2014-06-27 13:23:05 +02:00
namespace Duplicati.Server.WebServer
{
public class Server
{
/// <summary>
/// Option for changing the webroot folder
/// </summary>
public const string OPTION_WEBROOT = "webservice-webroot" ;
2016-06-26 13:47:24 +02:00
2014-06-27 13:23:05 +02:00
/// <summary>
/// Option for changing the webservice listen port
/// </summary>
public const string OPTION_PORT = "webservice-port" ;
2016-06-26 13:47:24 +02:00
2014-06-27 13:23:05 +02:00
/// <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 ;
2016-06-26 13:47:24 +02:00
/// <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" ;
2014-06-27 13:23:05 +02:00
/// <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>
/// 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 );
2014-08-15 00:31:26 +02:00
if ( string . IsNullOrWhiteSpace ( interfacestring ))
interfacestring = Program . DataConnection . ApplicationSettings . ServerListenInterface ;
2014-06-27 13:23:05 +02:00
if ( string . IsNullOrWhiteSpace ( interfacestring ))
interfacestring = DEFAULT_OPTION_INTERFACE ;
2014-08-15 00:31:26 +02:00
2014-06-27 13:23:05 +02:00
if ( interfacestring . Trim () == "*" || interfacestring . Trim (). Equals ( "any" , StringComparison . InvariantCultureIgnoreCase ))
listenInterface = System . Net . IPAddress . Any ;
else if ( interfacestring . Trim () == "loopback" )
listenInterface = System . Net . IPAddress . Loopback ;
else
listenInterface = System . Net . IPAddress . Parse ( interfacestring );
2016-06-26 13:47:24 +02:00
string certificateFile ;
options . TryGetValue ( OPTION_SSLCERTIFICATEFILE , out certificateFile );
string certificateFilePassword ;
options . TryGetValue ( OPTION_SSLCERTIFICATEFILEPASSWORD , out certificateFilePassword );
X509Certificate2 cert = null ;
bool certValid = false ;
2016-06-26 16:09:07 +02:00
if ( certificateFile == null )
{
try
{
cert = Program . DataConnection . ApplicationSettings . ServerSSLCertificate ;
if ( cert != null )
2016-06-27 18:34:00 +02:00
certValid = cert . HasPrivateKey ;
2016-06-26 16:09:07 +02:00
}
catch ( Exception ex )
{
Library . Logging . Log . WriteMessage ( "Unable to create SSL certificate using data from database. Starting without SSL." , Duplicati . Library . Logging . LogMessageType . Warning , ex );
}
}
else if ( certificateFile . Length == 0 )
{
Program . DataConnection . ApplicationSettings . ServerSSLCertificate = null ;
}
else
2016-06-26 13:47:24 +02:00
{
try
{
if ( string . IsNullOrWhiteSpace ( certificateFilePassword ))
2016-06-26 16:09:07 +02:00
cert = new X509Certificate2 ( certificateFile , "" , X509KeyStorageFlags . Exportable );
2016-06-26 13:47:24 +02:00
else
2016-06-26 16:09:07 +02:00
cert = new X509Certificate2 ( certificateFile , certificateFilePassword , X509KeyStorageFlags . Exportable );
2016-06-26 13:47:24 +02:00
2016-06-27 18:23:05 +02:00
certValid = cert . HasPrivateKey ;
2016-06-26 13:47:24 +02:00
}
catch ( Exception ex )
{
throw new Exception ( "Unable to create SSL certificate using provided parameters. Exception detail: " + ex . Message );
}
}
2014-06-27 13:23:05 +02:00
// If we are in hosted mode with no specified port,
// then try different ports
2016-06-26 13:47:24 +02:00
foreach ( var p in ports )
2014-06-27 13:23:05 +02:00
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 );
2016-06-26 13:47:24 +02:00
if (! certValid )
server . Start ( listenInterface , p );
else
server . Start ( listenInterface , p , cert , System . Security . Authentication . SslProtocols . Tls | System . Security . Authentication . SslProtocols . Tls11 | System . Security . Authentication . SslProtocols . Tls12 , null , false );
2014-06-27 13:23:05 +02:00
m_server = server ;
m_server . ServerName = "Duplicati v" + System . Reflection . Assembly . GetExecutingAssembly (). GetName (). Version . ToString ();
this . Port = p ;
2014-08-15 00:31:26 +02:00
if ( interfacestring != Program . DataConnection . ApplicationSettings . ServerListenInterface )
Program . DataConnection . ApplicationSettings . ServerListenInterface = interfacestring ;
2016-06-26 16:09:07 +02:00
if ( certValid && ! cert . Equals ( Program . DataConnection . ApplicationSettings . ServerSSLCertificate ))
Program . DataConnection . ApplicationSettings . ServerSSLCertificate = cert ;
2014-06-27 13:23:05 +02:00
return ;
}
catch ( System . Net . Sockets . SocketException )
{
}
throw new Exception ( "Unable to open a socket for listening, tried ports: " + string . Join ( "," , from n in ports select n . ToString ()));
}
2015-07-04 14:23:47 +02:00
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" ;
2016-09-15 11:39:27 +02:00
fm . MimeTypes [ "woff2" ] = "application/font-woff" ;
}
2014-08-05 10:10:27 +02:00
2014-06-27 13:23:05 +02:00
private static HttpServer . HttpServer CreateServer ( IDictionary < string , string > options )
{
HttpServer . HttpServer server = new HttpServer . HttpServer ();
server . Add ( new AuthenticationHandler ());
server . Add ( new ControlHandler ());
2015-07-04 13:42:52 +02:00
server . Add ( new RESTHandler ());
2014-06-27 13:23:05 +02:00
string webroot = System . IO . Path . GetDirectoryName ( System . Reflection . Assembly . GetExecutingAssembly (). Location );
2014-08-05 10:10:27 +02:00
string install_webroot = System . IO . Path . Combine ( Library . AutoUpdater . UpdaterManager . InstalledBaseDir , "webroot" );
2014-06-27 13:23:05 +02:00
#if DEBUG
2014-08-05 10:10:27 +02:00
// Easy test for extensions while debugging
install_webroot = Library . AutoUpdater . UpdaterManager . InstalledBaseDir ;
2014-07-15 17:24:50 +02:00
if (! System . IO . Directory . Exists ( System . IO . Path . Combine ( webroot , "webroot" )))
2014-06-27 13:23:05 +02:00
{
2014-07-15 17:24:50 +02:00
//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" );
2014-06-27 13:23:05 +02:00
if ( System . IO . Directory . Exists ( System . IO . Path . Combine ( tmpwebroot , "webroot" )))
webroot = tmpwebroot ;
2014-07-15 17:24:50 +02:00
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 ;
}
2014-06-27 13:23:05 +02:00
}
#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 ;
2014-08-05 10:10:27 +02:00
install_webroot = webroot ;
2014-06-27 13:23:05 +02:00
}
}
2014-08-05 10:10:27 +02:00
if ( install_webroot != webroot && System . IO . Directory . Exists ( System . IO . Path . Combine ( install_webroot , "customized" )))
{
var customized_files = new FileModule ( "/customized/" , System . IO . Path . Combine ( install_webroot , "customized" ));
2015-07-04 14:23:47 +02:00
AddMimeTypes ( customized_files );
2014-08-05 10:10:27 +02:00
server . Add ( customized_files );
}
2016-09-15 11:39:27 +02:00
if ( install_webroot != webroot && System . IO . Directory . Exists ( System . IO . Path . Combine ( install_webroot , "oem" )))
{
var oem_files = new FileModule ( "/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 FileModule ( "/proxy/" , System . IO . Path . Combine ( install_webroot , "package" ));
AddMimeTypes ( proxy_files );
server . Add ( proxy_files );
}
2016-05-13 00:03:16 +02:00
2014-08-05 10:10:27 +02:00
var fh = new FileModule ( "/" , webroot );
2015-07-04 14:23:47 +02:00
AddMimeTypes ( fh );
2014-06-27 13:23:05 +02:00
server . Add ( fh );
2015-07-04 14:23:47 +02:00
2014-08-15 11:11:51 +02:00
server . Add ( new IndexHtmlHandler ( webroot ));
2014-06-27 13:23:05 +02:00
#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 ;
}
}
}
}