From 310e6bcf618693e9bda2ee04a3ad781172685e25 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 30 Aug 2018 11:35:44 -0400 Subject: [PATCH] convert setup to use config.yml --- util/Setup/AppIdBuilder.cs | 37 ++-- util/Setup/CertBuilder.cs | 78 ++++--- util/Setup/Context.cs | 254 ++++++++++++++++++++++ util/Setup/DockerComposeBuilder.cs | 255 +++-------------------- util/Setup/EnvironmentFileBuilder.cs | 156 ++++++++------ util/Setup/Helpers.cs | 18 ++ util/Setup/NginxConfigBuilder.cs | 210 ++++++------------- util/Setup/Program.cs | 228 ++++++-------------- util/Setup/Setup.csproj | 3 + util/Setup/Templates/AppId.hbs | 15 ++ util/Setup/Templates/DockerCompose.hbs | 133 ++++++++++++ util/Setup/Templates/EnvironmentFile.hbs | 3 + util/Setup/Templates/NginxConfig.hbs | 99 +++++++++ util/Setup/YamlComments.cs | 111 ++++++++++ 14 files changed, 954 insertions(+), 646 deletions(-) create mode 100644 util/Setup/Context.cs create mode 100644 util/Setup/Templates/AppId.hbs create mode 100644 util/Setup/Templates/DockerCompose.hbs create mode 100644 util/Setup/Templates/EnvironmentFile.hbs create mode 100644 util/Setup/Templates/NginxConfig.hbs create mode 100644 util/Setup/YamlComments.cs diff --git a/util/Setup/AppIdBuilder.cs b/util/Setup/AppIdBuilder.cs index 23177e98d..f31a84e8c 100644 --- a/util/Setup/AppIdBuilder.cs +++ b/util/Setup/AppIdBuilder.cs @@ -5,35 +5,32 @@ namespace Bit.Setup { public class AppIdBuilder { - public AppIdBuilder(string url) - { - Url = url; - } + private readonly Context _context; - public string Url { get; private set; } + public AppIdBuilder(Context context) + { + _context = context; + } public void Build() { + var model = new TemplateModel + { + Url = _context.Config.Url + }; + Console.WriteLine("Building FIDO U2F app id."); Directory.CreateDirectory("/bitwarden/web/"); + var template = Helpers.ReadTemplate("AppId"); using(var sw = File.CreateText("/bitwarden/web/app-id.json")) { - sw.Write($@"{{ - ""trustedFacets"": [ - {{ - ""version"": {{ - ""major"": 1, - ""minor"": 0 - }}, - ""ids"": [ - ""{Url}"", - ""ios:bundle-id:com.8bit.bitwarden"", - ""android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"" - ] - }} - ] -}}"); + sw.Write(template(model)); } } + + public class TemplateModel + { + public string Url { get; set; } + } } } diff --git a/util/Setup/CertBuilder.cs b/util/Setup/CertBuilder.cs index 441ee8e3f..fd8e485be 100644 --- a/util/Setup/CertBuilder.cs +++ b/util/Setup/CertBuilder.cs @@ -5,53 +5,81 @@ namespace Bit.Setup { public class CertBuilder { - public CertBuilder(string domain, string identityCertPassword, bool letsEncrypt, bool ssl) + private readonly Context _context; + + public CertBuilder(Context context) { - Domain = domain; - IdentityCertPassword = identityCertPassword; - LetsEncrypt = letsEncrypt; - Ssl = ssl; + _context = context; } - public string Domain { get; private set; } - public bool LetsEncrypt { get; private set; } - public bool Ssl { get; private set; } - public string IdentityCertPassword { get; private set; } - - public bool BuildForInstall() + public void BuildForInstall() { - var selfSignedSsl = false; - if(!Ssl) + _context.Config.Ssl = _context.Config.SslManagedLetsEncrypt; + + if(!_context.Config.Ssl) { - if(Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?")) + _context.Config.Ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?"); + if(_context.Config.Ssl) { - Directory.CreateDirectory($"/bitwarden/ssl/self/{Domain}/"); + Directory.CreateDirectory($"/bitwarden/ssl/{_context.Install.Domain}/"); + var message = "Make sure 'certificate.crt' and 'private.key' are provided in the \n" + + "appropriate directory before running 'start' (see docs for info)."; + Helpers.ShowBanner("NOTE", message); + } + else if(Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?")) + { + Directory.CreateDirectory($"/bitwarden/ssl/self/{_context.Install.Domain}/"); Console.WriteLine("Generating self signed SSL certificate."); - Ssl = selfSignedSsl = true; + _context.Config.Ssl = true; + _context.Install.Trusted = false; + _context.Install.SelfSignedCert = true; Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 365 " + - $"-keyout /bitwarden/ssl/self/{Domain}/private.key " + - $"-out /bitwarden/ssl/self/{Domain}/certificate.crt " + + $"-keyout /bitwarden/ssl/self/{_context.Install.Domain}/private.key " + + $"-out /bitwarden/ssl/self/{_context.Install.Domain}/certificate.crt " + $"-reqexts SAN -extensions SAN " + - $"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{Domain}\nbasicConstraints=CA:true')) " + - $"-subj \"/C=US/ST=Florida/L=Jacksonville/O=8bit Solutions LLC/OU=Bitwarden/CN={Domain}\""); + $"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true')) " + + $"-subj \"/C=US/ST=Florida/L=Jacksonville/O=8bit Solutions LLC/OU=Bitwarden/CN={_context.Install.Domain}\""); } } - if(LetsEncrypt) + if(_context.Config.SslManagedLetsEncrypt) { - Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{Domain}/"); - Helpers.Exec($"openssl dhparam -out /bitwarden/letsencrypt/live/{Domain}/dhparam.pem 2048"); + _context.Install.Trusted = true; + _context.Install.DiffieHellman = true; + Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{_context.Install.Domain}/"); + Helpers.Exec($"openssl dhparam -out /bitwarden/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048"); + } + else if(_context.Config.Ssl && !_context.Install.SelfSignedCert) + { + _context.Install.Trusted = Helpers.ReadQuestion("Is this a trusted SSL certificate " + + "(requires ca.crt, see docs)?"); } Console.WriteLine("Generating key for IdentityServer."); + _context.Install.IdentityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); Directory.CreateDirectory("/bitwarden/identity/"); Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " + "-out identity.crt -subj \"/CN=Bitwarden IdentityServer\" -days 10950"); Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " + - $"-in identity.crt -certfile identity.crt -passout pass:{IdentityCertPassword}"); + $"-in identity.crt -certfile identity.crt -passout pass:{_context.Install.IdentityCertPassword}"); Console.WriteLine(); - return selfSignedSsl; + + if(!_context.Config.Ssl) + { + var message = "You are not using a SSL certificate. Bitwarden requires HTTPS to operate. \n" + + "You must front your installation with a HTTPS proxy or the web vault (and \n" + + "other Bitwarden apps) will not work properly."; + Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow); + } + else if(_context.Config.Ssl && !_context.Install.Trusted) + { + var message = "You are using an untrusted SSL certificate. This certificate will not be \n" + + "trusted by Bitwarden client applications. You must add this certificate to \n" + + "the trusted store on each device or else you will receive errors when trying \n" + + "to connect to your installation."; + Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow); + } } } } diff --git a/util/Setup/Context.cs b/util/Setup/Context.cs new file mode 100644 index 000000000..91a20c84e --- /dev/null +++ b/util/Setup/Context.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace Bit.Setup +{ + public class Context + { + private const string ConfigPath = "/bitwarden/config.yml"; + + public string[] Args { get; set; } + public IDictionary Parameters { get; set; } + public string OutputDir { get; set; } = "/etc/bitwarden"; + public string HostOS { get; set; } = "win"; + public string CoreVersion { get; set; } = "latest"; + public string WebVersion { get; set; } = "latest"; + public Installation Install { get; set; } = new Installation(); + public Configuration Config { get; set; } = new Configuration(); + + public void LoadConfiguration() + { + if(!File.Exists(ConfigPath)) + { + // Looks like updating from older version. Try to create config file. + var url = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault"); + if(!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + Console.WriteLine("Unable to determine existing installation url."); + return; + } + Config.Url = url; + + var push = Helpers.GetValueFronEnvFile("global", "globalSettings__pushRelayBaseUri"); + Config.PushNotifications = push != "REPLACE"; + + var composeFile = "/bitwarden/docker/docker-compose.yml"; + if(File.Exists(composeFile)) + { + var fileLines = File.ReadAllLines(composeFile); + foreach(var line in fileLines) + { + if(!line.StartsWith("# Parameter:")) + { + continue; + } + + var paramParts = line.Split("="); + if(paramParts.Length < 2) + { + continue; + } + + if(paramParts[0] == "# Parameter:MssqlDataDockerVolume" && + bool.TryParse(paramParts[1], out var mssqlDataDockerVolume)) + { + Config.DatabaseDockerVolume = mssqlDataDockerVolume; + continue; + } + + if(paramParts[0] == "# Parameter:HttpPort" && int.TryParse(paramParts[1], out var httpPort)) + { + Config.HttpPort = httpPort == 0 ? null : httpPort.ToString(); + continue; + } + + if(paramParts[0] == "# Parameter:HttpsPort" && int.TryParse(paramParts[1], out var httpsPort)) + { + Config.HttpsPort = httpsPort == 0 ? null : httpsPort.ToString(); + continue; + } + } + } + + var nginxFile = "/bitwarden/nginx/default.conf"; + if(File.Exists(nginxFile)) + { + var selfSigned = false; + var diffieHellman = false; + var trusted = false; + var fileLines = File.ReadAllLines(nginxFile); + foreach(var line in fileLines) + { + if(!line.StartsWith("# Parameter:")) + { + continue; + } + + var paramParts = line.Split("="); + if(paramParts.Length < 2) + { + continue; + } + + if(paramParts[0] == "# Parameter:Ssl" && bool.TryParse(paramParts[1], out var ssl)) + { + Config.Ssl = ssl; + continue; + } + + if(paramParts[0] == "# Parameter:LetsEncrypt" && bool.TryParse(paramParts[1], out var le)) + { + Config.SslManagedLetsEncrypt = le; + continue; + } + + if(paramParts[0] == "# Parameter:SelfSignedSsl" && bool.TryParse(paramParts[1], out var self)) + { + selfSigned = self; + return; + } + + if(paramParts[0] == "# Parameter:DiffieHellman" && bool.TryParse(paramParts[1], out var dh)) + { + diffieHellman = dh; + return; + } + + if(paramParts[0] == "# Parameter:Trusted" && bool.TryParse(paramParts[1], out var trust)) + { + trusted = trust; + return; + } + } + + if(Config.SslManagedLetsEncrypt) + { + Config.Ssl = true; + } + else if(Config.Ssl) + { + var sslPath = selfSigned ? $"/etc/ssl/self/{Config.Domain}" : $"/etc/ssl/{Config.Domain}"; + Config.SslCertificatePath = string.Concat(sslPath, "/", "certificate.crt"); + Config.SslKeyPath = string.Concat(sslPath, "/", "private.key"); + if(trusted) + { + Config.SslCaPath = string.Concat(sslPath, "/", "ca.crt"); + } + if(diffieHellman) + { + Config.SslDiffieHellmanPath = string.Concat(sslPath, "/", "dhparam.pem"); + } + } + } + + SaveConfiguration(); + } + + var configText = File.ReadAllText(ConfigPath); + var deserializer = new DeserializerBuilder() + .WithNamingConvention(new UnderscoredNamingConvention()) + .Build(); + Config = deserializer.Deserialize(configText); + } + + public void SaveConfiguration() + { + if(Config == null) + { + throw new Exception("Config is null."); + } + var serializer = new SerializerBuilder() + .EmitDefaults() + .WithNamingConvention(new UnderscoredNamingConvention()) + .WithTypeInspector(inner => new CommentGatheringTypeInspector(inner)) + .WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor)) + .Build(); + var yaml = serializer.Serialize(Config); + Directory.CreateDirectory("/bitwarden/"); + using(var sw = File.CreateText(ConfigPath)) + { + sw.Write(yaml); + } + } + + public class Installation + { + public Guid InstallationId { get; set; } + public string InstallationKey { get; set; } + public bool DiffieHellman { get; set; } + public bool Trusted { get; set; } + public bool SelfSignedCert { get; set; } + public string IdentityCertPassword { get; set; } + public string Domain { get; set; } + } + + public class Configuration + { + [Description("Full URL for accessing the installation from a browser. (Required)")] + public string Url { get; set; } = "https://localhost"; + + [Description("Auto-generate the `./docker/docker-compose.yml` config file.\n" + + "WARNING: Disabling generated config files can break future updates. You will be responsible\n" + + "for maintaining this config file.")] + public bool GenerateComposeConfig { get; set; } = true; + + [Description("Auto-generate the `./nginx/default.conf` file.\n" + + "WARNING: Disabling generated config files can break future updates. You will be responsible\n" + + "for maintaining this config file.")] + public bool GenerateNginxConfig { get; set; } = true; + + [Description("Compose file port mapping for HTTP. Leave empty for remove the port mapping.")] + public string HttpPort { get; set; } = "80"; + + [Description("Compose file port mapping for HTTPS. Leave empty for remove the port mapping.")] + public string HttpsPort { get; set; } = "443"; + + [Description("Set up the Nginx config file for SSL.")] + public bool Ssl { get; set; } = true; + + [Description("Installation uses a managed Let's Encrypt certificate.")] + public bool SslManagedLetsEncrypt { get; set; } + + [Description("The actual certificate. (Required if using SSL without managed Let's Encrypt)\n" + + "Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")] + public string SslCertificatePath { get; set; } + + [Description("The certificate's private key. (Required if using SSL without managed Let's Encrypt)\n" + + "Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")] + public string SslKeyPath { get; set; } + + [Description("If the certificate is trusted by a CA, you should provide the CA's certificate.\n" + + "Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")] + public string SslCaPath { get; set; } + + [Description("Diffie Hellman ephemeral parameters\n" + + "Learn more: https://security.stackexchange.com/q/94390/79072\n" + + "Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")] + public string SslDiffieHellmanPath { get; set; } + + [Description("Communicate with the Bitwarden push relay service (push.bitwarden.com) for mobile app live sync.")] + public bool PushNotifications { get; set; } = true; + + [Description("Use a docker volume instead of a host-mapped volume for the persisted database.\n" + + "WARNING: Changing this value will cause you to lose access to the existing persisted database.")] + public bool DatabaseDockerVolume { get; set; } + + [YamlIgnore] + public string Domain + { + get + { + if(Uri.TryCreate(Url, UriKind.Absolute, out var uri)) + { + return uri.Host; + } + return null; + } + } + } + } +} diff --git a/util/Setup/DockerComposeBuilder.cs b/util/Setup/DockerComposeBuilder.cs index 0e6bce73e..8e1162e4a 100644 --- a/util/Setup/DockerComposeBuilder.cs +++ b/util/Setup/DockerComposeBuilder.cs @@ -5,245 +5,58 @@ namespace Bit.Setup { public class DockerComposeBuilder { - public DockerComposeBuilder(string os, string webVersion, string coreVersion) - { - MssqlDataDockerVolume = os == "mac"; + private readonly Context _context; - if(!string.IsNullOrWhiteSpace(webVersion)) - { - WebVersion = webVersion; - } - if(!string.IsNullOrWhiteSpace(coreVersion)) - { - CoreVersion = coreVersion; - } + public DockerComposeBuilder(Context context) + { + _context = context; } - public bool MssqlDataDockerVolume { get; private set; } - public int HttpPort { get; private set; } - public int HttpsPort { get; private set; } - public string CoreVersion { get; private set; } = "latest"; - public string WebVersion { get; private set; } = "latest"; - - public void BuildForInstaller(int httpPort, int httpsPort) + public void BuildForInstaller() { - if(httpPort != default(int)) - { - HttpPort = httpPort; - } - - if(httpsPort != default(int)) - { - HttpsPort = httpsPort; - } - + _context.Config.DatabaseDockerVolume = _context.HostOS == "mac"; Build(); } public void BuildForUpdater() { - var composeFile = "/bitwarden/docker/docker-compose.yml"; - if(File.Exists(composeFile)) - { - var fileLines = File.ReadAllLines(composeFile); - foreach(var line in fileLines) - { - if(!line.StartsWith("# Parameter:")) - { - continue; - } - - var paramParts = line.Split("="); - if(paramParts.Length < 2) - { - continue; - } - - if(paramParts[0] == "# Parameter:MssqlDataDockerVolume" && - bool.TryParse(paramParts[1], out var mssqlDataDockerVolume)) - { - MssqlDataDockerVolume = mssqlDataDockerVolume; - continue; - } - - if(paramParts[0] == "# Parameter:HttpPort" && int.TryParse(paramParts[1], out var httpPort)) - { - HttpPort = httpPort; - continue; - } - - if(paramParts[0] == "# Parameter:HttpsPort" && int.TryParse(paramParts[1], out var httpsPort)) - { - HttpsPort = httpsPort; - continue; - } - } - } - Build(); } private void Build() { - Console.WriteLine("Building docker-compose.yml."); Directory.CreateDirectory("/bitwarden/docker/"); + Console.WriteLine("Building docker-compose.yml."); + if(!_context.Config.GenerateComposeConfig) + { + Console.WriteLine("...skipped"); + return; + } + + var template = Helpers.ReadTemplate("DockerCompose"); + var model = new TemplateModel(_context); using(var sw = File.CreateText("/bitwarden/docker/docker-compose.yml")) { - sw.Write($@"# https://docs.docker.com/compose/compose-file/ -# Parameter:MssqlDataDockerVolume={MssqlDataDockerVolume} -# Parameter:HttpPort={HttpPort} -# Parameter:HttpsPort={HttpsPort} -# Parameter:CoreVersion={CoreVersion} -# Parameter:WebVersion={WebVersion} - -version: '3' - -services: - mssql: - image: bitwarden/mssql:{CoreVersion} - container_name: bitwarden-mssql - restart: always - volumes:"); - - if(MssqlDataDockerVolume) - { - sw.Write(@" - - mssql_data:/var/opt/mssql/data"); - } - else - { - sw.Write(@" - - ../mssql/data:/var/opt/mssql/data"); - } - - sw.Write($@" - - ../logs/mssql:/var/opt/mssql/log - - ../mssql/backups:/etc/bitwarden/mssql/backups - env_file: - - mssql.env - - ../env/uid.env - - ../env/mssql.override.env - - web: - image: bitwarden/web:{WebVersion} - container_name: bitwarden-web - restart: always - volumes: - - ../web:/etc/bitwarden/web - env_file: - - global.env - - ../env/uid.env - - attachments: - image: bitwarden/attachments:{CoreVersion} - container_name: bitwarden-attachments - restart: always - volumes: - - ../core/attachments:/etc/bitwarden/core/attachments - env_file: - - global.env - - ../env/uid.env - - api: - image: bitwarden/api:{CoreVersion} - container_name: bitwarden-api - restart: always - volumes: - - ../core:/etc/bitwarden/core - - ../ca-certificates:/etc/bitwarden/ca-certificates - - ../logs/api:/etc/bitwarden/logs - env_file: - - global.env - - ../env/uid.env - - ../env/global.override.env - - identity: - image: bitwarden/identity:{CoreVersion} - container_name: bitwarden-identity - restart: always - volumes: - - ../identity:/etc/bitwarden/identity - - ../core:/etc/bitwarden/core - - ../ca-certificates:/etc/bitwarden/ca-certificates - - ../logs/identity:/etc/bitwarden/logs - env_file: - - global.env - - ../env/uid.env - - ../env/global.override.env - - admin: - image: bitwarden/admin:{CoreVersion} - container_name: bitwarden-admin - restart: always - volumes: - - ../core:/etc/bitwarden/core - - ../ca-certificates:/etc/bitwarden/ca-certificates - - ../logs/admin:/etc/bitwarden/logs - env_file: - - global.env - - ../env/uid.env - - ../env/global.override.env - - icons: - image: bitwarden/icons:{CoreVersion} - container_name: bitwarden-icons - restart: always - volumes: - - ../ca-certificates:/etc/bitwarden/ca-certificates - - ../logs/icons:/etc/bitwarden/logs - env_file: - - global.env - - ../env/uid.env - - notifications: - image: bitwarden/notifications:{CoreVersion} - container_name: bitwarden-notifications - restart: always - volumes: - - ../ca-certificates:/etc/bitwarden/ca-certificates - - ../logs/notifications:/etc/bitwarden/logs - env_file: - - global.env - - ../env/uid.env - - ../env/global.override.env - - nginx: - image: bitwarden/nginx:{CoreVersion} - container_name: bitwarden-nginx - restart: always - ports:"); - - if(HttpPort != default(int)) - { - sw.Write($@" - - '{HttpPort}:8080'"); - } - - if(HttpsPort != default(int)) - { - sw.Write($@" - - '{HttpsPort}:8443'"); - } - - sw.Write($@" - volumes: - - ../nginx:/etc/bitwarden/nginx - - ../letsencrypt:/etc/letsencrypt - - ../ssl:/etc/ssl - - ../logs/nginx:/var/log/nginx - env_file: - - ../env/uid.env"); - - if(MssqlDataDockerVolume) - { - sw.Write(@" -volumes: - mssql_data:"); - } - - // New line at end of file. - sw.Write("\n"); + sw.Write(template(model)); } } + + public class TemplateModel + { + public TemplateModel(Context context) + { + MssqlDataDockerVolume = context.Config.DatabaseDockerVolume; + HttpPort = context.Config.HttpPort; + HttpsPort = context.Config.HttpsPort; + CoreVersion = context.CoreVersion; + WebVersion = context.WebVersion; + } + + public bool MssqlDataDockerVolume { get; set; } + public string HttpPort { get; set; } + public string HttpsPort { get; set; } + public string CoreVersion { get; set; } = "latest"; + public string WebVersion { get; set; } = "latest"; + } } } diff --git a/util/Setup/EnvironmentFileBuilder.cs b/util/Setup/EnvironmentFileBuilder.cs index e3768ea83..2ee963d83 100644 --- a/util/Setup/EnvironmentFileBuilder.cs +++ b/util/Setup/EnvironmentFileBuilder.cs @@ -1,62 +1,95 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; namespace Bit.Setup { public class EnvironmentFileBuilder { + private readonly Context _context; + private IDictionary _globalValues; private IDictionary _mssqlValues; + private IDictionary _globalOverrideValues; + private IDictionary _mssqlOverrideValues; - public string Url { get; set; } = "https://localhost"; - public string Domain { get; set; } = "localhost"; - public string IdentityCertPassword { get; set; } = "REPLACE"; - public Guid? InstallationId { get; set; } - public string InstallationKey { get; set; } - public bool Push { get; set; } - public string DatabasePassword { get; set; } = "REPLACE"; - public string OutputDirectory { get; set; } = "."; + public EnvironmentFileBuilder(Context context) + { + _context = context; + _globalValues = new Dictionary + { + ["ASPNETCORE_ENVIRONMENT"] = "Production", + ["globalSettings__selfHosted"] = "true", + ["globalSettings__baseServiceUri__vault"] = "http://localhost", + ["globalSettings__baseServiceUri__api"] = "http://localhost/api", + ["globalSettings__baseServiceUri__identity"] = "http://localhost/identity", + ["globalSettings__baseServiceUri__admin"] = "http://localhost/admin", + ["globalSettings__baseServiceUri__notifications"] = "http://localhost/notifications", + ["globalSettings__baseServiceUri__internalNotifications"] = "http://notifications:5000", + ["globalSettings__baseServiceUri__internalAdmin"] = "http://admin:5000", + ["globalSettings__baseServiceUri__internalIdentity"] = "http://identity:5000", + ["globalSettings__baseServiceUri__internalApi"] = "http://api:5000", + ["globalSettings__baseServiceUri__internalVault"] = "http://web:5000", + ["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com", + ["globalSettings__installation__identityUri"] = "https://identity.bitwarden.com", + }; + _mssqlValues = new Dictionary + { + ["ACCEPT_EULA"] = "Y", + ["MSSQL_PID"] = "Express", + ["SA_PASSWORD"] = "SECRET", + }; + } public void BuildForInstaller() { Directory.CreateDirectory("/bitwarden/env/"); - Init(true); + Init(); Build(); } public void BuildForUpdater() { - Init(false); - LoadExistingValues(_globalValues, "/bitwarden/env/global.override.env"); - LoadExistingValues(_mssqlValues, "/bitwarden/env/mssql.override.env"); + Init(); + LoadExistingValues(_globalOverrideValues, "/bitwarden/env/global.override.env"); + LoadExistingValues(_mssqlOverrideValues, "/bitwarden/env/mssql.override.env"); + + if(_context.Config.PushNotifications && + _globalOverrideValues.ContainsKey("globalSettings__pushRelayBaseUri") && + _globalOverrideValues["globalSettings__pushRelayBaseUri"] == "REPLACE") + { + _globalOverrideValues.Remove("globalSettings__pushRelayBaseUri"); + } + Build(); } - private void Init(bool forInstall) + private void Init() { - var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", DatabasePassword); - _globalValues = new Dictionary + var dbPassword = Helpers.SecureRandomString(32); + var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", dbPassword); + _globalOverrideValues = new Dictionary { - ["globalSettings__baseServiceUri__vault"] = Url, - ["globalSettings__baseServiceUri__api"] = $"{Url}/api", - ["globalSettings__baseServiceUri__identity"] = $"{Url}/identity", - ["globalSettings__baseServiceUri__admin"] = $"{Url}/admin", - ["globalSettings__baseServiceUri__notifications"] = $"{Url}/notifications", - ["globalSettings__sqlServer__connectionString"] = $"\"{ dbConnectionString }\"", - ["globalSettings__identityServer__certificatePassword"] = IdentityCertPassword, - ["globalSettings__attachment__baseDirectory"] = $"{OutputDirectory}/core/attachments", - ["globalSettings__attachment__baseUrl"] = $"{Url}/attachments", - ["globalSettings__dataProtection__directory"] = $"{OutputDirectory}/core/aspnet-dataprotection", - ["globalSettings__logDirectory"] = $"{OutputDirectory}/logs", - ["globalSettings__licenseDirectory"] = $"{OutputDirectory}/core/licenses", + ["globalSettings__baseServiceUri__vault"] = _context.Config.Url, + ["globalSettings__baseServiceUri__api"] = $"{_context.Config.Url}/api", + ["globalSettings__baseServiceUri__identity"] = $"{_context.Config.Url}/identity", + ["globalSettings__baseServiceUri__admin"] = $"{_context.Config.Url}/admin", + ["globalSettings__baseServiceUri__notifications"] = $"{_context.Config.Url}/notifications", + ["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString}\"", + ["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword, + ["globalSettings__attachment__baseDirectory"] = $"{_context.OutputDir}/core/attachments", + ["globalSettings__attachment__baseUrl"] = $"{_context.Config.Url}/attachments", + ["globalSettings__dataProtection__directory"] = $"{_context.OutputDir}/core/aspnet-dataprotection", + ["globalSettings__logDirectory"] = $"{_context.OutputDir}/logs", + ["globalSettings__licenseDirectory"] = $"{_context.OutputDir}/core/licenses", ["globalSettings__internalIdentityKey"] = Helpers.SecureRandomString(64, alpha: true, numeric: true), ["globalSettings__duo__aKey"] = Helpers.SecureRandomString(64, alpha: true, numeric: true), - ["globalSettings__installation__id"] = InstallationId?.ToString(), - ["globalSettings__installation__key"] = InstallationKey, + ["globalSettings__installation__id"] = _context.Install?.InstallationId.ToString(), + ["globalSettings__installation__key"] = _context.Install?.InstallationKey, ["globalSettings__yubico__clientId"] = "REPLACE", ["globalSettings__yubico__key"] = "REPLACE", - ["globalSettings__mail__replyToEmail"] = $"no-reply@{Domain}", + ["globalSettings__mail__replyToEmail"] = $"no-reply@{_context.Config.Domain}", ["globalSettings__mail__smtp__host"] = "REPLACE", ["globalSettings__mail__smtp__username"] = "REPLACE", ["globalSettings__mail__smtp__password"] = "REPLACE", @@ -67,16 +100,16 @@ namespace Bit.Setup ["adminSettings__admins"] = string.Empty, }; - if(forInstall && !Push) + if(!_context.Config.PushNotifications) { - _globalValues.Add("globalSettings__pushRelayBaseUri", "REPLACE"); + _globalOverrideValues.Add("globalSettings__pushRelayBaseUri", "REPLACE"); } - _mssqlValues = new Dictionary + _mssqlOverrideValues = new Dictionary { ["ACCEPT_EULA"] = "Y", ["MSSQL_PID"] = "Express", - ["SA_PASSWORD"] = DatabasePassword, + ["SA_PASSWORD"] = dbPassword, }; } @@ -120,59 +153,34 @@ namespace Bit.Setup private void Build() { + var template = Helpers.ReadTemplate("EnvironmentFile"); + Console.WriteLine("Building docker environment files."); Directory.CreateDirectory("/bitwarden/docker/"); using(var sw = File.CreateText("/bitwarden/docker/global.env")) { - sw.Write($@"ASPNETCORE_ENVIRONMENT=Production -globalSettings__selfHosted=true -globalSettings__baseServiceUri__vault=http://localhost -globalSettings__baseServiceUri__api=http://localhost/api -globalSettings__baseServiceUri__identity=http://localhost/identity -globalSettings__baseServiceUri__admin=http://localhost/admin -globalSettings__baseServiceUri__notifications=http://localhost/notifications -globalSettings__baseServiceUri__internalNotifications=http://notifications:5000 -globalSettings__baseServiceUri__internalAdmin=http://admin:5000 -globalSettings__baseServiceUri__internalIdentity=http://identity:5000 -globalSettings__baseServiceUri__internalApi=http://api:5000 -globalSettings__baseServiceUri__internalVault=http://web:5000 -globalSettings__pushRelayBaseUri=https://push.bitwarden.com -globalSettings__installation__identityUri=https://identity.bitwarden.com -"); + sw.Write(template(new TemplateModel(_globalValues))); } - Helpers.Exec("chmod 600 /bitwarden/docker/global.env"); using(var sw = File.CreateText("/bitwarden/docker/mssql.env")) { - sw.Write($@"ACCEPT_EULA=Y -MSSQL_PID=Express -SA_PASSWORD=SECRET -"); + sw.Write(template(new TemplateModel(_mssqlValues))); } - Helpers.Exec("chmod 600 /bitwarden/docker/mssql.env"); Console.WriteLine("Building docker environment override files."); - Directory.CreateDirectory(" /bitwarden/env/"); + Directory.CreateDirectory("/bitwarden/env/"); using(var sw = File.CreateText("/bitwarden/env/global.override.env")) { - foreach(var item in _globalValues) - { - sw.WriteLine($"{item.Key}={item.Value}"); - } + sw.Write(template(new TemplateModel(_globalOverrideValues))); } - Helpers.Exec("chmod 600 /bitwarden/env/global.override.env"); using(var sw = File.CreateText("/bitwarden/env/mssql.override.env")) { - foreach(var item in _mssqlValues) - { - sw.WriteLine($"{item.Key}={item.Value}"); - } + sw.Write(template(new TemplateModel(_mssqlOverrideValues))); } - Helpers.Exec("chmod 600 /bitwarden/env/mssql.override.env"); // Empty uid env file. Only used on Linux hosts. @@ -181,5 +189,21 @@ SA_PASSWORD=SECRET using(var sw = File.CreateText("/bitwarden/env/uid.env")) { } } } + + public class TemplateModel + { + public TemplateModel(IEnumerable> variables) + { + Variables = variables.Select(v => new Kvp { Key = v.Key, Value = v.Value }); + } + + public IEnumerable Variables { get; set; } + + public class Kvp + { + public string Key { get; set; } + public string Value { get; set; } + } + } } } diff --git a/util/Setup/Helpers.cs b/util/Setup/Helpers.cs index 3c6bc8f96..41525a4a2 100644 --- a/util/Setup/Helpers.cs +++ b/util/Setup/Helpers.cs @@ -2,6 +2,8 @@ using System.Data.SqlClient; using System.Diagnostics; using System.IO; +using System.Linq; +using System.Reflection; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -192,5 +194,21 @@ namespace Bit.Setup Console.WriteLine(); Console.ResetColor(); } + + public static Func ReadTemplate(string templateName) + { + var assembly = typeof(Helpers).GetTypeInfo().Assembly; + var fullTemplateName = $"Bit.Setup.Templates.{templateName}.hbs"; + if(!assembly.GetManifestResourceNames().Any(f => f == fullTemplateName)) + { + return null; + } + using(var s = assembly.GetManifestResourceStream(fullTemplateName)) + using(var sr = new StreamReader(s)) + { + var templateText = sr.ReadToEnd(); + return HandlebarsDotNet.Handlebars.Compile(templateText); + } + } } } diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs index 775f6d646..4d49e7fd9 100644 --- a/util/Setup/NginxConfigBuilder.cs +++ b/util/Setup/NginxConfigBuilder.cs @@ -16,177 +16,99 @@ namespace Bit.Setup "child-src 'self' https://*.duosecurity.com; frame-src 'self' https://*.duosecurity.com; " + "connect-src 'self' wss://{0} https://haveibeenpwned.com https://api.pwnedpasswords.com;"; - public NginxConfigBuilder(string domain, string url, bool ssl, bool selfSignedSsl, bool letsEncrypt, - bool trusted, bool diffieHellman) - { - Domain = domain; - Url = url; - Ssl = ssl; - SelfSignedSsl = selfSignedSsl; - LetsEncrypt = letsEncrypt; - Trusted = trusted; - DiffieHellman = diffieHellman; - } + private readonly Context _context; - public NginxConfigBuilder(string domain, string url) + public NginxConfigBuilder(Context context) { - Domain = domain; - Url = url; + _context = context; } - public bool Ssl { get; private set; } - public bool SelfSignedSsl { get; private set; } - public bool LetsEncrypt { get; private set; } - public string Domain { get; private set; } - public string Url { get; private set; } - public bool DiffieHellman { get; private set; } - public bool Trusted { get; private set; } - public void BuildForInstaller() { - Build(); + var model = new TemplateModel(_context); + if(model.Ssl && !_context.Config.SslManagedLetsEncrypt) + { + var sslPath = _context.Install.SelfSignedCert ? + $"/etc/ssl/self/{model.Domain}" : $"/etc/ssl/{model.Domain}"; + _context.Config.SslCertificatePath = model.CertificatePath = + string.Concat(sslPath, "/", "certificate.crt"); + _context.Config.SslKeyPath = model.KeyPath = + string.Concat(sslPath, "/", "private.key"); + if(_context.Install.Trusted) + { + _context.Config.SslCaPath = model.CaPath = + string.Concat(sslPath, "/", "ca.crt"); + } + if(_context.Install.DiffieHellman) + { + _context.Config.SslDiffieHellmanPath = model.DiffieHellmanPath = + string.Concat(sslPath, "/", "dhparam.pem"); + } + } + Build(model); } public void BuildForUpdater() { - if(File.Exists(ConfFile)) - { - var confContent = File.ReadAllText(ConfFile); - Ssl = confContent.Contains("ssl http2;"); - SelfSignedSsl = confContent.Contains("/etc/ssl/self/"); - LetsEncrypt = !SelfSignedSsl && confContent.Contains("/etc/letsencrypt/live/"); - DiffieHellman = confContent.Contains("/dhparam.pem;"); - Trusted = confContent.Contains("ssl_trusted_certificate "); - } - - Build(); + var model = new TemplateModel(_context); + Build(model); } - private void Build() + private void Build(TemplateModel model) { Directory.CreateDirectory("/bitwarden/nginx/"); - - var sslPath = LetsEncrypt ? $"/etc/letsencrypt/live/{Domain}" : - SelfSignedSsl ? $"/etc/ssl/self/{Domain}" : $"/etc/ssl/{Domain}"; - var certFile = LetsEncrypt ? "fullchain.pem" : "certificate.crt"; - var keyFile = LetsEncrypt ? "privkey.pem" : "private.key"; - var caFile = LetsEncrypt ? "fullchain.pem" : "ca.crt"; - Console.WriteLine("Building nginx config."); + if(!_context.Config.GenerateNginxConfig) + { + Console.WriteLine("...skipped"); + return; + } + + var template = Helpers.ReadTemplate("NginxConfig"); using(var sw = File.CreateText(ConfFile)) { - sw.WriteLine($@"# Config Parameters -# Parameter:Ssl={Ssl} -# Parameter:SelfSignedSsl={SelfSignedSsl} -# Parameter:LetsEncrypt={LetsEncrypt} -# Parameter:Domain={Domain} -# Parameter:Url={Url} -# Parameter:DiffieHellman={DiffieHellman} -# Parameter:Trusted={Trusted} + sw.WriteLine(template(model)); + } + } -server {{ - listen 8080 default_server; - listen [::]:8080 default_server; - server_name {Domain};"); + public class TemplateModel + { + public TemplateModel() { } + + public TemplateModel(Context context) + { + Ssl = context.Config.Ssl; + Domain = context.Config.Domain; + Url = context.Config.Url; if(Ssl) { - sw.WriteLine($@" return 301 {Url}$request_uri; -}} - -server {{ - listen 8443 ssl http2; - listen [::]:8443 ssl http2; - server_name {Domain}; - - ssl_certificate {sslPath}/{certFile}; - ssl_certificate_key {sslPath}/{keyFile}; - - ssl_session_timeout 30m; - ssl_session_cache shared:SSL:20m; - ssl_session_tickets off;"); - - if(DiffieHellman) + if(context.Config.SslManagedLetsEncrypt) { - sw.WriteLine($@" - # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits - ssl_dhparam {sslPath}/dhparam.pem;"); + var sslPath = $"/etc/letsencrypt/live/{Domain}"; + CertificatePath = CaPath = string.Concat(sslPath, "/", "fullchain.pem"); + KeyPath = string.Concat(sslPath, "/", "privkey.pem"); + DiffieHellmanPath = string.Concat(sslPath, "/", "dhparam.pem"); } - - sw.WriteLine($@" - # SSL protocol TLSv1.2 is allowed. Disabled SSLv3, TLSv1, and TLSv1.1 - ssl_protocols TLSv1.2; - # Enable most secure cipher suites only. - ssl_ciphers ""{SslCiphers}""; - # Enables server-side protection from BEAST attacks - ssl_prefer_server_ciphers on;"); - - if(Trusted) + else { - sw.WriteLine($@" - # OCSP Stapling --- - # Fetch OCSP records from URL in ssl_certificate and cache them - ssl_stapling on; - ssl_stapling_verify on; - - # Verify chain of trust of OCSP response using Root CA and Intermediate certs - ssl_trusted_certificate {sslPath}/{caFile}; - - resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s; - - # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age - add_header Strict-Transport-Security max-age=15768000;"); + CertificatePath = context.Config.SslCertificatePath; + KeyPath = context.Config.SslKeyPath; + CaPath = context.Config.SslCaPath; + DiffieHellmanPath = context.Config.SslDiffieHellmanPath; } } - - sw.WriteLine($@" - location / {{ - proxy_pass http://web:5000/; - # Security headers - #add_header X-Frame-Options SAMEORIGIN; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection ""1; mode=block""; - add_header Referrer-Policy same-origin; - add_header Content-Security-Policy ""{string.Format(ContentSecurityPolicy, Domain)}""; - }} - - location = /app-id.json {{ - proxy_pass http://web:5000/app-id.json; - proxy_hide_header Content-Type; - add_header Content-Type $fido_content_type; - }} - - location /attachments/ {{ - proxy_pass http://attachments:5000/; - }} - - location /api/ {{ - proxy_pass http://api:5000/; - }} - - location /identity/ {{ - proxy_pass http://identity:5000/; - }} - - location /icons/ {{ - proxy_pass http://icons:5000/; - }} - - location /notifications/ {{ - proxy_pass http://notifications:5000/; - }} - - location /notifications/hub {{ - proxy_pass http://notifications:5000/hub; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection $http_connection; - }} - - location /admin {{ - proxy_pass http://admin:5000; - }} -}}"); } + + public bool Ssl { get; set; } + public string Domain { get; set; } + public string Url { get; set; } + public string CertificatePath { get; set; } + public string KeyPath { get; set; } + public string CaPath { get; set; } + public string DiffieHellmanPath { get; set; } + public string ContentSecurityPolicy => string.Format(NginxConfigBuilder.ContentSecurityPolicy, Domain); + public string SslCiphers => NginxConfigBuilder.SslCiphers; } } } diff --git a/util/Setup/Program.cs b/util/Setup/Program.cs index 6a167635f..ee6984ede 100644 --- a/util/Setup/Program.cs +++ b/util/Setup/Program.cs @@ -5,48 +5,44 @@ using System.Collections.Generic; using System.Data.SqlClient; using System.Net.Http; using System.Reflection; -using System.IO; namespace Bit.Setup { public class Program { - private static string[] _args = null; - private static IDictionary _parameters = null; - private static Guid? _installationId = null; - private static string _installationKey = null; - private static string _hostOs = "win"; - private static string _coreVersion = "latest"; - private static string _webVersion = "latest"; + private static Context _context; public static void Main(string[] args) { Console.WriteLine(); + _context = new Context + { + Args = args + }; + ParseParameters(); - _args = args; - _parameters = ParseParameters(); - if(_parameters.ContainsKey("os")) + if(_context.Parameters.ContainsKey("os")) { - _hostOs = _parameters["os"]; + _context.HostOS = _context.Parameters["os"]; } - if(_parameters.ContainsKey("corev")) + if(_context.Parameters.ContainsKey("corev")) { - _coreVersion = _parameters["corev"]; + _context.CoreVersion = _context.Parameters["corev"]; } - if(_parameters.ContainsKey("webv")) + if(_context.Parameters.ContainsKey("webv")) { - _webVersion = _parameters["webv"]; + _context.WebVersion = _context.Parameters["webv"]; } - if(_parameters.ContainsKey("install")) + if(_context.Parameters.ContainsKey("install")) { Install(); } - else if(_parameters.ContainsKey("update")) + else if(_context.Parameters.ContainsKey("update")) { Update(); } - else if(_parameters.ContainsKey("printenv")) + else if(_context.Parameters.ContainsKey("printenv")) { PrintEnvironment(); } @@ -58,147 +54,46 @@ namespace Bit.Setup private static void Install() { - var outputDir = _parameters.ContainsKey("out") ? - _parameters["out"].ToLowerInvariant() : "/etc/bitwarden"; - var domain = _parameters.ContainsKey("domain") ? - _parameters["domain"].ToLowerInvariant() : "localhost"; - var letsEncrypt = _parameters.ContainsKey("letsencrypt") ? - _parameters["letsencrypt"].ToLowerInvariant() == "y" : false; + if(_context.Parameters.ContainsKey("letsencrypt")) + { + _context.Config.SslManagedLetsEncrypt = + _context.Parameters["letsencrypt"].ToLowerInvariant() == "y"; + } + if(_context.Parameters.ContainsKey("domain")) + { + _context.Install.Domain = _context.Parameters["domain"].ToLowerInvariant(); + } if(!ValidateInstallation()) { return; } - var ssl = letsEncrypt; - if(!letsEncrypt) - { - ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?"); - if(ssl) - { - Directory.CreateDirectory($"/bitwarden/ssl/{domain}/"); - var message = "Make sure 'certificate.crt' and 'private.key' are provided in the \n" + - "appropriate directory before running 'start' (see docs for info)."; - Helpers.ShowBanner("NOTE", message); - } - } + var certBuilder = new CertBuilder(_context); + certBuilder.BuildForInstall(); + + // Set the URL + _context.Config.Url = string.Format("http{0}://{1}", + _context.Config.Ssl ? "s" : string.Empty, _context.Install.Domain); - var identityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true); - var certBuilder = new CertBuilder(domain, identityCertPassword, letsEncrypt, ssl); - var selfSignedSsl = certBuilder.BuildForInstall(); - ssl = certBuilder.Ssl; // Ssl prop can get flipped during the build - - var sslTrusted = letsEncrypt; - var sslDiffieHellman = letsEncrypt; - if(ssl && !selfSignedSsl && !letsEncrypt) - { - sslDiffieHellman = Helpers.ReadQuestion("Use Diffie Hellman ephemeral parameters for SSL " + - "(requires dhparam.pem, see docs)?"); - sslTrusted = Helpers.ReadQuestion("Is this a trusted SSL certificate (requires ca.crt, see docs)?"); - } - - if(!ssl) - { - var message = "You are not using a SSL certificate. Bitwarden requires HTTPS to operate. \n" + - "You must front your installation with a HTTPS proxy. The web vault (and \n" + - "other Bitwarden apps) will not work properly without HTTPS."; - Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow); - } - else if(ssl && !sslTrusted) - { - var message = "You are using an untrusted SSL certificate. This certificate will not be \n" + - "trusted by Bitwarden client applications. You must add this certificate to \n" + - "the trusted store on each device or else you will receive errors when trying \n" + - "to connect to your installation."; - Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow); - } - - var url = $"https://{domain}"; - int httpPort = default(int), httpsPort = default(int); - if(Helpers.ReadQuestion("Do you want to use the default ports for HTTP (80) and HTTPS (443)?")) - { - httpPort = 80; - if(ssl) - { - httpsPort = 443; - } - } - else if(ssl) - { - httpsPort = 443; - if(int.TryParse(Helpers.ReadInput("HTTPS port").Trim(), out httpsPort) && httpsPort != 443) - { - url += (":" + httpsPort); - } - else - { - Console.WriteLine("Using default port."); - } - } - else - { - httpPort = 80; - if(!int.TryParse(Helpers.ReadInput("HTTP port").Trim(), out httpPort) && httpPort != 80) - { - Console.WriteLine("Using default port."); - } - } - - if(Helpers.ReadQuestion("Is your installation behind a reverse proxy?")) - { - if(Helpers.ReadQuestion("Do you use the default HTTPS port (443) on your reverse proxy?")) - { - url = $"https://{domain}"; - } - else - { - if(int.TryParse(Helpers.ReadInput("Proxy HTTPS port").Trim(), out var httpsReversePort) - && httpsReversePort != 443) - { - url += (":" + httpsReversePort); - } - else - { - Console.WriteLine("Using default port."); - url = $"https://{domain}"; - } - } - } - else if(!ssl) - { - Console.WriteLine("ERROR: You must use a reverse proxy if not using SSL."); - return; - } - - var push = Helpers.ReadQuestion("Do you want to use push notifications?"); - - var nginxBuilder = new NginxConfigBuilder(domain, url, ssl, selfSignedSsl, letsEncrypt, - sslTrusted, sslDiffieHellman); + var nginxBuilder = new NginxConfigBuilder(_context); nginxBuilder.BuildForInstaller(); - var environmentFileBuilder = new EnvironmentFileBuilder - { - DatabasePassword = Helpers.SecureRandomString(32), - Domain = domain, - IdentityCertPassword = identityCertPassword, - InstallationId = _installationId, - InstallationKey = _installationKey, - OutputDirectory = outputDir, - Push = push, - Url = url - }; + var environmentFileBuilder = new EnvironmentFileBuilder(_context); environmentFileBuilder.BuildForInstaller(); - var appIdBuilder = new AppIdBuilder(url); + var appIdBuilder = new AppIdBuilder(_context); appIdBuilder.Build(); - var dockerComposeBuilder = new DockerComposeBuilder(_hostOs, _webVersion, _coreVersion); - dockerComposeBuilder.BuildForInstaller(httpPort, httpsPort); + var dockerComposeBuilder = new DockerComposeBuilder(_context); + dockerComposeBuilder.BuildForInstaller(); + + _context.SaveConfiguration(); } private static void Update() { - if(_parameters.ContainsKey("db")) + if(_context.Parameters.ContainsKey("db")) { MigrateDatabase(); } @@ -210,12 +105,12 @@ namespace Bit.Setup private static void PrintEnvironment() { - var vaultUrl = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault"); + _context.LoadConfiguration(); Console.WriteLine("\nBitwarden is up and running!"); Console.WriteLine("==================================================="); - Console.WriteLine("\nvisit {0}", vaultUrl); + Console.WriteLine("\nvisit {0}", _context.Config.Url); Console.Write("to update, run "); - if(_hostOs == "win") + if(_context.HostOS == "win") { Console.Write("'.\\bitwarden.ps1 -updateself' and then '.\\bitwarden.ps1 -update'"); } @@ -296,13 +191,13 @@ namespace Bit.Setup return false; } - _installationId = installationidGuid; - _installationKey = Helpers.ReadInput("Enter your installation key"); + _context.Install.InstallationId = installationidGuid; + _context.Install.InstallationKey = Helpers.ReadInput("Enter your installation key"); try { - var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" + _installationId) - .GetAwaiter().GetResult(); + var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" + + _context.Install.InstallationId).GetAwaiter().GetResult(); if(!response.IsSuccessStatusCode) { @@ -337,42 +232,35 @@ namespace Bit.Setup private static void RebuildConfigs() { - var environmentFileBuilder = new EnvironmentFileBuilder(); + _context.LoadConfiguration(); + + var environmentFileBuilder = new EnvironmentFileBuilder(_context); environmentFileBuilder.BuildForUpdater(); - var url = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault"); - if(!Uri.TryCreate(url, UriKind.Absolute, out Uri uri)) - { - Console.WriteLine("Unable to determine existing installation url."); - return; - } - - var domain = uri.Host; - - var nginxBuilder = new NginxConfigBuilder(domain, url); + var nginxBuilder = new NginxConfigBuilder(_context); nginxBuilder.BuildForUpdater(); - var appIdBuilder = new AppIdBuilder(url); + var appIdBuilder = new AppIdBuilder(_context); appIdBuilder.Build(); - var dockerComposeBuilder = new DockerComposeBuilder(_hostOs, _webVersion, _coreVersion); + var dockerComposeBuilder = new DockerComposeBuilder(_context); dockerComposeBuilder.BuildForUpdater(); + + _context.SaveConfiguration(); } - private static IDictionary ParseParameters() + private static void ParseParameters() { - var dict = new Dictionary(); - for(var i = 0; i < _args.Length; i = i + 2) + _context.Parameters = new Dictionary(); + for(var i = 0; i < _context.Args.Length; i = i + 2) { - if(!_args[i].StartsWith("-")) + if(!_context.Args[i].StartsWith("-")) { continue; } - dict.Add(_args[i].Substring(1), _args[i + 1]); + _context.Parameters.Add(_context.Args[i].Substring(1), _context.Args[i + 1]); } - - return dict; } } } diff --git a/util/Setup/Setup.csproj b/util/Setup/Setup.csproj index bbfe2dc8e..088733d22 100644 --- a/util/Setup/Setup.csproj +++ b/util/Setup/Setup.csproj @@ -9,12 +9,15 @@ + + + diff --git a/util/Setup/Templates/AppId.hbs b/util/Setup/Templates/AppId.hbs new file mode 100644 index 000000000..f726a55cd --- /dev/null +++ b/util/Setup/Templates/AppId.hbs @@ -0,0 +1,15 @@ +{ + "trustedFacets": [ + { + "version": { + "major": 1, + "minor": 0 + }, + "ids": [ + "{{{Url}}}", + "ios:bundle-id:com.8bit.bitwarden", + "android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI" + ] + } + ] +} \ No newline at end of file diff --git a/util/Setup/Templates/DockerCompose.hbs b/util/Setup/Templates/DockerCompose.hbs new file mode 100644 index 000000000..374b86732 --- /dev/null +++ b/util/Setup/Templates/DockerCompose.hbs @@ -0,0 +1,133 @@ +# https://docs.docker.com/compose/compose-file/ +# +# WARNING: This file is generated. Do not make changes to this file. +# They will be overwritten on update. If you want to make additions to +# this file, you can create a `docker-compose.override.yml` file in the +# same directory and it will be merged into this file at runtime. + +version: '3' + +services: + mssql: + image: bitwarden/mssql:{{{CoreVersion}}} + container_name: bitwarden-mssql + restart: always + volumes: +{{#if MssqlDataDockerVolume}} + - mssql_data:/var/opt/mssql/data +{{else}} + - ../mssql/data:/var/opt/mssql/data +{{/if}} + - ../logs/mssql:/var/opt/mssql/log + - ../mssql/backups:/etc/bitwarden/mssql/backups + env_file: + - mssql.env + - ../env/uid.env + - ../env/mssql.override.env + + web: + image: bitwarden/web:{{{WebVersion}}} + container_name: bitwarden-web + restart: always + volumes: + - ../web:/etc/bitwarden/web + env_file: + - global.env + - ../env/uid.env + + attachments: + image: bitwarden/attachments:{{{CoreVersion}}} + container_name: bitwarden-attachments + restart: always + volumes: + - ../core/attachments:/etc/bitwarden/core/attachments + env_file: + - global.env + - ../env/uid.env + + api: + image: bitwarden/api:{{{CoreVersion}}} + container_name: bitwarden-api + restart: always + volumes: + - ../core:/etc/bitwarden/core + - ../ca-certificates:/etc/bitwarden/ca-certificates + - ../logs/api:/etc/bitwarden/logs + env_file: + - global.env + - ../env/uid.env + - ../env/global.override.env + + identity: + image: bitwarden/identity:{{{CoreVersion}}} + container_name: bitwarden-identity + restart: always + volumes: + - ../identity:/etc/bitwarden/identity + - ../core:/etc/bitwarden/core + - ../ca-certificates:/etc/bitwarden/ca-certificates + - ../logs/identity:/etc/bitwarden/logs + env_file: + - global.env + - ../env/uid.env + - ../env/global.override.env + + admin: + image: bitwarden/admin:{{{CoreVersion}}} + container_name: bitwarden-admin + restart: always + volumes: + - ../core:/etc/bitwarden/core + - ../ca-certificates:/etc/bitwarden/ca-certificates + - ../logs/admin:/etc/bitwarden/logs + env_file: + - global.env + - ../env/uid.env + - ../env/global.override.env + + icons: + image: bitwarden/icons:{{{CoreVersion}}} + container_name: bitwarden-icons + restart: always + volumes: + - ../ca-certificates:/etc/bitwarden/ca-certificates + - ../logs/icons:/etc/bitwarden/logs + env_file: + - global.env + - ../env/uid.env + + notifications: + image: bitwarden/notifications:{{{CoreVersion}}} + container_name: bitwarden-notifications + restart: always + volumes: + - ../ca-certificates:/etc/bitwarden/ca-certificates + - ../logs/notifications:/etc/bitwarden/logs + env_file: + - global.env + - ../env/uid.env + - ../env/global.override.env + + nginx: + image: bitwarden/nginx:{{{CoreVersion}}} + container_name: bitwarden-nginx + restart: always + ports: +{{#if HttpPort}} + - '{{{HttpPort}}}:8080' +{{/if}} +{{#if HttpsPort}} + - '{{{HttpsPort}}}:8443' +{{/if}} + volumes: + - ../nginx:/etc/bitwarden/nginx + - ../letsencrypt:/etc/letsencrypt + - ../ssl:/etc/ssl + - ../logs/nginx:/var/log/nginx + env_file: + - ../env/uid.env +{{#if MssqlDataDockerVolume}} + +volumes: + mssql_data: +{{/if}} diff --git a/util/Setup/Templates/EnvironmentFile.hbs b/util/Setup/Templates/EnvironmentFile.hbs new file mode 100644 index 000000000..a1d0cf4f8 --- /dev/null +++ b/util/Setup/Templates/EnvironmentFile.hbs @@ -0,0 +1,3 @@ +{{#each Variables}} +{{{Key}}}={{{Value}}} +{{/each}} diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs new file mode 100644 index 000000000..6a0b00b12 --- /dev/null +++ b/util/Setup/Templates/NginxConfig.hbs @@ -0,0 +1,99 @@ +# WARNING: This file is generated. Do not make changes to this file. +# They will be overwritten on update. + +server { + listen 8080 default_server; + listen [::]:8080 default_server; + server_name {{{Domain}}}; +{{#if Ssl}} + + return 301 {{{Url}}}$request_uri; +} + +server { + listen 8443 ssl http2; + listen [::]:8443 ssl http2; + server_name {{{Domain}}}; + + ssl_certificate {{{CertificatePath}}}; + ssl_certificate_key {{{KeyPath}}}; + ssl_session_timeout 30m; + ssl_session_cache shared:SSL:20m; + ssl_session_tickets off; +{{#if DiffieHellmanPath}} + + # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits + ssl_dhparam {{{DiffieHellmanPath}}}; +{{/if}} + + # SSL protocol TLSv1.2 is allowed. Disabled SSLv3, TLSv1, and TLSv1.1 + ssl_protocols TLSv1.2; + # Enable most secure cipher suites only. + ssl_ciphers "{{{SslCiphers}}}"; + # Enables server-side protection from BEAST attacks + ssl_prefer_server_ciphers on; +{{#if CaPath}} + + # OCSP Stapling --- + # Fetch OCSP records from URL in ssl_certificate and cache them + ssl_stapling on; + ssl_stapling_verify on; + + # Verify chain of trust of OCSP response using Root CA and Intermediate certs + ssl_trusted_certificate {{{CaPath}}}; + resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s; +{{/if}} +{{/if}} + + # Security headers + add_header Referrer-Policy same-origin; + #add_header X-Frame-Options SAMEORIGIN; +{{#if Ssl}} + add_header X-Content-Type-Options nosniff; + # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age + add_header Strict-Transport-Security max-age=15768000; +{{/if}} + + location / { + proxy_pass http://web:5000/; + # Security headers + add_header X-XSS-Protection "1; mode=block"; + add_header Content-Security-Policy "{{{ContentSecurityPolicy}}}"; + } + + location = /app-id.json { + proxy_pass http://web:5000/app-id.json; + proxy_hide_header Content-Type; + add_header Content-Type $fido_content_type; + } + + location /attachments/ { + proxy_pass http://attachments:5000/; + } + + location /api/ { + proxy_pass http://api:5000/; + } + + location /identity/ { + proxy_pass http://identity:5000/; + } + + location /icons/ { + proxy_pass http://icons:5000/; + } + + location /notifications/ { + proxy_pass http://notifications:5000/; + } + + location /notifications/hub { + proxy_pass http://notifications:5000/hub; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + } + + location /admin { + proxy_pass http://admin:5000; + } +} diff --git a/util/Setup/YamlComments.cs b/util/Setup/YamlComments.cs new file mode 100644 index 000000000..22075b89e --- /dev/null +++ b/util/Setup/YamlComments.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.ObjectGraphVisitors; +using YamlDotNet.Serialization.TypeInspectors; + +// ref: https://github.com/aaubry/YamlDotNet/issues/152#issuecomment-349034754 + +namespace Bit.Setup +{ + public class CommentGatheringTypeInspector : TypeInspectorSkeleton + { + private readonly ITypeInspector _innerTypeDescriptor; + + public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor) + { + _innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor)); + } + + public override IEnumerable GetProperties(Type type, object container) + { + return _innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d)); + } + + private sealed class CommentsPropertyDescriptor : IPropertyDescriptor + { + private readonly IPropertyDescriptor _baseDescriptor; + + public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor) + { + _baseDescriptor = baseDescriptor; + Name = baseDescriptor.Name; + } + + public string Name { get; set; } + public int Order { get; set; } + public Type Type => _baseDescriptor.Type; + public bool CanWrite => _baseDescriptor.CanWrite; + + public Type TypeOverride + { + get { return _baseDescriptor.TypeOverride; } + set { _baseDescriptor.TypeOverride = value; } + } + + public ScalarStyle ScalarStyle + { + get { return _baseDescriptor.ScalarStyle; } + set { _baseDescriptor.ScalarStyle = value; } + } + + public void Write(object target, object value) + { + _baseDescriptor.Write(target, value); + } + + public T GetCustomAttribute() where T : Attribute + { + return _baseDescriptor.GetCustomAttribute(); + } + + public IObjectDescriptor Read(object target) + { + var description = _baseDescriptor.GetCustomAttribute(); + return description != null ? + new CommentsObjectDescriptor(_baseDescriptor.Read(target), description.Description) : + _baseDescriptor.Read(target); + } + } + } + + public sealed class CommentsObjectDescriptor : IObjectDescriptor + { + private readonly IObjectDescriptor _innerDescriptor; + + public CommentsObjectDescriptor(IObjectDescriptor innerDescriptor, string comment) + { + _innerDescriptor = innerDescriptor; + Comment = comment; + } + + public string Comment { get; private set; } + public object Value => _innerDescriptor.Value; + public Type Type => _innerDescriptor.Type; + public Type StaticType => _innerDescriptor.StaticType; + public ScalarStyle ScalarStyle => _innerDescriptor.ScalarStyle; + } + + public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor + { + public CommentsObjectGraphVisitor(IObjectGraphVisitor nextVisitor) + : base(nextVisitor) { } + + public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context) + { + if(value is CommentsObjectDescriptor commentsDescriptor && commentsDescriptor.Comment != null) + { + context.Emit(new Comment(string.Empty, false)); + foreach(var comment in commentsDescriptor.Comment.Split(Environment.NewLine)) + { + context.Emit(new Comment(comment, false)); + } + } + return base.EnterMapping(key, value, context); + } + } +}