1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

Turn on file scoped namespaces (#2225)

This commit is contained in:
Justin Baur
2022-08-29 14:53:16 -04:00
committed by GitHub
parent 7c4521e0b4
commit 34fb4cca2a
1206 changed files with 73816 additions and 75022 deletions

View File

@ -1,34 +1,33 @@
namespace Bit.Setup
namespace Bit.Setup;
public class AppIdBuilder
{
public class AppIdBuilder
private readonly Context _context;
public AppIdBuilder(Context context)
{
private readonly Context _context;
_context = context;
}
public AppIdBuilder(Context context)
public void Build()
{
var model = new TemplateModel
{
_context = context;
}
Url = _context.Config.Url
};
public void Build()
// Needed for backwards compatability with migrated U2F tokens.
Helpers.WriteLine(_context, "Building FIDO U2F app id.");
Directory.CreateDirectory("/bitwarden/web/");
var template = Helpers.ReadTemplate("AppId");
using (var sw = File.CreateText("/bitwarden/web/app-id.json"))
{
var model = new TemplateModel
{
Url = _context.Config.Url
};
// Needed for backwards compatability with migrated U2F tokens.
Helpers.WriteLine(_context, "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(template(model));
}
}
public class TemplateModel
{
public string Url { get; set; }
sw.Write(template(model));
}
}
public class TemplateModel
{
public string Url { get; set; }
}
}

View File

@ -1,112 +1,111 @@
namespace Bit.Setup
{
public class CertBuilder
{
private readonly Context _context;
namespace Bit.Setup;
public CertBuilder(Context context)
public class CertBuilder
{
private readonly Context _context;
public CertBuilder(Context context)
{
_context = context;
}
public void BuildForInstall()
{
if (_context.Stub)
{
_context = context;
_context.Config.Ssl = true;
_context.Install.Trusted = true;
_context.Install.SelfSignedCert = false;
_context.Install.DiffieHellman = false;
_context.Install.IdentityCertPassword = "IDENTITY_CERT_PASSWORD";
return;
}
public void BuildForInstall()
_context.Config.Ssl = _context.Config.SslManagedLetsEncrypt;
if (!_context.Config.Ssl)
{
if (_context.Stub)
var skipSSL = _context.Parameters.ContainsKey("skip-ssl") && (_context.Parameters["skip-ssl"] == "true" || _context.Parameters["skip-ssl"] == "1");
if (!skipSSL)
{
_context.Config.Ssl = true;
_context.Install.Trusted = true;
_context.Install.SelfSignedCert = false;
_context.Install.DiffieHellman = false;
_context.Install.IdentityCertPassword = "IDENTITY_CERT_PASSWORD";
return;
}
_context.Config.Ssl = _context.Config.SslManagedLetsEncrypt;
if (!_context.Config.Ssl)
{
var skipSSL = _context.Parameters.ContainsKey("skip-ssl") && (_context.Parameters["skip-ssl"] == "true" || _context.Parameters["skip-ssl"] == "1");
if (!skipSSL)
_context.Config.Ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?");
if (_context.Config.Ssl)
{
_context.Config.Ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?");
if (_context.Config.Ssl)
{
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(_context, "NOTE", message);
}
else if (Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
{
Directory.CreateDirectory($"/bitwarden/ssl/self/{_context.Install.Domain}/");
Helpers.WriteLine(_context, "Generating self signed SSL certificate.");
_context.Config.Ssl = true;
_context.Install.Trusted = false;
_context.Install.SelfSignedCert = true;
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 36500 " +
$"-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:{_context.Install.Domain}\nbasicConstraints=CA:true')) " +
$"-subj \"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.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(_context, "NOTE", message);
}
else if (Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
{
Directory.CreateDirectory($"/bitwarden/ssl/self/{_context.Install.Domain}/");
Helpers.WriteLine(_context, "Generating self signed SSL certificate.");
_context.Config.Ssl = true;
_context.Install.Trusted = false;
_context.Install.SelfSignedCert = true;
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 36500 " +
$"-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:{_context.Install.Domain}\nbasicConstraints=CA:true')) " +
$"-subj \"/C=US/ST=California/L=Santa Barbara/O=Bitwarden Inc./OU=Bitwarden/CN={_context.Install.Domain}\"");
}
}
if (_context.Config.SslManagedLetsEncrypt)
{
_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)?");
}
Helpers.WriteLine(_context, "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 36500");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword}");
Helpers.WriteLine(_context);
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(_context, "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(_context, "WARNING", message, ConsoleColor.Yellow);
}
}
public void BuildForUpdater()
if (_context.Config.SslManagedLetsEncrypt)
{
if (_context.Config.EnableKeyConnector && !File.Exists("/bitwarden/key-connector/bwkc.pfx"))
{
Directory.CreateDirectory("/bitwarden/key-connector/");
var keyConnectorCertPassword = Helpers.GetValueFromEnvFile("key-connector",
"keyConnectorSettings__certificate__filesystemPassword");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key " +
"-out bwkc.crt -subj \"/CN=Bitwarden Key Connector\" -days 36500");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/key-connector/bwkc.pfx -inkey bwkc.key " +
$"-in bwkc.crt -passout pass:{keyConnectorCertPassword}");
}
_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)?");
}
Helpers.WriteLine(_context, "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 36500");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword}");
Helpers.WriteLine(_context);
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(_context, "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(_context, "WARNING", message, ConsoleColor.Yellow);
}
}
public void BuildForUpdater()
{
if (_context.Config.EnableKeyConnector && !File.Exists("/bitwarden/key-connector/bwkc.pfx"))
{
Directory.CreateDirectory("/bitwarden/key-connector/");
var keyConnectorCertPassword = Helpers.GetValueFromEnvFile("key-connector",
"keyConnectorSettings__certificate__filesystemPassword");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout bwkc.key " +
"-out bwkc.crt -subj \"/CN=Bitwarden Key Connector\" -days 36500");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/key-connector/bwkc.pfx -inkey bwkc.key " +
$"-in bwkc.crt -passout pass:{keyConnectorCertPassword}");
}
}
}

View File

