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:
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user