@ -1,122 +1,121 @@
using System.ComponentModel;
using YamlDotNet.Serialization;
namespace Bit.Setup
namespace Bit.Setup;
public class Configuration
{
public class Configuration
[Description("Note: After making changes to this file you need to run the `rebuild` or `update`\n" +
"command for them to be applied.\n\n" +
"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\n" +
"responsible for maintaining this config file.\n" +
"Template: https://github.com/bitwarden/server/blob/master/util/Setup/Templates/DockerCompose.hbs")]
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\n" +
"responsible for maintaining this config file.\n" +
"Template: https://github.com/bitwarden/server/blob/master/util/Setup/Templates/NginxConfig.hbs")]
public bool GenerateNginxConfig { get; set; } = true;
[Description("Docker compose file port mapping for HTTP. Leave empty to remove the port mapping.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
public string HttpPort { get; set; } = "80";
[Description("Docker compose file port mapping for HTTPS. Leave empty to remove the port mapping.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
public string HttpsPort { get; set; } = "443";
[Description("Docker compose file version. Leave empty for default.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/compose-versioning/")]
public string ComposeVersion { get; set; }
[Description("Configure Nginx for Captcha.")]
public bool Captcha { get; set; } = false;
[Description("Configure Nginx for SSL.")]
public bool Ssl { get; set; } = true;
[Description("SSL versions used by Nginx (ssl_protocols). Leave empty for recommended default.\n" +
"Learn more: https://wiki.mozilla.org/Security/Server_Side_TLS")]
public string SslVersions { get; set; }
[Description("SSL ciphersuites used by Nginx (ssl_ciphers). Leave empty for recommended default.\n" +
"Learn more: https://wiki.mozilla.org/Security/Server_Side_TLS")]
public string SslCiphersuites { get; set; }
[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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/etc/ssl` within the container.")]
public string SslDiffieHellmanPath { get; set; }
[Description("Nginx Header Content-Security-Policy parameter\n" +
"WARNING: Reconfiguring this parameter may break features. By changing this parameter\n" +
"you become responsible for maintaining this value.")]
public string NginxHeaderContentSecurityPolicy { get; set; } = "default-src 'self'; style-src 'self' " +
"'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com https://www.gravatar.com; " +
"child-src 'self' https://*.duosecurity.com https://*.duofederal.com; " +
"frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; " +
"connect-src 'self' wss://{0} https://api.pwnedpasswords.com " +
"https://2fa.directory; object-src 'self' blob:;";
[Description("Communicate with the Bitwarden push relay service (push.bitwarden.com) for mobile\n" +
"app live sync.")]
public bool PushNotifications { get; set; } = true;
[Description("Use a docker volume (`mssql_data`) 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.\n" +
"Learn more: https://docs.docker.com/storage/volumes/")]
public bool DatabaseDockerVolume { get; set; }
[Description("Defines \"real\" IPs in nginx.conf. Useful for defining proxy servers that forward the \n" +
"client IP address.\n" +
"Learn more: https://nginx.org/en/docs/http/ngx_http_realip_module.html\n\n" +
"Defined as a dictionary, e.g.:\n" +
"real_ips: ['10.10.0.0/24', '172.16.0.0/16']")]
public List<string> RealIps { get; set; }
[Description("Enable Key Connector (https://bitwarden.com/help/article/deploy-key-connector)")]
public bool EnableKeyConnector { get; set; } = false;
[Description("Enable SCIM")]
public bool EnableScim { get; set; } = false;
[YamlIgnore]
public string Domain
{
[Description("Note: After making changes to this file you need to run the `rebuild` or `update`\n" +
"command for them to be applied.\n\n" +
"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\n" +
"responsible for maintaining this config file.\n" +
"Template: https://github.com/bitwarden/server/blob/master/util/Setup/Templates/DockerCompose.hbs")]
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\n" +
"responsible for maintaining this config file.\n" +
"Template: https://github.com/bitwarden/server/blob/master/util/Setup/Templates/NginxConfig.hbs")]
public bool GenerateNginxConfig { get; set; } = true;
[Description("Docker compose file port mapping for HTTP. Leave empty to remove the port mapping.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
public string HttpPort { get; set; } = "80";
[Description("Docker compose file port mapping for HTTPS. Leave empty to remove the port mapping.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/#ports")]
public string HttpsPort { get; set; } = "443";
[Description("Docker compose file version. Leave empty for default.\n" +
"Learn more: https://docs.docker.com/compose/compose-file/compose-versioning/")]
public string ComposeVersion { get; set; }
[Description("Configure Nginx for Captcha.")]
public bool Captcha { get; set; } = false;
[Description("Configure Nginx for SSL.")]
public bool Ssl { get; set; } = true;
[Description("SSL versions used by Nginx (ssl_protocols). Leave empty for recommended default.\n" +
"Learn more: https://wiki.mozilla.org/Security/Server_Side_TLS")]
public string SslVersions { get; set; }
[Description("SSL ciphersuites used by Nginx (ssl_ciphers). Leave empty for recommended default.\n" +
"Learn more: https://wiki.mozilla.org/Security/Server_Side_TLS")]
public string SslCiphersuites { get; set; }
[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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/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: Path uses the container's ssl directory. The `./ssl` host directory is mapped to\n" +
"`/etc/ssl` within the container.")]
public string SslDiffieHellmanPath { get; set; }
[Description("Nginx Header Content-Security-Policy parameter\n" +
"WARNING: Reconfiguring this parameter may break features. By changing this parameter\n" +
"you become responsible for maintaining this value.")]
public string NginxHeaderContentSecurityPolicy { get; set; } = "default-src 'self'; style-src 'self' " +
"'unsafe-inline'; img-src 'self' data: https://haveibeenpwned.com https://www.gravatar.com; " +
"child-src 'self' https://*.duosecurity.com https://*.duofederal.com; " +
"frame-src 'self' https://*.duosecurity.com https://*.duofederal.com; " +
"connect-src 'self' wss://{0} https://api.pwnedpasswords.com " +
"https://2fa.directory; object-src 'self' blob:;";
[Description("Communicate with the Bitwarden push relay service (push.bitwarden.com) for mobile\n" +
"app live sync.")]
public bool PushNotifications { get; set; } = true;
[Description("Use a docker volume (`mssql_data`) 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.\n" +
"Learn more: https://docs.docker.com/storage/volumes/")]
public bool DatabaseDockerVolume { get; set; }
[Description("Defines \"real\" IPs in nginx.conf. Useful for defining proxy servers that forward the \n" +
"client IP address.\n" +
"Learn more: https://nginx.org/en/docs/http/ngx_http_realip_module.html\n\n" +
"Defined as a dictionary, e.g.:\n" +
"real_ips: ['10.10.0.0/24', '172.16.0.0/16']")]
public List<string> RealIps { get; set; }
[Description("Enable Key Connector (https://bitwarden.com/help/article/deploy-key-connector)")]
public bool EnableKeyConnector { get; set; } = false;
[Description("Enable SCIM")]
public bool EnableScim { get; set; } = false;
[YamlIgnore]
public string Domain
get
{
get
if (Uri.TryCreate(Url, UriKind.Absolute, out var uri))
{
if (Uri.TryCreate(Url, UriKind.Absolute, out var uri))
{
return uri.Host;
}
return null;
return uri.Host;
}
return null;
}
}
}

View File

@ -1,153 +1,152 @@
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Bit.Setup
namespace Bit.Setup;
public class Context
{
public class Context
private const string ConfigPath = "/bitwarden/config.yml";
public string[] Args { get; set; }
public bool Quiet { get; set; }
public bool Stub { get; set; }
public IDictionary<string, string> 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 string KeyConnectorVersion { get; set; } = "latest";
public Installation Install { get; set; } = new Installation();
public Configuration Config { get; set; } = new Configuration();
public bool PrintToScreen()
{
private const string ConfigPath = "/bitwarden/config.yml";
return !Quiet || Parameters.ContainsKey("install");
}
public string[] Args { get; set; }
public bool Quiet { get; set; }
public bool Stub { get; set; }
public IDictionary<string, string> 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 string KeyConnectorVersion { get; set; } = "latest";
public Installation Install { get; set; } = new Installation();
public Configuration Config { get; set; } = new Configuration();
public bool PrintToScreen()
public void LoadConfiguration()
{
if (!File.Exists(ConfigPath))
{
return !Quiet || Parameters.ContainsKey("install");
}
Helpers.WriteLine(this, "No existing `config.yml` detected. Let's generate one.");
public void LoadConfiguration()
{
if (!File.Exists(ConfigPath))
// Looks like updating from older version. Try to create config file.
var url = Helpers.GetValueFromEnvFile("global", "globalSettings__baseServiceUri__vault");
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
Helpers.WriteLine(this, "No existing `config.yml` detected. Let's generate one.");
Helpers.WriteLine(this, "Unable to determine existing installation url.");
return;
}
Config.Url = url;
// Looks like updating from older version. Try to create config file.
var url = Helpers.GetValueFromEnvFile("global", "globalSettings__baseServiceUri__vault");
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
var push = Helpers.GetValueFromEnvFile("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)
{
Helpers.WriteLine(this, "Unable to determine existing installation url.");
return;
}
Config.Url = url;
var push = Helpers.GetValueFromEnvFile("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:"))
{
if (!line.StartsWith("# Parameter:"))
{
continue;
}
continue;
}
var paramParts = line.Split("=");
if (paramParts.Length < 2)
{
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: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: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;
}
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 nginxFile = "/bitwarden/nginx/default.conf";
if (File.Exists(nginxFile))
{
var confContent = File.ReadAllText(nginxFile);
var selfSigned = confContent.Contains("/etc/ssl/self/");
Config.Ssl = confContent.Contains("ssl http2;");
Config.SslManagedLetsEncrypt = !selfSigned && confContent.Contains("/etc/letsencrypt/live/");
var diffieHellman = confContent.Contains("/dhparam.pem;");
var trusted = confContent.Contains("ssl_trusted_certificate ");
if (Config.SslManagedLetsEncrypt)
{
var confContent = File.ReadAllText(nginxFile);
var selfSigned = confContent.Contains("/etc/ssl/self/");
Config.Ssl = confContent.Contains("ssl http2;");
Config.SslManagedLetsEncrypt = !selfSigned && confContent.Contains("/etc/letsencrypt/live/");
var diffieHellman = confContent.Contains("/dhparam.pem;");
var trusted = confContent.Contains("ssl_trusted_certificate ");
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.Ssl = true;
Config.SslCaPath = string.Concat(sslPath, "/", "ca.crt");
}
else if (Config.Ssl)
if (diffieHellman)
{
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");
}
Config.SslDiffieHellmanPath = string.Concat(sslPath, "/", "dhparam.pem");
}
}
SaveConfiguration();
}
var configText = File.ReadAllText(ConfigPath);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
Config = deserializer.Deserialize<Configuration>(configText);
SaveConfiguration();
}
public void SaveConfiguration()
{
if (Config == null)
{
throw new Exception("Config is null.");
}
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.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);
}
}
var configText = File.ReadAllText(ConfigPath);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.Build();
Config = deserializer.Deserialize<Configuration>(configText);
}
public class Installation
public void SaveConfiguration()
{
if (Config == null)
{
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 string Database { get; set; }
throw new Exception("Config is null.");
}
var serializer = new SerializerBuilder()
.WithNamingConvention(UnderscoredNamingConvention.Instance)
.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 string Database { get; set; }
}
}

View File

@ -1,80 +1,79 @@
namespace Bit.Setup
namespace Bit.Setup;
public class DockerComposeBuilder
{
public class DockerComposeBuilder
private readonly Context _context;
public DockerComposeBuilder(Context context)
{
private readonly Context _context;
_context = context;
}
public DockerComposeBuilder(Context context)
public void BuildForInstaller()
{
_context.Config.DatabaseDockerVolume = _context.HostOS == "mac";
Build();
}
public void BuildForUpdater()
{
Build();
}
private void Build()
{
Directory.CreateDirectory("/bitwarden/docker/");
Helpers.WriteLine(_context, "Building docker-compose.yml.");
if (!_context.Config.GenerateComposeConfig)
{
_context = context;
Helpers.WriteLine(_context, "...skipped");
return;
}
public void BuildForInstaller()
var template = Helpers.ReadTemplate("DockerCompose");
var model = new TemplateModel(_context);
using (var sw = File.CreateText("/bitwarden/docker/docker-compose.yml"))
{
_context.Config.DatabaseDockerVolume = _context.HostOS == "mac";
Build();
}
public void BuildForUpdater()
{
Build();
}
private void Build()
{
Directory.CreateDirectory("/bitwarden/docker/");
Helpers.WriteLine(_context, "Building docker-compose.yml.");
if (!_context.Config.GenerateComposeConfig)
{
Helpers.WriteLine(_context, "...skipped");
return;
}
var template = Helpers.ReadTemplate("DockerCompose");
var model = new TemplateModel(_context);
using (var sw = File.CreateText("/bitwarden/docker/docker-compose.yml"))
{
sw.Write(template(model));
}
}
public class TemplateModel
{
public TemplateModel(Context context)
{
if (!string.IsNullOrWhiteSpace(context.Config.ComposeVersion))
{
ComposeVersion = context.Config.ComposeVersion;
}
MssqlDataDockerVolume = context.Config.DatabaseDockerVolume;
EnableKeyConnector = context.Config.EnableKeyConnector;
EnableScim = context.Config.EnableScim;
HttpPort = context.Config.HttpPort;
HttpsPort = context.Config.HttpsPort;
if (!string.IsNullOrWhiteSpace(context.CoreVersion))
{
CoreVersion = context.CoreVersion;
}
if (!string.IsNullOrWhiteSpace(context.WebVersion))
{
WebVersion = context.WebVersion;
}
if (!string.IsNullOrWhiteSpace(context.KeyConnectorVersion))
{
KeyConnectorVersion = context.KeyConnectorVersion;
}
}
public string ComposeVersion { get; set; } = "3";
public bool MssqlDataDockerVolume { get; set; }
public bool EnableKeyConnector { get; set; }
public bool EnableScim { get; set; }
public string HttpPort { get; set; }
public string HttpsPort { get; set; }
public bool HasPort => !string.IsNullOrWhiteSpace(HttpPort) || !string.IsNullOrWhiteSpace(HttpsPort);
public string CoreVersion { get; set; } = "latest";
public string WebVersion { get; set; } = "latest";
public string KeyConnectorVersion { get; set; } = "latest";
sw.Write(template(model));
}
}
public class TemplateModel
{
public TemplateModel(Context context)
{
if (!string.IsNullOrWhiteSpace(context.Config.ComposeVersion))
{
ComposeVersion = context.Config.ComposeVersion;
}
MssqlDataDockerVolume = context.Config.DatabaseDockerVolume;
EnableKeyConnector = context.Config.EnableKeyConnector;
EnableScim = context.Config.EnableScim;
HttpPort = context.Config.HttpPort;
HttpsPort = context.Config.HttpsPort;
if (!string.IsNullOrWhiteSpace(context.CoreVersion))
{
CoreVersion = context.CoreVersion;
}
if (!string.IsNullOrWhiteSpace(context.WebVersion))
{
WebVersion = context.WebVersion;
}
if (!string.IsNullOrWhiteSpace(context.KeyConnectorVersion))
{
KeyConnectorVersion = context.KeyConnectorVersion;
}
}
public string ComposeVersion { get; set; } = "3";
public bool MssqlDataDockerVolume { get; set; }
public bool EnableKeyConnector { get; set; }
public bool EnableScim { get; set; }
public string HttpPort { get; set; }
public string HttpsPort { get; set; }
public bool HasPort => !string.IsNullOrWhiteSpace(HttpPort) || !string.IsNullOrWhiteSpace(HttpsPort);
public string CoreVersion { get; set; } = "latest";
public string WebVersion { get; set; } = "latest";
public string KeyConnectorVersion { get; set; } = "latest";
}
}

View File

@ -1,225 +1,224 @@
using System.Data.SqlClient;
namespace Bit.Setup
namespace Bit.Setup;
public class EnvironmentFileBuilder
{
public class EnvironmentFileBuilder
private readonly Context _context;
private IDictionary<string, string> _globalValues;
private IDictionary<string, string> _mssqlValues;
private IDictionary<string, string> _globalOverrideValues;
private IDictionary<string, string> _mssqlOverrideValues;
private IDictionary<string, string> _keyConnectorOverrideValues;
public EnvironmentFileBuilder(Context context)
{
private readonly Context _context;
private IDictionary<string, string> _globalValues;
private IDictionary<string, string> _mssqlValues;
private IDictionary<string, string> _globalOverrideValues;
private IDictionary<string, string> _mssqlOverrideValues;
private IDictionary<string, string> _keyConnectorOverrideValues;
public EnvironmentFileBuilder(Context context)
_context = context;
_globalValues = new Dictionary<string, string>
{
_context = context;
_globalValues = new Dictionary<string, string>
{
["ASPNETCORE_ENVIRONMENT"] = "Production",
["globalSettings__selfHosted"] = "true",
["globalSettings__baseServiceUri__vault"] = "http://localhost",
["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com",
};
_mssqlValues = new Dictionary<string, string>
{
["ACCEPT_EULA"] = "Y",
["MSSQL_PID"] = "Express",
["SA_PASSWORD"] = "SECRET",
};
["ASPNETCORE_ENVIRONMENT"] = "Production",
["globalSettings__selfHosted"] = "true",
["globalSettings__baseServiceUri__vault"] = "http://localhost",
["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com",
};
_mssqlValues = new Dictionary<string, string>
{
["ACCEPT_EULA"] = "Y",
["MSSQL_PID"] = "Express",
["SA_PASSWORD"] = "SECRET",
};
}
public void BuildForInstaller()
{
Directory.CreateDirectory("/bitwarden/env/");
Init();
Build();
}
public void BuildForUpdater()
{
Init();
LoadExistingValues(_globalOverrideValues, "/bitwarden/env/global.override.env");
LoadExistingValues(_mssqlOverrideValues, "/bitwarden/env/mssql.override.env");
LoadExistingValues(_keyConnectorOverrideValues, "/bitwarden/env/key-connector.override.env");
if (_context.Config.PushNotifications &&
_globalOverrideValues.ContainsKey("globalSettings__pushRelayBaseUri") &&
_globalOverrideValues["globalSettings__pushRelayBaseUri"] == "REPLACE")
{
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
}
public void BuildForInstaller()
Build();
}
private void Init()
{
var dbPassword = _context.Stub ? "RANDOM_DATABASE_PASSWORD" : Helpers.SecureRandomString(32);
var dbConnectionString = new SqlConnectionStringBuilder
{
Directory.CreateDirectory("/bitwarden/env/");
Init();
Build();
DataSource = "tcp:mssql,1433",
InitialCatalog = _context.Install?.Database ?? "vault",
UserID = "sa",
Password = dbPassword,
MultipleActiveResultSets = false,
Encrypt = true,
ConnectTimeout = 30,
TrustServerCertificate = true,
PersistSecurityInfo = false
}.ConnectionString;
_globalOverrideValues = new Dictionary<string, string>
{
["globalSettings__baseServiceUri__vault"] = _context.Config.Url,
["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString.Replace("\"", "\\\"")}\"",
["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword,
["globalSettings__internalIdentityKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__oidcIdentityClientKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__duo__aKey"] = _context.Stub ? "RANDOM_DUO_AKEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["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@{_context.Config.Domain}",
["globalSettings__mail__smtp__host"] = "REPLACE",
["globalSettings__mail__smtp__port"] = "587",
["globalSettings__mail__smtp__ssl"] = "false",
["globalSettings__mail__smtp__username"] = "REPLACE",
["globalSettings__mail__smtp__password"] = "REPLACE",
["globalSettings__disableUserRegistration"] = "false",
["globalSettings__hibpApiKey"] = "REPLACE",
["adminSettings__admins"] = string.Empty,
};
if (!_context.Config.PushNotifications)
{
_globalOverrideValues.Add("globalSettings__pushRelayBaseUri", "REPLACE");
}
public void BuildForUpdater()
_mssqlOverrideValues = new Dictionary<string, string>
{
Init();
LoadExistingValues(_globalOverrideValues, "/bitwarden/env/global.override.env");
LoadExistingValues(_mssqlOverrideValues, "/bitwarden/env/mssql.override.env");
LoadExistingValues(_keyConnectorOverrideValues, "/bitwarden/env/key-connector.override.env");
["SA_PASSWORD"] = dbPassword,
};
if (_context.Config.PushNotifications &&
_globalOverrideValues.ContainsKey("globalSettings__pushRelayBaseUri") &&
_globalOverrideValues["globalSettings__pushRelayBaseUri"] == "REPLACE")
{
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
}
_keyConnectorOverrideValues = new Dictionary<string, string>
{
["keyConnectorSettings__webVaultUri"] = _context.Config.Url,
["keyConnectorSettings__identityServerUri"] = "http://identity:5000",
["keyConnectorSettings__database__provider"] = "json",
["keyConnectorSettings__database__jsonFilePath"] = "/etc/bitwarden/key-connector/data.json",
["keyConnectorSettings__rsaKey__provider"] = "certificate",
["keyConnectorSettings__certificate__provider"] = "filesystem",
["keyConnectorSettings__certificate__filesystemPath"] = "/etc/bitwarden/key-connector/bwkc.pfx",
["keyConnectorSettings__certificate__filesystemPassword"] = Helpers.SecureRandomString(32, alpha: true, numeric: true),
};
}
Build();
private void LoadExistingValues(IDictionary<string, string> _values, string file)
{
if (!File.Exists(file))
{
return;
}
private void Init()
var fileLines = File.ReadAllLines(file);
foreach (var line in fileLines)
{
var dbPassword = _context.Stub ? "RANDOM_DATABASE_PASSWORD" : Helpers.SecureRandomString(32);
var dbConnectionString = new SqlConnectionStringBuilder
if (!line.Contains("="))
{
DataSource = "tcp:mssql,1433",
InitialCatalog = _context.Install?.Database ?? "vault",
UserID = "sa",
Password = dbPassword,
MultipleActiveResultSets = false,
Encrypt = true,
ConnectTimeout = 30,
TrustServerCertificate = true,
PersistSecurityInfo = false
}.ConnectionString;
_globalOverrideValues = new Dictionary<string, string>
{
["globalSettings__baseServiceUri__vault"] = _context.Config.Url,
["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString.Replace("\"", "\\\"")}\"",
["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword,
["globalSettings__internalIdentityKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__oidcIdentityClientKey"] = _context.Stub ? "RANDOM_IDENTITY_KEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__duo__aKey"] = _context.Stub ? "RANDOM_DUO_AKEY" :
Helpers.SecureRandomString(64, alpha: true, numeric: true),
["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@{_context.Config.Domain}",
["globalSettings__mail__smtp__host"] = "REPLACE",
["globalSettings__mail__smtp__port"] = "587",
["globalSettings__mail__smtp__ssl"] = "false",
["globalSettings__mail__smtp__username"] = "REPLACE",
["globalSettings__mail__smtp__password"] = "REPLACE",
["globalSettings__disableUserRegistration"] = "false",
["globalSettings__hibpApiKey"] = "REPLACE",
["adminSettings__admins"] = string.Empty,
};
if (!_context.Config.PushNotifications)
{
_globalOverrideValues.Add("globalSettings__pushRelayBaseUri", "REPLACE");
continue;
}
_mssqlOverrideValues = new Dictionary<string, string>
var value = string.Empty;
var lineParts = line.Split("=", 2);
if (lineParts.Length < 1)
{
["SA_PASSWORD"] = dbPassword,
};
_keyConnectorOverrideValues = new Dictionary<string, string>
{
["keyConnectorSettings__webVaultUri"] = _context.Config.Url,
["keyConnectorSettings__identityServerUri"] = "http://identity:5000",
["keyConnectorSettings__database__provider"] = "json",
["keyConnectorSettings__database__jsonFilePath"] = "/etc/bitwarden/key-connector/data.json",
["keyConnectorSettings__rsaKey__provider"] = "certificate",
["keyConnectorSettings__certificate__provider"] = "filesystem",
["keyConnectorSettings__certificate__filesystemPath"] = "/etc/bitwarden/key-connector/bwkc.pfx",
["keyConnectorSettings__certificate__filesystemPassword"] = Helpers.SecureRandomString(32, alpha: true, numeric: true),
};
}
private void LoadExistingValues(IDictionary<string, string> _values, string file)
{
if (!File.Exists(file))
{
return;
continue;
}
var fileLines = File.ReadAllLines(file);
foreach (var line in fileLines)
if (lineParts.Length > 1)
{
if (!line.Contains("="))
{
continue;
}
var value = string.Empty;
var lineParts = line.Split("=", 2);
if (lineParts.Length < 1)
{
continue;
}
if (lineParts.Length > 1)
{
value = lineParts[1];
}
if (_values.ContainsKey(lineParts[0]))
{
_values[lineParts[0]] = value;
}
else
{
_values.Add(lineParts[0], value.Replace("\\\"", "\""));
}
}
}
private void Build()
{
var template = Helpers.ReadTemplate("EnvironmentFile");
Helpers.WriteLine(_context, "Building docker environment files.");
Directory.CreateDirectory("/bitwarden/docker/");
using (var sw = File.CreateText("/bitwarden/docker/global.env"))
{
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(template(new TemplateModel(_mssqlValues)));
}
Helpers.Exec("chmod 600 /bitwarden/docker/mssql.env");
Helpers.WriteLine(_context, "Building docker environment override files.");
Directory.CreateDirectory("/bitwarden/env/");
using (var sw = File.CreateText("/bitwarden/env/global.override.env"))
{
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"))
{
sw.Write(template(new TemplateModel(_mssqlOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/mssql.override.env");
if (_context.Config.EnableKeyConnector)
{
using (var sw = File.CreateText("/bitwarden/env/key-connector.override.env"))
{
sw.Write(template(new TemplateModel(_keyConnectorOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/key-connector.override.env");
value = lineParts[1];
}
// Empty uid env file. Only used on Linux hosts.
if (!File.Exists("/bitwarden/env/uid.env"))
if (_values.ContainsKey(lineParts[0]))
{
using (var sw = File.CreateText("/bitwarden/env/uid.env")) { }
_values[lineParts[0]] = value;
}
}
public class TemplateModel
{
public TemplateModel(IEnumerable<KeyValuePair<string, string>> variables)
else
{
Variables = variables.Select(v => new Kvp { Key = v.Key, Value = v.Value });
}
public IEnumerable<Kvp> Variables { get; set; }
public class Kvp
{
public string Key { get; set; }
public string Value { get; set; }
_values.Add(lineParts[0], value.Replace("\\\"", "\""));
}
}
}
private void Build()
{
var template = Helpers.ReadTemplate("EnvironmentFile");
Helpers.WriteLine(_context, "Building docker environment files.");
Directory.CreateDirectory("/bitwarden/docker/");
using (var sw = File.CreateText("/bitwarden/docker/global.env"))
{
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(template(new TemplateModel(_mssqlValues)));
}
Helpers.Exec("chmod 600 /bitwarden/docker/mssql.env");
Helpers.WriteLine(_context, "Building docker environment override files.");
Directory.CreateDirectory("/bitwarden/env/");
using (var sw = File.CreateText("/bitwarden/env/global.override.env"))
{
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"))
{
sw.Write(template(new TemplateModel(_mssqlOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/mssql.override.env");
if (_context.Config.EnableKeyConnector)
{
using (var sw = File.CreateText("/bitwarden/env/key-connector.override.env"))
{
sw.Write(template(new TemplateModel(_keyConnectorOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/key-connector.override.env");
}
// Empty uid env file. Only used on Linux hosts.
if (!File.Exists("/bitwarden/env/uid.env"))
{
using (var sw = File.CreateText("/bitwarden/env/uid.env")) { }
}
}
public class TemplateModel
{
public TemplateModel(IEnumerable<KeyValuePair<string, string>> variables)
{
Variables = variables.Select(v => new Kvp { Key = v.Key, Value = v.Value });
}
public IEnumerable<Kvp> Variables { get; set; }
public class Kvp
{
public string Key { get; set; }
public string Value { get; set; }
}
}
}

View File

@ -4,223 +4,222 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace Bit.Setup
namespace Bit.Setup;
public static class Helpers
{
public static class Helpers
public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
bool numeric = true, bool special = false)
{
public static string SecureRandomString(int length, bool alpha = true, bool upper = true, bool lower = true,
bool numeric = true, bool special = false)
return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
}
// ref https://stackoverflow.com/a/8996788/1090359 with modifications
public static string SecureRandomString(int length, string characters)
{
if (length < 0)
{
return SecureRandomString(length, RandomStringCharacters(alpha, upper, lower, numeric, special));
throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero.");
}
// ref https://stackoverflow.com/a/8996788/1090359 with modifications
public static string SecureRandomString(int length, string characters)
if ((characters?.Length ?? 0) == 0)
{
if (length < 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "length cannot be less than zero.");
}
throw new ArgumentOutOfRangeException(nameof(characters), "characters invalid.");
}
if ((characters?.Length ?? 0) == 0)
{
throw new ArgumentOutOfRangeException(nameof(characters), "characters invalid.");
}
const int byteSize = 0x100;
if (byteSize < characters.Length)
{
throw new ArgumentException(
string.Format("{0} may contain no more than {1} characters.", nameof(characters), byteSize),
nameof(characters));
}
const int byteSize = 0x100;
if (byteSize < characters.Length)
var outOfRangeStart = byteSize - (byteSize % characters.Length);
using (var rng = RandomNumberGenerator.Create())
{
var sb = new StringBuilder();
var buffer = new byte[128];
while (sb.Length < length)
{
throw new ArgumentException(
string.Format("{0} may contain no more than {1} characters.", nameof(characters), byteSize),
nameof(characters));
}
var outOfRangeStart = byteSize - (byteSize % characters.Length);
using (var rng = RandomNumberGenerator.Create())
{
var sb = new StringBuilder();
var buffer = new byte[128];
while (sb.Length < length)
rng.GetBytes(buffer);
for (var i = 0; i < buffer.Length && sb.Length < length; ++i)
{
rng.GetBytes(buffer);
for (var i = 0; i < buffer.Length && sb.Length < length; ++i)
// Divide the byte into charSet-sized groups. If the random value falls into the last group and the
// last group is too small to choose from the entire allowedCharSet, ignore the value in order to
// avoid biasing the result.
if (outOfRangeStart <= buffer[i])
{
// Divide the byte into charSet-sized groups. If the random value falls into the last group and the
// last group is too small to choose from the entire allowedCharSet, ignore the value in order to
// avoid biasing the result.
if (outOfRangeStart <= buffer[i])
{
continue;
}
sb.Append(characters[buffer[i] % characters.Length]);
continue;
}
}
return sb.ToString();
sb.Append(characters[buffer[i] % characters.Length]);
}
}
return sb.ToString();
}
}
private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)
{
var characters = string.Empty;
if (alpha)
{
if (upper)
{
characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
if (lower)
{
characters += "abcdefghijklmnopqrstuvwxyz";
}
}
private static string RandomStringCharacters(bool alpha, bool upper, bool lower, bool numeric, bool special)
if (numeric)
{
var characters = string.Empty;
if (alpha)
{
if (upper)
{
characters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
}
if (lower)
{
characters += "abcdefghijklmnopqrstuvwxyz";
}
}
if (numeric)
{
characters += "0123456789";
}
if (special)
{
characters += "!@#$%^*&";
}
return characters;
characters += "0123456789";
}
public static string GetValueFromEnvFile(string envFile, string key)
if (special)
{
if (!File.Exists($"/bitwarden/env/{envFile}.override.env"))
{
return null;
}
characters += "!@#$%^*&";
}
var lines = File.ReadAllLines($"/bitwarden/env/{envFile}.override.env");
foreach (var line in lines)
{
if (line.StartsWith($"{key}="))
{
return line.Split(new char[] { '=' }, 2)[1].Trim('"').Replace("\\\"", "\"");
}
}
return characters;
}
public static string GetValueFromEnvFile(string envFile, string key)
{
if (!File.Exists($"/bitwarden/env/{envFile}.override.env"))
{
return null;
}
public static string Exec(string cmd, bool returnStdout = false)
var lines = File.ReadAllLines($"/bitwarden/env/{envFile}.override.env");
foreach (var line in lines)
{
var process = new Process
if (line.StartsWith($"{key}="))
{
StartInfo = new ProcessStartInfo
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
};
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var escapedArgs = cmd.Replace("\"", "\\\"");
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $"-c \"{escapedArgs}\"";
return line.Split(new char[] { '=' }, 2)[1].Trim('"').Replace("\\\"", "\"");
}
else
{
process.StartInfo.FileName = "powershell";
process.StartInfo.Arguments = cmd;
}
process.Start();
var result = returnStdout ? process.StandardOutput.ReadToEnd() : null;
process.WaitForExit();
return result;
}
public static string ReadInput(string prompt)
return null;
}
public static string Exec(string cmd, bool returnStdout = false)
{
var process = new Process
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("(!) ");
Console.ResetColor();
Console.Write(prompt);
if (prompt.EndsWith("?"))
StartInfo = new ProcessStartInfo
{
Console.Write(" (y/n)");
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
}
Console.Write(": ");
var input = Console.ReadLine();
};
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var escapedArgs = cmd.Replace("\"", "\\\"");
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $"-c \"{escapedArgs}\"";
}
else
{
process.StartInfo.FileName = "powershell";
process.StartInfo.Arguments = cmd;
}
process.Start();
var result = returnStdout ? process.StandardOutput.ReadToEnd() : null;
process.WaitForExit();
return result;
}
public static string ReadInput(string prompt)
{
Console.ForegroundColor = ConsoleColor.Cyan;
Console.Write("(!) ");
Console.ResetColor();
Console.Write(prompt);
if (prompt.EndsWith("?"))
{
Console.Write(" (y/n)");
}
Console.Write(": ");
var input = Console.ReadLine();
Console.WriteLine();
return input;
}
public static bool ReadQuestion(string prompt)
{
var input = ReadInput(prompt).ToLowerInvariant().Trim();
return input == "y" || input == "yes";
}
public static void ShowBanner(Context context, string title, string message, ConsoleColor? color = null)
{
if (!context.PrintToScreen())
{
return;
}
if (color != null)
{
Console.ForegroundColor = color.Value;
}
Console.WriteLine($"!!!!!!!!!! {title} !!!!!!!!!!");
Console.WriteLine(message);
Console.WriteLine();
Console.ResetColor();
}
public static HandlebarsDotNet.HandlebarsTemplate<object, object> 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);
}
}
public static void WriteLine(Context context, string format = null, object arg0 = null, object arg1 = null,
object arg2 = null)
{
if (!context.PrintToScreen())
{
return;
}
if (format != null && arg0 != null && arg1 != null && arg2 != null)
{
Console.WriteLine(format, arg0, arg1, arg2);
}
else if (format != null && arg0 != null && arg1 != null)
{
Console.WriteLine(format, arg0, arg1);
}
else if (format != null && arg0 != null)
{
Console.WriteLine(format, arg0);
}
else if (format != null)
{
Console.WriteLine(format);
}
else
{
Console.WriteLine();
return input;
}
public static bool ReadQuestion(string prompt)
{
var input = ReadInput(prompt).ToLowerInvariant().Trim();
return input == "y" || input == "yes";
}
public static void ShowBanner(Context context, string title, string message, ConsoleColor? color = null)
{
if (!context.PrintToScreen())
{
return;
}
if (color != null)
{
Console.ForegroundColor = color.Value;
}
Console.WriteLine($"!!!!!!!!!! {title} !!!!!!!!!!");
Console.WriteLine(message);
Console.WriteLine();
Console.ResetColor();
}
public static HandlebarsDotNet.HandlebarsTemplate<object, object> 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);
}
}
public static void WriteLine(Context context, string format = null, object arg0 = null, object arg1 = null,
object arg2 = null)
{
if (!context.PrintToScreen())
{
return;
}
if (format != null && arg0 != null && arg1 != null && arg2 != null)
{
Console.WriteLine(format, arg0, arg1, arg2);
}
else if (format != null && arg0 != null && arg1 != null)
{
Console.WriteLine(format, arg0, arg1);
}
else if (format != null && arg0 != null)
{
Console.WriteLine(format, arg0);
}
else if (format != null)
{
Console.WriteLine(format);
}
else
{
Console.WriteLine();
}
}
}
}

View File

@ -1,133 +1,132 @@
namespace Bit.Setup
namespace Bit.Setup;
public class NginxConfigBuilder
{
public class NginxConfigBuilder
private const string ConfFile = "/bitwarden/nginx/default.conf";
private readonly Context _context;
public NginxConfigBuilder(Context context)
{
private const string ConfFile = "/bitwarden/nginx/default.conf";
_context = context;
}
private readonly Context _context;
public NginxConfigBuilder(Context context)
public void BuildForInstaller()
{
var model = new TemplateModel(_context);
if (model.Ssl && !_context.Config.SslManagedLetsEncrypt)
{
_context = context;
}
public void BuildForInstaller()
{
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)
{
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");
}
_context.Config.SslCaPath = model.CaPath =
string.Concat(sslPath, "/", "ca.crt");
}
Build(model);
}
public void BuildForUpdater()
{
var model = new TemplateModel(_context);
Build(model);
}
private void Build(TemplateModel model)
{
Directory.CreateDirectory("/bitwarden/nginx/");
Helpers.WriteLine(_context, "Building nginx config.");
if (!_context.Config.GenerateNginxConfig)
if (_context.Install.DiffieHellman)
{
Helpers.WriteLine(_context, "...skipped");
return;
}
var template = Helpers.ReadTemplate("NginxConfig");
using (var sw = File.CreateText(ConfFile))
{
sw.WriteLine(template(model));
_context.Config.SslDiffieHellmanPath = model.DiffieHellmanPath =
string.Concat(sslPath, "/", "dhparam.pem");
}
}
Build(model);
}
public class TemplateModel
public void BuildForUpdater()
{
var model = new TemplateModel(_context);
Build(model);
}
private void Build(TemplateModel model)
{
Directory.CreateDirectory("/bitwarden/nginx/");
Helpers.WriteLine(_context, "Building nginx config.");
if (!_context.Config.GenerateNginxConfig)
{
public TemplateModel() { }
Helpers.WriteLine(_context, "...skipped");
return;
}
public TemplateModel(Context context)
{
Captcha = context.Config.Captcha;
Ssl = context.Config.Ssl;
EnableKeyConnector = context.Config.EnableKeyConnector;
EnableScim = context.Config.EnableScim;
Domain = context.Config.Domain;
Url = context.Config.Url;
RealIps = context.Config.RealIps;
ContentSecurityPolicy = string.Format(context.Config.NginxHeaderContentSecurityPolicy, Domain);
if (Ssl)
{
if (context.Config.SslManagedLetsEncrypt)
{
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");
}
else
{
CertificatePath = context.Config.SslCertificatePath;
KeyPath = context.Config.SslKeyPath;
CaPath = context.Config.SslCaPath;
DiffieHellmanPath = context.Config.SslDiffieHellmanPath;
}
}
if (!string.IsNullOrWhiteSpace(context.Config.SslCiphersuites))
{
SslCiphers = context.Config.SslCiphersuites;
}
else
{
SslCiphers = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" +
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:" +
"ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" +
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256";
}
if (!string.IsNullOrWhiteSpace(context.Config.SslVersions))
{
SslProtocols = context.Config.SslVersions;
}
else
{
SslProtocols = "TLSv1.2";
}
}
public bool Captcha { get; set; }
public bool Ssl { get; set; }
public bool EnableKeyConnector { get; set; }
public bool EnableScim { 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 SslCiphers { get; set; }
public string SslProtocols { get; set; }
public string ContentSecurityPolicy { get; set; }
public List<string> RealIps { get; set; }
var template = Helpers.ReadTemplate("NginxConfig");
using (var sw = File.CreateText(ConfFile))
{
sw.WriteLine(template(model));
}
}
public class TemplateModel
{
public TemplateModel() { }
public TemplateModel(Context context)
{
Captcha = context.Config.Captcha;
Ssl = context.Config.Ssl;
EnableKeyConnector = context.Config.EnableKeyConnector;
EnableScim = context.Config.EnableScim;
Domain = context.Config.Domain;
Url = context.Config.Url;
RealIps = context.Config.RealIps;
ContentSecurityPolicy = string.Format(context.Config.NginxHeaderContentSecurityPolicy, Domain);
if (Ssl)
{
if (context.Config.SslManagedLetsEncrypt)
{
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");
}
else
{
CertificatePath = context.Config.SslCertificatePath;
KeyPath = context.Config.SslKeyPath;
CaPath = context.Config.SslCaPath;
DiffieHellmanPath = context.Config.SslDiffieHellmanPath;
}
}
if (!string.IsNullOrWhiteSpace(context.Config.SslCiphersuites))
{
SslCiphers = context.Config.SslCiphersuites;
}
else
{
SslCiphers = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:" +
"ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:" +
"ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:" +
"ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256";
}
if (!string.IsNullOrWhiteSpace(context.Config.SslVersions))
{
SslProtocols = context.Config.SslVersions;
}
else
{
SslProtocols = "TLSv1.2";
}
}
public bool Captcha { get; set; }
public bool Ssl { get; set; }
public bool EnableKeyConnector { get; set; }
public bool EnableScim { 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 SslCiphers { get; set; }
public string SslProtocols { get; set; }
public string ContentSecurityPolicy { get; set; }
public List<string> RealIps { get; set; }
}
}

View File

@ -3,328 +3,327 @@ using System.Globalization;
using System.Net.Http.Json;
using Bit.Migrator;
namespace Bit.Setup
namespace Bit.Setup;
public class Program
{
public class Program
private static Context _context;
public static void Main(string[] args)
{
private static Context _context;
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
public static void Main(string[] args)
_context = new Context
{
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US");
Args = args
};
ParseParameters();
_context = new Context
{
Args = args
};
ParseParameters();
if (_context.Parameters.ContainsKey("q"))
{
_context.Quiet = _context.Parameters["q"] == "true" || _context.Parameters["q"] == "1";
}
if (_context.Parameters.ContainsKey("os"))
{
_context.HostOS = _context.Parameters["os"];
}
if (_context.Parameters.ContainsKey("corev"))
{
_context.CoreVersion = _context.Parameters["corev"];
}
if (_context.Parameters.ContainsKey("webv"))
{
_context.WebVersion = _context.Parameters["webv"];
}
if (_context.Parameters.ContainsKey("keyconnectorv"))
{
_context.KeyConnectorVersion = _context.Parameters["keyconnectorv"];
}
if (_context.Parameters.ContainsKey("stub"))
{
_context.Stub = _context.Parameters["stub"] == "true" ||
_context.Parameters["stub"] == "1";
}
if (_context.Parameters.ContainsKey("q"))
{
_context.Quiet = _context.Parameters["q"] == "true" || _context.Parameters["q"] == "1";
}
if (_context.Parameters.ContainsKey("os"))
{
_context.HostOS = _context.Parameters["os"];
}
if (_context.Parameters.ContainsKey("corev"))
{
_context.CoreVersion = _context.Parameters["corev"];
}
if (_context.Parameters.ContainsKey("webv"))
{
_context.WebVersion = _context.Parameters["webv"];
}
if (_context.Parameters.ContainsKey("keyconnectorv"))
{
_context.KeyConnectorVersion = _context.Parameters["keyconnectorv"];
}
if (_context.Parameters.ContainsKey("stub"))
{
_context.Stub = _context.Parameters["stub"] == "true" ||
_context.Parameters["stub"] == "1";
}
Helpers.WriteLine(_context);
Helpers.WriteLine(_context);
if (_context.Parameters.ContainsKey("install"))
{
Install();
}
else if (_context.Parameters.ContainsKey("update"))
{
Update();
}
else if (_context.Parameters.ContainsKey("printenv"))
{
PrintEnvironment();
}
else
{
Helpers.WriteLine(_context, "No top-level command detected. Exiting...");
}
}
if (_context.Parameters.ContainsKey("install"))
private static void Install()
{
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 (_context.Parameters.ContainsKey("dbname"))
{
_context.Install.Database = _context.Parameters["dbname"];
}
if (_context.Stub)
{
_context.Install.InstallationId = Guid.Empty;
_context.Install.InstallationKey = "SECRET_INSTALLATION_KEY";
}
else if (!ValidateInstallation())
{
return;
}
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 nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForInstaller();
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForInstaller();
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForInstaller();
_context.SaveConfiguration();
Console.WriteLine("\nInstallation complete");
Console.WriteLine("\nIf you need to make additional configuration changes, you can modify\n" +
"the settings in `{0}` and then run:\n{1}",
_context.HostOS == "win" ? ".\\bwdata\\config.yml" : "./bwdata/config.yml",
_context.HostOS == "win" ? "`.\\bitwarden.ps1 -rebuild` or `.\\bitwarden.ps1 -update`" :
"`./bitwarden.sh rebuild` or `./bitwarden.sh update`");
Console.WriteLine("\nNext steps, run:");
if (_context.HostOS == "win")
{
Console.WriteLine("`.\\bitwarden.ps1 -start`");
}
else
{
Console.WriteLine("`./bitwarden.sh start`");
}
Console.WriteLine(string.Empty);
}
private static void Update()
{
// This portion of code checks for multiple certs in the Identity.pfx PKCS12 bag. If found, it generates
// a new cert and bag to replace the old Identity.pfx. This fixes an issue that came up as a result of
// moving the project to .NET 5.
_context.Install.IdentityCertPassword = Helpers.GetValueFromEnvFile("global", "globalSettings__identityServer__certificatePassword");
var certCountString = Helpers.Exec("openssl pkcs12 -nokeys -info -in /bitwarden/identity/identity.pfx " +
$"-passin pass:{_context.Install.IdentityCertPassword} 2> /dev/null | grep -c \"\\-----BEGIN CERTIFICATE----\"", true);
if (int.TryParse(certCountString, out var certCount) && certCount > 1)
{
// Extract key from identity.pfx
Helpers.Exec("openssl pkcs12 -in /bitwarden/identity/identity.pfx -nocerts -nodes -out identity.key " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
// Extract certificate from identity.pfx
Helpers.Exec("openssl pkcs12 -in /bitwarden/identity/identity.pfx -clcerts -nokeys -out identity.crt " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
// Create new PKCS12 bag with certificate and key
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
}
if (_context.Parameters.ContainsKey("db"))
{
MigrateDatabase();
}
else
{
RebuildConfigs();
}
}
private static void PrintEnvironment()
{
_context.LoadConfiguration();
if (!_context.PrintToScreen())
{
return;
}
Console.WriteLine("\nBitwarden is up and running!");
Console.WriteLine("===================================================");
Console.WriteLine("\nvisit {0}", _context.Config.Url);
Console.Write("to update, run ");
if (_context.HostOS == "win")
{
Console.Write("`.\\bitwarden.ps1 -updateself` and then `.\\bitwarden.ps1 -update`");
}
else
{
Console.Write("`./bitwarden.sh updateself` and then `./bitwarden.sh update`");
}
Console.WriteLine("\n");
}
private static void MigrateDatabase(int attempt = 1)
{
try
{
Helpers.WriteLine(_context, "Migrating database.");
var vaultConnectionString = Helpers.GetValueFromEnvFile("global",
"globalSettings__sqlServer__connectionString");
var migrator = new DbMigrator(vaultConnectionString, null);
var success = migrator.MigrateMsSqlDatabase(false);
if (success)
{
Install();
}
else if (_context.Parameters.ContainsKey("update"))
{
Update();
}
else if (_context.Parameters.ContainsKey("printenv"))
{
PrintEnvironment();
Helpers.WriteLine(_context, "Migration successful.");
}
else
{
Helpers.WriteLine(_context, "No top-level command detected. Exiting...");
Helpers.WriteLine(_context, "Migration failed.");
}
}
private static void Install()
catch (SqlException e)
{
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 (_context.Parameters.ContainsKey("dbname"))
{
_context.Install.Database = _context.Parameters["dbname"];
}
if (_context.Stub)
{
_context.Install.InstallationId = Guid.Empty;
_context.Install.InstallationKey = "SECRET_INSTALLATION_KEY";
}
else if (!ValidateInstallation())
if (e.Message.Contains("Server is in script upgrade mode") && attempt < 10)
{
var nextAttempt = attempt + 1;
Helpers.WriteLine(_context, "Database is in script upgrade mode. " +
"Trying again (attempt #{0})...", nextAttempt);
System.Threading.Thread.Sleep(20000);
MigrateDatabase(nextAttempt);
return;
}
throw;
}
}
var certBuilder = new CertBuilder(_context);
certBuilder.BuildForInstall();
private static bool ValidateInstallation()
{
var installationId = string.Empty;
var installationKey = string.Empty;
// Set the URL
_context.Config.Url = string.Format("http{0}://{1}",
_context.Config.Ssl ? "s" : string.Empty, _context.Install.Domain);
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForInstaller();
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForInstaller();
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForInstaller();
_context.SaveConfiguration();
Console.WriteLine("\nInstallation complete");
Console.WriteLine("\nIf you need to make additional configuration changes, you can modify\n" +
"the settings in `{0}` and then run:\n{1}",
_context.HostOS == "win" ? ".\\bwdata\\config.yml" : "./bwdata/config.yml",
_context.HostOS == "win" ? "`.\\bitwarden.ps1 -rebuild` or `.\\bitwarden.ps1 -update`" :
"`./bitwarden.sh rebuild` or `./bitwarden.sh update`");
Console.WriteLine("\nNext steps, run:");
if (_context.HostOS == "win")
{
Console.WriteLine("`.\\bitwarden.ps1 -start`");
}
else
{
Console.WriteLine("`./bitwarden.sh start`");
}
Console.WriteLine(string.Empty);
if (_context.Parameters.ContainsKey("install-id"))
{
installationId = _context.Parameters["install-id"].ToLowerInvariant();
}
else
{
installationId = Helpers.ReadInput("Enter your installation id (get at https://bitwarden.com/host)");
}
private static void Update()
if (!Guid.TryParse(installationId.Trim(), out var installationidGuid))
{
// This portion of code checks for multiple certs in the Identity.pfx PKCS12 bag. If found, it generates
// a new cert and bag to replace the old Identity.pfx. This fixes an issue that came up as a result of
// moving the project to .NET 5.
_context.Install.IdentityCertPassword = Helpers.GetValueFromEnvFile("global", "globalSettings__identityServer__certificatePassword");
var certCountString = Helpers.Exec("openssl pkcs12 -nokeys -info -in /bitwarden/identity/identity.pfx " +
$"-passin pass:{_context.Install.IdentityCertPassword} 2> /dev/null | grep -c \"\\-----BEGIN CERTIFICATE----\"", true);
if (int.TryParse(certCountString, out var certCount) && certCount > 1)
{
// Extract key from identity.pfx
Helpers.Exec("openssl pkcs12 -in /bitwarden/identity/identity.pfx -nocerts -nodes -out identity.key " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
// Extract certificate from identity.pfx
Helpers.Exec("openssl pkcs12 -in /bitwarden/identity/identity.pfx -clcerts -nokeys -out identity.crt " +
$"-passin pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
// Create new PKCS12 bag with certificate and key
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -passout pass:{_context.Install.IdentityCertPassword} > /dev/null 2>&1");
}
if (_context.Parameters.ContainsKey("db"))
{
MigrateDatabase();
}
else
{
RebuildConfigs();
}
Console.WriteLine("Invalid installation id.");
return false;
}
private static void PrintEnvironment()
if (_context.Parameters.ContainsKey("install-key"))
{
_context.LoadConfiguration();
if (!_context.PrintToScreen())
{
return;
}
Console.WriteLine("\nBitwarden is up and running!");
Console.WriteLine("===================================================");
Console.WriteLine("\nvisit {0}", _context.Config.Url);
Console.Write("to update, run ");
if (_context.HostOS == "win")
{
Console.Write("`.\\bitwarden.ps1 -updateself` and then `.\\bitwarden.ps1 -update`");
}
else
{
Console.Write("`./bitwarden.sh updateself` and then `./bitwarden.sh update`");
}
Console.WriteLine("\n");
installationKey = _context.Parameters["install-key"];
}
else
{
installationKey = Helpers.ReadInput("Enter your installation key");
}
private static void MigrateDatabase(int attempt = 1)
_context.Install.InstallationId = installationidGuid;
_context.Install.InstallationKey = installationKey;
try
{
try
var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" +
_context.Install.InstallationId).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
Helpers.WriteLine(_context, "Migrating database.");
var vaultConnectionString = Helpers.GetValueFromEnvFile("global",
"globalSettings__sqlServer__connectionString");
var migrator = new DbMigrator(vaultConnectionString, null);
var success = migrator.MigrateMsSqlDatabase(false);
if (success)
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Helpers.WriteLine(_context, "Migration successful.");
Console.WriteLine("Invalid installation id.");
}
else
{
Helpers.WriteLine(_context, "Migration failed.");
Console.WriteLine("Unable to validate installation id.");
}
}
catch (SqlException e)
{
if (e.Message.Contains("Server is in script upgrade mode") && attempt < 10)
{
var nextAttempt = attempt + 1;
Helpers.WriteLine(_context, "Database is in script upgrade mode. " +
"Trying again (attempt #{0})...", nextAttempt);
System.Threading.Thread.Sleep(20000);
MigrateDatabase(nextAttempt);
return;
}
throw;
}
}
private static bool ValidateInstallation()
{
var installationId = string.Empty;
var installationKey = string.Empty;
if (_context.Parameters.ContainsKey("install-id"))
{
installationId = _context.Parameters["install-id"].ToLowerInvariant();
}
else
{
installationId = Helpers.ReadInput("Enter your installation id (get at https://bitwarden.com/host)");
}
if (!Guid.TryParse(installationId.Trim(), out var installationidGuid))
{
Console.WriteLine("Invalid installation id.");
return false;
}
if (_context.Parameters.ContainsKey("install-key"))
var result = response.Content.ReadFromJsonAsync<InstallationValidationResponseModel>().GetAwaiter().GetResult();
if (!result.Enabled)
{
installationKey = _context.Parameters["install-key"];
}
else
{
installationKey = Helpers.ReadInput("Enter your installation key");
}
_context.Install.InstallationId = installationidGuid;
_context.Install.InstallationKey = installationKey;
try
{
var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" +
_context.Install.InstallationId).GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
Console.WriteLine("Invalid installation id.");
}
else
{
Console.WriteLine("Unable to validate installation id.");
}
return false;
}
var result = response.Content.ReadFromJsonAsync<InstallationValidationResponseModel>().GetAwaiter().GetResult();
if (!result.Enabled)
{
Console.WriteLine("Installation id has been disabled.");
return false;
}
return true;
}
catch
{
Console.WriteLine("Unable to validate installation id. Problem contacting Bitwarden server.");
Console.WriteLine("Installation id has been disabled.");
return false;
}
return true;
}
private static void RebuildConfigs()
catch
{
_context.LoadConfiguration();
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForUpdater();
var certBuilder = new CertBuilder(_context);
certBuilder.BuildForUpdater();
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForUpdater();
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForUpdater();
_context.SaveConfiguration();
Console.WriteLine(string.Empty);
}
private static void ParseParameters()
{
_context.Parameters = new Dictionary<string, string>();
for (var i = 0; i < _context.Args.Length; i = i + 2)
{
if (!_context.Args[i].StartsWith("-"))
{
continue;
}
_context.Parameters.Add(_context.Args[i].Substring(1), _context.Args[i + 1]);
}
}
class InstallationValidationResponseModel
{
public bool Enabled { get; init; }
Console.WriteLine("Unable to validate installation id. Problem contacting Bitwarden server.");
return false;
}
}
private static void RebuildConfigs()
{
_context.LoadConfiguration();
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForUpdater();
var certBuilder = new CertBuilder(_context);
certBuilder.BuildForUpdater();
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForUpdater();
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForUpdater();
_context.SaveConfiguration();
Console.WriteLine(string.Empty);
}
private static void ParseParameters()
{
_context.Parameters = new Dictionary<string, string>();
for (var i = 0; i < _context.Args.Length; i = i + 2)
{
if (!_context.Args[i].StartsWith("-"))
{
continue;
}
_context.Parameters.Add(_context.Args[i].Substring(1), _context.Args[i + 1]);
}
}
class InstallationValidationResponseModel
{
public bool Enabled { get; init; }
}
}

View File

@ -7,102 +7,101 @@ using YamlDotNet.Serialization.TypeInspectors;
// ref: https://github.com/aaubry/YamlDotNet/issues/152#issuecomment-349034754
namespace Bit.Setup
namespace Bit.Setup;
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
{
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
private readonly ITypeInspector _innerTypeDescriptor;
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
{
private readonly ITypeInspector _innerTypeDescriptor;
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
{
_innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor));
}
public override IEnumerable<IPropertyDescriptor> 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<T>() where T : Attribute
{
return _baseDescriptor.GetCustomAttribute<T>();
}
public IObjectDescriptor Read(object target)
{
var description = _baseDescriptor.GetCustomAttribute<DescriptionAttribute>();
return description != null ?
new CommentsObjectDescriptor(_baseDescriptor.Read(target), description.Description) :
_baseDescriptor.Read(target);
}
}
_innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor));
}
public sealed class CommentsObjectDescriptor : IObjectDescriptor
public override IEnumerable<IPropertyDescriptor> GetProperties(Type type, object container)
{
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;
return _innerTypeDescriptor.GetProperties(type, container).Select(d => new CommentsPropertyDescriptor(d));
}
public class CommentsObjectGraphVisitor : ChainedObjectGraphVisitor
private sealed class CommentsPropertyDescriptor : IPropertyDescriptor
{
public CommentsObjectGraphVisitor(IObjectGraphVisitor<IEmitter> nextVisitor)
: base(nextVisitor) { }
private readonly IPropertyDescriptor _baseDescriptor;
public override bool EnterMapping(IPropertyDescriptor key, IObjectDescriptor value, IEmitter context)
public CommentsPropertyDescriptor(IPropertyDescriptor baseDescriptor)
{
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);
_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<T>() where T : Attribute
{
return _baseDescriptor.GetCustomAttribute<T>();
}
public IObjectDescriptor Read(object target)
{
var description = _baseDescriptor.GetCustomAttribute<DescriptionAttribute>();
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<IEmitter> 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);
}
}