1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-22 12:15:36 +01:00

convert setup to use config.yml

This commit is contained in:
Kyle Spearrin 2018-08-30 11:35:44 -04:00
parent a1f0f04660
commit 310e6bcf61
14 changed files with 954 additions and 646 deletions

View File

@ -5,35 +5,32 @@ namespace Bit.Setup
{
public class AppIdBuilder
{
public AppIdBuilder(string url)
{
Url = url;
}
private readonly Context _context;
public string Url { get; private set; }
public AppIdBuilder(Context context)
{
_context = context;
}
public void Build()
{
var model = new TemplateModel
{
Url = _context.Config.Url
};
Console.WriteLine("Building FIDO U2F app id.");
Directory.CreateDirectory("/bitwarden/web/");
var template = Helpers.ReadTemplate("AppId");
using(var sw = File.CreateText("/bitwarden/web/app-id.json"))
{
sw.Write($@"{{
""trustedFacets"": [
{{
""version"": {{
""major"": 1,
""minor"": 0
}},
""ids"": [
""{Url}"",
""ios:bundle-id:com.8bit.bitwarden"",
""android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI""
]
}}
]
}}");
sw.Write(template(model));
}
}
public class TemplateModel
{
public string Url { get; set; }
}
}
}

View File

@ -5,53 +5,81 @@ namespace Bit.Setup
{
public class CertBuilder
{
public CertBuilder(string domain, string identityCertPassword, bool letsEncrypt, bool ssl)
private readonly Context _context;
public CertBuilder(Context context)
{
Domain = domain;
IdentityCertPassword = identityCertPassword;
LetsEncrypt = letsEncrypt;
Ssl = ssl;
_context = context;
}
public string Domain { get; private set; }
public bool LetsEncrypt { get; private set; }
public bool Ssl { get; private set; }
public string IdentityCertPassword { get; private set; }
public bool BuildForInstall()
public void BuildForInstall()
{
var selfSignedSsl = false;
if(!Ssl)
_context.Config.Ssl = _context.Config.SslManagedLetsEncrypt;
if(!_context.Config.Ssl)
{
if(Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
_context.Config.Ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?");
if(_context.Config.Ssl)
{
Directory.CreateDirectory($"/bitwarden/ssl/self/{Domain}/");
Directory.CreateDirectory($"/bitwarden/ssl/{_context.Install.Domain}/");
var message = "Make sure 'certificate.crt' and 'private.key' are provided in the \n" +
"appropriate directory before running 'start' (see docs for info).";
Helpers.ShowBanner("NOTE", message);
}
else if(Helpers.ReadQuestion("Do you want to generate a self-signed SSL certificate?"))
{
Directory.CreateDirectory($"/bitwarden/ssl/self/{_context.Install.Domain}/");
Console.WriteLine("Generating self signed SSL certificate.");
Ssl = selfSignedSsl = true;
_context.Config.Ssl = true;
_context.Install.Trusted = false;
_context.Install.SelfSignedCert = true;
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -days 365 " +
$"-keyout /bitwarden/ssl/self/{Domain}/private.key " +
$"-out /bitwarden/ssl/self/{Domain}/certificate.crt " +
$"-keyout /bitwarden/ssl/self/{_context.Install.Domain}/private.key " +
$"-out /bitwarden/ssl/self/{_context.Install.Domain}/certificate.crt " +
$"-reqexts SAN -extensions SAN " +
$"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{Domain}\nbasicConstraints=CA:true')) " +
$"-subj \"/C=US/ST=Florida/L=Jacksonville/O=8bit Solutions LLC/OU=Bitwarden/CN={Domain}\"");
$"-config <(cat /usr/lib/ssl/openssl.cnf <(printf '[SAN]\nsubjectAltName=DNS:{_context.Install.Domain}\nbasicConstraints=CA:true')) " +
$"-subj \"/C=US/ST=Florida/L=Jacksonville/O=8bit Solutions LLC/OU=Bitwarden/CN={_context.Install.Domain}\"");
}
}
if(LetsEncrypt)
if(_context.Config.SslManagedLetsEncrypt)
{
Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{Domain}/");
Helpers.Exec($"openssl dhparam -out /bitwarden/letsencrypt/live/{Domain}/dhparam.pem 2048");
_context.Install.Trusted = true;
_context.Install.DiffieHellman = true;
Directory.CreateDirectory($"/bitwarden/letsencrypt/live/{_context.Install.Domain}/");
Helpers.Exec($"openssl dhparam -out /bitwarden/letsencrypt/live/{_context.Install.Domain}/dhparam.pem 2048");
}
else if(_context.Config.Ssl && !_context.Install.SelfSignedCert)
{
_context.Install.Trusted = Helpers.ReadQuestion("Is this a trusted SSL certificate " +
"(requires ca.crt, see docs)?");
}
Console.WriteLine("Generating key for IdentityServer.");
_context.Install.IdentityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true);
Directory.CreateDirectory("/bitwarden/identity/");
Helpers.Exec("openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout identity.key " +
"-out identity.crt -subj \"/CN=Bitwarden IdentityServer\" -days 10950");
Helpers.Exec("openssl pkcs12 -export -out /bitwarden/identity/identity.pfx -inkey identity.key " +
$"-in identity.crt -certfile identity.crt -passout pass:{IdentityCertPassword}");
$"-in identity.crt -certfile identity.crt -passout pass:{_context.Install.IdentityCertPassword}");
Console.WriteLine();
return selfSignedSsl;
if(!_context.Config.Ssl)
{
var message = "You are not using a SSL certificate. Bitwarden requires HTTPS to operate. \n" +
"You must front your installation with a HTTPS proxy or the web vault (and \n" +
"other Bitwarden apps) will not work properly.";
Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow);
}
else if(_context.Config.Ssl && !_context.Install.Trusted)
{
var message = "You are using an untrusted SSL certificate. This certificate will not be \n" +
"trusted by Bitwarden client applications. You must add this certificate to \n" +
"the trusted store on each device or else you will receive errors when trying \n" +
"to connect to your installation.";
Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow);
}
}
}
}

254
util/Setup/Context.cs Normal file
View File

@ -0,0 +1,254 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace Bit.Setup
{
public class Context
{
private const string ConfigPath = "/bitwarden/config.yml";
public string[] Args { get; set; }
public IDictionary<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 Installation Install { get; set; } = new Installation();
public Configuration Config { get; set; } = new Configuration();
public void LoadConfiguration()
{
if(!File.Exists(ConfigPath))
{
// Looks like updating from older version. Try to create config file.
var url = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault");
if(!Uri.TryCreate(url, UriKind.Absolute, out var uri))
{
Console.WriteLine("Unable to determine existing installation url.");
return;
}
Config.Url = url;
var push = Helpers.GetValueFronEnvFile("global", "globalSettings__pushRelayBaseUri");
Config.PushNotifications = push != "REPLACE";
var composeFile = "/bitwarden/docker/docker-compose.yml";
if(File.Exists(composeFile))
{
var fileLines = File.ReadAllLines(composeFile);
foreach(var line in fileLines)
{
if(!line.StartsWith("# Parameter:"))
{
continue;
}
var paramParts = line.Split("=");
if(paramParts.Length < 2)
{
continue;
}
if(paramParts[0] == "# Parameter:MssqlDataDockerVolume" &&
bool.TryParse(paramParts[1], out var mssqlDataDockerVolume))
{
Config.DatabaseDockerVolume = mssqlDataDockerVolume;
continue;
}
if(paramParts[0] == "# Parameter:HttpPort" && int.TryParse(paramParts[1], out var httpPort))
{
Config.HttpPort = httpPort == 0 ? null : httpPort.ToString();
continue;
}
if(paramParts[0] == "# Parameter:HttpsPort" && int.TryParse(paramParts[1], out var httpsPort))
{
Config.HttpsPort = httpsPort == 0 ? null : httpsPort.ToString();
continue;
}
}
}
var nginxFile = "/bitwarden/nginx/default.conf";
if(File.Exists(nginxFile))
{
var selfSigned = false;
var diffieHellman = false;
var trusted = false;
var fileLines = File.ReadAllLines(nginxFile);
foreach(var line in fileLines)
{
if(!line.StartsWith("# Parameter:"))
{
continue;
}
var paramParts = line.Split("=");
if(paramParts.Length < 2)
{
continue;
}
if(paramParts[0] == "# Parameter:Ssl" && bool.TryParse(paramParts[1], out var ssl))
{
Config.Ssl = ssl;
continue;
}
if(paramParts[0] == "# Parameter:LetsEncrypt" && bool.TryParse(paramParts[1], out var le))
{
Config.SslManagedLetsEncrypt = le;
continue;
}
if(paramParts[0] == "# Parameter:SelfSignedSsl" && bool.TryParse(paramParts[1], out var self))
{
selfSigned = self;
return;
}
if(paramParts[0] == "# Parameter:DiffieHellman" && bool.TryParse(paramParts[1], out var dh))
{
diffieHellman = dh;
return;
}
if(paramParts[0] == "# Parameter:Trusted" && bool.TryParse(paramParts[1], out var trust))
{
trusted = trust;
return;
}
}
if(Config.SslManagedLetsEncrypt)
{
Config.Ssl = true;
}
else if(Config.Ssl)
{
var sslPath = selfSigned ? $"/etc/ssl/self/{Config.Domain}" : $"/etc/ssl/{Config.Domain}";
Config.SslCertificatePath = string.Concat(sslPath, "/", "certificate.crt");
Config.SslKeyPath = string.Concat(sslPath, "/", "private.key");
if(trusted)
{
Config.SslCaPath = string.Concat(sslPath, "/", "ca.crt");
}
if(diffieHellman)
{
Config.SslDiffieHellmanPath = string.Concat(sslPath, "/", "dhparam.pem");
}
}
}
SaveConfiguration();
}
var configText = File.ReadAllText(ConfigPath);
var deserializer = new DeserializerBuilder()
.WithNamingConvention(new UnderscoredNamingConvention())
.Build();
Config = deserializer.Deserialize<Configuration>(configText);
}
public void SaveConfiguration()
{
if(Config == null)
{
throw new Exception("Config is null.");
}
var serializer = new SerializerBuilder()
.EmitDefaults()
.WithNamingConvention(new UnderscoredNamingConvention())
.WithTypeInspector(inner => new CommentGatheringTypeInspector(inner))
.WithEmissionPhaseObjectGraphVisitor(args => new CommentsObjectGraphVisitor(args.InnerVisitor))
.Build();
var yaml = serializer.Serialize(Config);
Directory.CreateDirectory("/bitwarden/");
using(var sw = File.CreateText(ConfigPath))
{
sw.Write(yaml);
}
}
public class Installation
{
public Guid InstallationId { get; set; }
public string InstallationKey { get; set; }
public bool DiffieHellman { get; set; }
public bool Trusted { get; set; }
public bool SelfSignedCert { get; set; }
public string IdentityCertPassword { get; set; }
public string Domain { get; set; }
}
public class Configuration
{
[Description("Full URL for accessing the installation from a browser. (Required)")]
public string Url { get; set; } = "https://localhost";
[Description("Auto-generate the `./docker/docker-compose.yml` config file.\n" +
"WARNING: Disabling generated config files can break future updates. You will be responsible\n" +
"for maintaining this config file.")]
public bool GenerateComposeConfig { get; set; } = true;
[Description("Auto-generate the `./nginx/default.conf` file.\n" +
"WARNING: Disabling generated config files can break future updates. You will be responsible\n" +
"for maintaining this config file.")]
public bool GenerateNginxConfig { get; set; } = true;
[Description("Compose file port mapping for HTTP. Leave empty for remove the port mapping.")]
public string HttpPort { get; set; } = "80";
[Description("Compose file port mapping for HTTPS. Leave empty for remove the port mapping.")]
public string HttpsPort { get; set; } = "443";
[Description("Set up the Nginx config file for SSL.")]
public bool Ssl { get; set; } = true;
[Description("Installation uses a managed Let's Encrypt certificate.")]
public bool SslManagedLetsEncrypt { get; set; }
[Description("The actual certificate. (Required if using SSL without managed Let's Encrypt)\n" +
"Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")]
public string SslCertificatePath { get; set; }
[Description("The certificate's private key. (Required if using SSL without managed Let's Encrypt)\n" +
"Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")]
public string SslKeyPath { get; set; }
[Description("If the certificate is trusted by a CA, you should provide the CA's certificate.\n" +
"Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")]
public string SslCaPath { get; set; }
[Description("Diffie Hellman ephemeral parameters\n" +
"Learn more: https://security.stackexchange.com/q/94390/79072\n" +
"Note: The `./ssl` directory is mapped to `/etc/ssl` within the container.")]
public string SslDiffieHellmanPath { get; set; }
[Description("Communicate with the Bitwarden push relay service (push.bitwarden.com) for mobile app live sync.")]
public bool PushNotifications { get; set; } = true;
[Description("Use a docker volume instead of a host-mapped volume for the persisted database.\n" +
"WARNING: Changing this value will cause you to lose access to the existing persisted database.")]
public bool DatabaseDockerVolume { get; set; }
[YamlIgnore]
public string Domain
{
get
{
if(Uri.TryCreate(Url, UriKind.Absolute, out var uri))
{
return uri.Host;
}
return null;
}
}
}
}
}

View File

@ -5,245 +5,58 @@ namespace Bit.Setup
{
public class DockerComposeBuilder
{
public DockerComposeBuilder(string os, string webVersion, string coreVersion)
{
MssqlDataDockerVolume = os == "mac";
private readonly Context _context;
if(!string.IsNullOrWhiteSpace(webVersion))
{
WebVersion = webVersion;
}
if(!string.IsNullOrWhiteSpace(coreVersion))
{
CoreVersion = coreVersion;
}
public DockerComposeBuilder(Context context)
{
_context = context;
}
public bool MssqlDataDockerVolume { get; private set; }
public int HttpPort { get; private set; }
public int HttpsPort { get; private set; }
public string CoreVersion { get; private set; } = "latest";
public string WebVersion { get; private set; } = "latest";
public void BuildForInstaller(int httpPort, int httpsPort)
public void BuildForInstaller()
{
if(httpPort != default(int))
{
HttpPort = httpPort;
}
if(httpsPort != default(int))
{
HttpsPort = httpsPort;
}
_context.Config.DatabaseDockerVolume = _context.HostOS == "mac";
Build();
}
public void BuildForUpdater()
{
var composeFile = "/bitwarden/docker/docker-compose.yml";
if(File.Exists(composeFile))
{
var fileLines = File.ReadAllLines(composeFile);
foreach(var line in fileLines)
{
if(!line.StartsWith("# Parameter:"))
{
continue;
}
var paramParts = line.Split("=");
if(paramParts.Length < 2)
{
continue;
}
if(paramParts[0] == "# Parameter:MssqlDataDockerVolume" &&
bool.TryParse(paramParts[1], out var mssqlDataDockerVolume))
{
MssqlDataDockerVolume = mssqlDataDockerVolume;
continue;
}
if(paramParts[0] == "# Parameter:HttpPort" && int.TryParse(paramParts[1], out var httpPort))
{
HttpPort = httpPort;
continue;
}
if(paramParts[0] == "# Parameter:HttpsPort" && int.TryParse(paramParts[1], out var httpsPort))
{
HttpsPort = httpsPort;
continue;
}
}
}
Build();
}
private void Build()
{
Console.WriteLine("Building docker-compose.yml.");
Directory.CreateDirectory("/bitwarden/docker/");
Console.WriteLine("Building docker-compose.yml.");
if(!_context.Config.GenerateComposeConfig)
{
Console.WriteLine("...skipped");
return;
}
var template = Helpers.ReadTemplate("DockerCompose");
var model = new TemplateModel(_context);
using(var sw = File.CreateText("/bitwarden/docker/docker-compose.yml"))
{
sw.Write($@"# https://docs.docker.com/compose/compose-file/
# Parameter:MssqlDataDockerVolume={MssqlDataDockerVolume}
# Parameter:HttpPort={HttpPort}
# Parameter:HttpsPort={HttpsPort}
# Parameter:CoreVersion={CoreVersion}
# Parameter:WebVersion={WebVersion}
version: '3'
services:
mssql:
image: bitwarden/mssql:{CoreVersion}
container_name: bitwarden-mssql
restart: always
volumes:");
if(MssqlDataDockerVolume)
{
sw.Write(@"
- mssql_data:/var/opt/mssql/data");
}
else
{
sw.Write(@"
- ../mssql/data:/var/opt/mssql/data");
}
sw.Write($@"
- ../logs/mssql:/var/opt/mssql/log
- ../mssql/backups:/etc/bitwarden/mssql/backups
env_file:
- mssql.env
- ../env/uid.env
- ../env/mssql.override.env
web:
image: bitwarden/web:{WebVersion}
container_name: bitwarden-web
restart: always
volumes:
- ../web:/etc/bitwarden/web
env_file:
- global.env
- ../env/uid.env
attachments:
image: bitwarden/attachments:{CoreVersion}
container_name: bitwarden-attachments
restart: always
volumes:
- ../core/attachments:/etc/bitwarden/core/attachments
env_file:
- global.env
- ../env/uid.env
api:
image: bitwarden/api:{CoreVersion}
container_name: bitwarden-api
restart: always
volumes:
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/api:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
identity:
image: bitwarden/identity:{CoreVersion}
container_name: bitwarden-identity
restart: always
volumes:
- ../identity:/etc/bitwarden/identity
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/identity:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
admin:
image: bitwarden/admin:{CoreVersion}
container_name: bitwarden-admin
restart: always
volumes:
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/admin:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
icons:
image: bitwarden/icons:{CoreVersion}
container_name: bitwarden-icons
restart: always
volumes:
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/icons:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
notifications:
image: bitwarden/notifications:{CoreVersion}
container_name: bitwarden-notifications
restart: always
volumes:
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/notifications:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
nginx:
image: bitwarden/nginx:{CoreVersion}
container_name: bitwarden-nginx
restart: always
ports:");
if(HttpPort != default(int))
{
sw.Write($@"
- '{HttpPort}:8080'");
}
if(HttpsPort != default(int))
{
sw.Write($@"
- '{HttpsPort}:8443'");
}
sw.Write($@"
volumes:
- ../nginx:/etc/bitwarden/nginx
- ../letsencrypt:/etc/letsencrypt
- ../ssl:/etc/ssl
- ../logs/nginx:/var/log/nginx
env_file:
- ../env/uid.env");
if(MssqlDataDockerVolume)
{
sw.Write(@"
volumes:
mssql_data:");
}
// New line at end of file.
sw.Write("\n");
sw.Write(template(model));
}
}
public class TemplateModel
{
public TemplateModel(Context context)
{
MssqlDataDockerVolume = context.Config.DatabaseDockerVolume;
HttpPort = context.Config.HttpPort;
HttpsPort = context.Config.HttpsPort;
CoreVersion = context.CoreVersion;
WebVersion = context.WebVersion;
}
public bool MssqlDataDockerVolume { get; set; }
public string HttpPort { get; set; }
public string HttpsPort { get; set; }
public string CoreVersion { get; set; } = "latest";
public string WebVersion { get; set; } = "latest";
}
}
}

View File

@ -1,62 +1,95 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace Bit.Setup
{
public class EnvironmentFileBuilder
{
private readonly Context _context;
private IDictionary<string, string> _globalValues;
private IDictionary<string, string> _mssqlValues;
private IDictionary<string, string> _globalOverrideValues;
private IDictionary<string, string> _mssqlOverrideValues;
public string Url { get; set; } = "https://localhost";
public string Domain { get; set; } = "localhost";
public string IdentityCertPassword { get; set; } = "REPLACE";
public Guid? InstallationId { get; set; }
public string InstallationKey { get; set; }
public bool Push { get; set; }
public string DatabasePassword { get; set; } = "REPLACE";
public string OutputDirectory { get; set; } = ".";
public EnvironmentFileBuilder(Context context)
{
_context = context;
_globalValues = new Dictionary<string, string>
{
["ASPNETCORE_ENVIRONMENT"] = "Production",
["globalSettings__selfHosted"] = "true",
["globalSettings__baseServiceUri__vault"] = "http://localhost",
["globalSettings__baseServiceUri__api"] = "http://localhost/api",
["globalSettings__baseServiceUri__identity"] = "http://localhost/identity",
["globalSettings__baseServiceUri__admin"] = "http://localhost/admin",
["globalSettings__baseServiceUri__notifications"] = "http://localhost/notifications",
["globalSettings__baseServiceUri__internalNotifications"] = "http://notifications:5000",
["globalSettings__baseServiceUri__internalAdmin"] = "http://admin:5000",
["globalSettings__baseServiceUri__internalIdentity"] = "http://identity:5000",
["globalSettings__baseServiceUri__internalApi"] = "http://api:5000",
["globalSettings__baseServiceUri__internalVault"] = "http://web:5000",
["globalSettings__pushRelayBaseUri"] = "https://push.bitwarden.com",
["globalSettings__installation__identityUri"] = "https://identity.bitwarden.com",
};
_mssqlValues = new Dictionary<string, string>
{
["ACCEPT_EULA"] = "Y",
["MSSQL_PID"] = "Express",
["SA_PASSWORD"] = "SECRET",
};
}
public void BuildForInstaller()
{
Directory.CreateDirectory("/bitwarden/env/");
Init(true);
Init();
Build();
}
public void BuildForUpdater()
{
Init(false);
LoadExistingValues(_globalValues, "/bitwarden/env/global.override.env");
LoadExistingValues(_mssqlValues, "/bitwarden/env/mssql.override.env");
Init();
LoadExistingValues(_globalOverrideValues, "/bitwarden/env/global.override.env");
LoadExistingValues(_mssqlOverrideValues, "/bitwarden/env/mssql.override.env");
if(_context.Config.PushNotifications &&
_globalOverrideValues.ContainsKey("globalSettings__pushRelayBaseUri") &&
_globalOverrideValues["globalSettings__pushRelayBaseUri"] == "REPLACE")
{
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
}
Build();
}
private void Init(bool forInstall)
private void Init()
{
var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", DatabasePassword);
_globalValues = new Dictionary<string, string>
var dbPassword = Helpers.SecureRandomString(32);
var dbConnectionString = Helpers.MakeSqlConnectionString("mssql", "vault", "sa", dbPassword);
_globalOverrideValues = new Dictionary<string, string>
{
["globalSettings__baseServiceUri__vault"] = Url,
["globalSettings__baseServiceUri__api"] = $"{Url}/api",
["globalSettings__baseServiceUri__identity"] = $"{Url}/identity",
["globalSettings__baseServiceUri__admin"] = $"{Url}/admin",
["globalSettings__baseServiceUri__notifications"] = $"{Url}/notifications",
["globalSettings__sqlServer__connectionString"] = $"\"{ dbConnectionString }\"",
["globalSettings__identityServer__certificatePassword"] = IdentityCertPassword,
["globalSettings__attachment__baseDirectory"] = $"{OutputDirectory}/core/attachments",
["globalSettings__attachment__baseUrl"] = $"{Url}/attachments",
["globalSettings__dataProtection__directory"] = $"{OutputDirectory}/core/aspnet-dataprotection",
["globalSettings__logDirectory"] = $"{OutputDirectory}/logs",
["globalSettings__licenseDirectory"] = $"{OutputDirectory}/core/licenses",
["globalSettings__baseServiceUri__vault"] = _context.Config.Url,
["globalSettings__baseServiceUri__api"] = $"{_context.Config.Url}/api",
["globalSettings__baseServiceUri__identity"] = $"{_context.Config.Url}/identity",
["globalSettings__baseServiceUri__admin"] = $"{_context.Config.Url}/admin",
["globalSettings__baseServiceUri__notifications"] = $"{_context.Config.Url}/notifications",
["globalSettings__sqlServer__connectionString"] = $"\"{dbConnectionString}\"",
["globalSettings__identityServer__certificatePassword"] = _context.Install?.IdentityCertPassword,
["globalSettings__attachment__baseDirectory"] = $"{_context.OutputDir}/core/attachments",
["globalSettings__attachment__baseUrl"] = $"{_context.Config.Url}/attachments",
["globalSettings__dataProtection__directory"] = $"{_context.OutputDir}/core/aspnet-dataprotection",
["globalSettings__logDirectory"] = $"{_context.OutputDir}/logs",
["globalSettings__licenseDirectory"] = $"{_context.OutputDir}/core/licenses",
["globalSettings__internalIdentityKey"] = Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__duo__aKey"] = Helpers.SecureRandomString(64, alpha: true, numeric: true),
["globalSettings__installation__id"] = InstallationId?.ToString(),
["globalSettings__installation__key"] = InstallationKey,
["globalSettings__installation__id"] = _context.Install?.InstallationId.ToString(),
["globalSettings__installation__key"] = _context.Install?.InstallationKey,
["globalSettings__yubico__clientId"] = "REPLACE",
["globalSettings__yubico__key"] = "REPLACE",
["globalSettings__mail__replyToEmail"] = $"no-reply@{Domain}",
["globalSettings__mail__replyToEmail"] = $"no-reply@{_context.Config.Domain}",
["globalSettings__mail__smtp__host"] = "REPLACE",
["globalSettings__mail__smtp__username"] = "REPLACE",
["globalSettings__mail__smtp__password"] = "REPLACE",
@ -67,16 +100,16 @@ namespace Bit.Setup
["adminSettings__admins"] = string.Empty,
};
if(forInstall && !Push)
if(!_context.Config.PushNotifications)
{
_globalValues.Add("globalSettings__pushRelayBaseUri", "REPLACE");
_globalOverrideValues.Add("globalSettings__pushRelayBaseUri", "REPLACE");
}
_mssqlValues = new Dictionary<string, string>
_mssqlOverrideValues = new Dictionary<string, string>
{
["ACCEPT_EULA"] = "Y",
["MSSQL_PID"] = "Express",
["SA_PASSWORD"] = DatabasePassword,
["SA_PASSWORD"] = dbPassword,
};
}
@ -120,59 +153,34 @@ namespace Bit.Setup
private void Build()
{
var template = Helpers.ReadTemplate("EnvironmentFile");
Console.WriteLine("Building docker environment files.");
Directory.CreateDirectory("/bitwarden/docker/");
using(var sw = File.CreateText("/bitwarden/docker/global.env"))
{
sw.Write($@"ASPNETCORE_ENVIRONMENT=Production
globalSettings__selfHosted=true
globalSettings__baseServiceUri__vault=http://localhost
globalSettings__baseServiceUri__api=http://localhost/api
globalSettings__baseServiceUri__identity=http://localhost/identity
globalSettings__baseServiceUri__admin=http://localhost/admin
globalSettings__baseServiceUri__notifications=http://localhost/notifications
globalSettings__baseServiceUri__internalNotifications=http://notifications:5000
globalSettings__baseServiceUri__internalAdmin=http://admin:5000
globalSettings__baseServiceUri__internalIdentity=http://identity:5000
globalSettings__baseServiceUri__internalApi=http://api:5000
globalSettings__baseServiceUri__internalVault=http://web:5000
globalSettings__pushRelayBaseUri=https://push.bitwarden.com
globalSettings__installation__identityUri=https://identity.bitwarden.com
");
sw.Write(template(new TemplateModel(_globalValues)));
}
Helpers.Exec("chmod 600 /bitwarden/docker/global.env");
using(var sw = File.CreateText("/bitwarden/docker/mssql.env"))
{
sw.Write($@"ACCEPT_EULA=Y
MSSQL_PID=Express
SA_PASSWORD=SECRET
");
sw.Write(template(new TemplateModel(_mssqlValues)));
}
Helpers.Exec("chmod 600 /bitwarden/docker/mssql.env");
Console.WriteLine("Building docker environment override files.");
Directory.CreateDirectory(" /bitwarden/env/");
Directory.CreateDirectory("/bitwarden/env/");
using(var sw = File.CreateText("/bitwarden/env/global.override.env"))
{
foreach(var item in _globalValues)
{
sw.WriteLine($"{item.Key}={item.Value}");
}
sw.Write(template(new TemplateModel(_globalOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/global.override.env");
using(var sw = File.CreateText("/bitwarden/env/mssql.override.env"))
{
foreach(var item in _mssqlValues)
{
sw.WriteLine($"{item.Key}={item.Value}");
}
sw.Write(template(new TemplateModel(_mssqlOverrideValues)));
}
Helpers.Exec("chmod 600 /bitwarden/env/mssql.override.env");
// Empty uid env file. Only used on Linux hosts.
@ -181,5 +189,21 @@ SA_PASSWORD=SECRET
using(var sw = File.CreateText("/bitwarden/env/uid.env")) { }
}
}
public class TemplateModel
{
public TemplateModel(IEnumerable<KeyValuePair<string, string>> variables)
{
Variables = variables.Select(v => new Kvp { Key = v.Key, Value = v.Value });
}
public IEnumerable<Kvp> Variables { get; set; }
public class Kvp
{
public string Key { get; set; }
public string Value { get; set; }
}
}
}
}

View File

@ -2,6 +2,8 @@
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@ -192,5 +194,21 @@ namespace Bit.Setup
Console.WriteLine();
Console.ResetColor();
}
public static Func<object, string> 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);
}
}
}
}

View File

@ -16,177 +16,99 @@ namespace Bit.Setup
"child-src 'self' https://*.duosecurity.com; frame-src 'self' https://*.duosecurity.com; " +
"connect-src 'self' wss://{0} https://haveibeenpwned.com https://api.pwnedpasswords.com;";
public NginxConfigBuilder(string domain, string url, bool ssl, bool selfSignedSsl, bool letsEncrypt,
bool trusted, bool diffieHellman)
{
Domain = domain;
Url = url;
Ssl = ssl;
SelfSignedSsl = selfSignedSsl;
LetsEncrypt = letsEncrypt;
Trusted = trusted;
DiffieHellman = diffieHellman;
}
private readonly Context _context;
public NginxConfigBuilder(string domain, string url)
public NginxConfigBuilder(Context context)
{
Domain = domain;
Url = url;
_context = context;
}
public bool Ssl { get; private set; }
public bool SelfSignedSsl { get; private set; }
public bool LetsEncrypt { get; private set; }
public string Domain { get; private set; }
public string Url { get; private set; }
public bool DiffieHellman { get; private set; }
public bool Trusted { get; private set; }
public void BuildForInstaller()
{
Build();
var model = new TemplateModel(_context);
if(model.Ssl && !_context.Config.SslManagedLetsEncrypt)
{
var sslPath = _context.Install.SelfSignedCert ?
$"/etc/ssl/self/{model.Domain}" : $"/etc/ssl/{model.Domain}";
_context.Config.SslCertificatePath = model.CertificatePath =
string.Concat(sslPath, "/", "certificate.crt");
_context.Config.SslKeyPath = model.KeyPath =
string.Concat(sslPath, "/", "private.key");
if(_context.Install.Trusted)
{
_context.Config.SslCaPath = model.CaPath =
string.Concat(sslPath, "/", "ca.crt");
}
if(_context.Install.DiffieHellman)
{
_context.Config.SslDiffieHellmanPath = model.DiffieHellmanPath =
string.Concat(sslPath, "/", "dhparam.pem");
}
}
Build(model);
}
public void BuildForUpdater()
{
if(File.Exists(ConfFile))
{
var confContent = File.ReadAllText(ConfFile);
Ssl = confContent.Contains("ssl http2;");
SelfSignedSsl = confContent.Contains("/etc/ssl/self/");
LetsEncrypt = !SelfSignedSsl && confContent.Contains("/etc/letsencrypt/live/");
DiffieHellman = confContent.Contains("/dhparam.pem;");
Trusted = confContent.Contains("ssl_trusted_certificate ");
}
Build();
var model = new TemplateModel(_context);
Build(model);
}
private void Build()
private void Build(TemplateModel model)
{
Directory.CreateDirectory("/bitwarden/nginx/");
var sslPath = LetsEncrypt ? $"/etc/letsencrypt/live/{Domain}" :
SelfSignedSsl ? $"/etc/ssl/self/{Domain}" : $"/etc/ssl/{Domain}";
var certFile = LetsEncrypt ? "fullchain.pem" : "certificate.crt";
var keyFile = LetsEncrypt ? "privkey.pem" : "private.key";
var caFile = LetsEncrypt ? "fullchain.pem" : "ca.crt";
Console.WriteLine("Building nginx config.");
if(!_context.Config.GenerateNginxConfig)
{
Console.WriteLine("...skipped");
return;
}
var template = Helpers.ReadTemplate("NginxConfig");
using(var sw = File.CreateText(ConfFile))
{
sw.WriteLine($@"# Config Parameters
# Parameter:Ssl={Ssl}
# Parameter:SelfSignedSsl={SelfSignedSsl}
# Parameter:LetsEncrypt={LetsEncrypt}
# Parameter:Domain={Domain}
# Parameter:Url={Url}
# Parameter:DiffieHellman={DiffieHellman}
# Parameter:Trusted={Trusted}
sw.WriteLine(template(model));
}
}
server {{
listen 8080 default_server;
listen [::]:8080 default_server;
server_name {Domain};");
public class TemplateModel
{
public TemplateModel() { }
public TemplateModel(Context context)
{
Ssl = context.Config.Ssl;
Domain = context.Config.Domain;
Url = context.Config.Url;
if(Ssl)
{
sw.WriteLine($@" return 301 {Url}$request_uri;
}}
server {{
listen 8443 ssl http2;
listen [::]:8443 ssl http2;
server_name {Domain};
ssl_certificate {sslPath}/{certFile};
ssl_certificate_key {sslPath}/{keyFile};
ssl_session_timeout 30m;
ssl_session_cache shared:SSL:20m;
ssl_session_tickets off;");
if(DiffieHellman)
if(context.Config.SslManagedLetsEncrypt)
{
sw.WriteLine($@"
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam {sslPath}/dhparam.pem;");
var sslPath = $"/etc/letsencrypt/live/{Domain}";
CertificatePath = CaPath = string.Concat(sslPath, "/", "fullchain.pem");
KeyPath = string.Concat(sslPath, "/", "privkey.pem");
DiffieHellmanPath = string.Concat(sslPath, "/", "dhparam.pem");
}
sw.WriteLine($@"
# SSL protocol TLSv1.2 is allowed. Disabled SSLv3, TLSv1, and TLSv1.1
ssl_protocols TLSv1.2;
# Enable most secure cipher suites only.
ssl_ciphers ""{SslCiphers}"";
# Enables server-side protection from BEAST attacks
ssl_prefer_server_ciphers on;");
if(Trusted)
else
{
sw.WriteLine($@"
# OCSP Stapling ---
# Fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
# Verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate {sslPath}/{caFile};
resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;
# This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age
add_header Strict-Transport-Security max-age=15768000;");
CertificatePath = context.Config.SslCertificatePath;
KeyPath = context.Config.SslKeyPath;
CaPath = context.Config.SslCaPath;
DiffieHellmanPath = context.Config.SslDiffieHellmanPath;
}
}
sw.WriteLine($@"
location / {{
proxy_pass http://web:5000/;
# Security headers
#add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection ""1; mode=block"";
add_header Referrer-Policy same-origin;
add_header Content-Security-Policy ""{string.Format(ContentSecurityPolicy, Domain)}"";
}}
location = /app-id.json {{
proxy_pass http://web:5000/app-id.json;
proxy_hide_header Content-Type;
add_header Content-Type $fido_content_type;
}}
location /attachments/ {{
proxy_pass http://attachments:5000/;
}}
location /api/ {{
proxy_pass http://api:5000/;
}}
location /identity/ {{
proxy_pass http://identity:5000/;
}}
location /icons/ {{
proxy_pass http://icons:5000/;
}}
location /notifications/ {{
proxy_pass http://notifications:5000/;
}}
location /notifications/hub {{
proxy_pass http://notifications:5000/hub;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}}
location /admin {{
proxy_pass http://admin:5000;
}}
}}");
}
public bool Ssl { get; set; }
public string Domain { get; set; }
public string Url { get; set; }
public string CertificatePath { get; set; }
public string KeyPath { get; set; }
public string CaPath { get; set; }
public string DiffieHellmanPath { get; set; }
public string ContentSecurityPolicy => string.Format(NginxConfigBuilder.ContentSecurityPolicy, Domain);
public string SslCiphers => NginxConfigBuilder.SslCiphers;
}
}
}

View File

@ -5,48 +5,44 @@ using System.Collections.Generic;
using System.Data.SqlClient;
using System.Net.Http;
using System.Reflection;
using System.IO;
namespace Bit.Setup
{
public class Program
{
private static string[] _args = null;
private static IDictionary<string, string> _parameters = null;
private static Guid? _installationId = null;
private static string _installationKey = null;
private static string _hostOs = "win";
private static string _coreVersion = "latest";
private static string _webVersion = "latest";
private static Context _context;
public static void Main(string[] args)
{
Console.WriteLine();
_context = new Context
{
Args = args
};
ParseParameters();
_args = args;
_parameters = ParseParameters();
if(_parameters.ContainsKey("os"))
if(_context.Parameters.ContainsKey("os"))
{
_hostOs = _parameters["os"];
_context.HostOS = _context.Parameters["os"];
}
if(_parameters.ContainsKey("corev"))
if(_context.Parameters.ContainsKey("corev"))
{
_coreVersion = _parameters["corev"];
_context.CoreVersion = _context.Parameters["corev"];
}
if(_parameters.ContainsKey("webv"))
if(_context.Parameters.ContainsKey("webv"))
{
_webVersion = _parameters["webv"];
_context.WebVersion = _context.Parameters["webv"];
}
if(_parameters.ContainsKey("install"))
if(_context.Parameters.ContainsKey("install"))
{
Install();
}
else if(_parameters.ContainsKey("update"))
else if(_context.Parameters.ContainsKey("update"))
{
Update();
}
else if(_parameters.ContainsKey("printenv"))
else if(_context.Parameters.ContainsKey("printenv"))
{
PrintEnvironment();
}
@ -58,147 +54,46 @@ namespace Bit.Setup
private static void Install()
{
var outputDir = _parameters.ContainsKey("out") ?
_parameters["out"].ToLowerInvariant() : "/etc/bitwarden";
var domain = _parameters.ContainsKey("domain") ?
_parameters["domain"].ToLowerInvariant() : "localhost";
var letsEncrypt = _parameters.ContainsKey("letsencrypt") ?
_parameters["letsencrypt"].ToLowerInvariant() == "y" : false;
if(_context.Parameters.ContainsKey("letsencrypt"))
{
_context.Config.SslManagedLetsEncrypt =
_context.Parameters["letsencrypt"].ToLowerInvariant() == "y";
}
if(_context.Parameters.ContainsKey("domain"))
{
_context.Install.Domain = _context.Parameters["domain"].ToLowerInvariant();
}
if(!ValidateInstallation())
{
return;
}
var ssl = letsEncrypt;
if(!letsEncrypt)
{
ssl = Helpers.ReadQuestion("Do you have a SSL certificate to use?");
if(ssl)
{
Directory.CreateDirectory($"/bitwarden/ssl/{domain}/");
var message = "Make sure 'certificate.crt' and 'private.key' are provided in the \n" +
"appropriate directory before running 'start' (see docs for info).";
Helpers.ShowBanner("NOTE", message);
}
}
var certBuilder = new CertBuilder(_context);
certBuilder.BuildForInstall();
// Set the URL
_context.Config.Url = string.Format("http{0}://{1}",
_context.Config.Ssl ? "s" : string.Empty, _context.Install.Domain);
var identityCertPassword = Helpers.SecureRandomString(32, alpha: true, numeric: true);
var certBuilder = new CertBuilder(domain, identityCertPassword, letsEncrypt, ssl);
var selfSignedSsl = certBuilder.BuildForInstall();
ssl = certBuilder.Ssl; // Ssl prop can get flipped during the build
var sslTrusted = letsEncrypt;
var sslDiffieHellman = letsEncrypt;
if(ssl && !selfSignedSsl && !letsEncrypt)
{
sslDiffieHellman = Helpers.ReadQuestion("Use Diffie Hellman ephemeral parameters for SSL " +
"(requires dhparam.pem, see docs)?");
sslTrusted = Helpers.ReadQuestion("Is this a trusted SSL certificate (requires ca.crt, see docs)?");
}
if(!ssl)
{
var message = "You are not using a SSL certificate. Bitwarden requires HTTPS to operate. \n" +
"You must front your installation with a HTTPS proxy. The web vault (and \n" +
"other Bitwarden apps) will not work properly without HTTPS.";
Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow);
}
else if(ssl && !sslTrusted)
{
var message = "You are using an untrusted SSL certificate. This certificate will not be \n" +
"trusted by Bitwarden client applications. You must add this certificate to \n" +
"the trusted store on each device or else you will receive errors when trying \n" +
"to connect to your installation.";
Helpers.ShowBanner("WARNING", message, ConsoleColor.Yellow);
}
var url = $"https://{domain}";
int httpPort = default(int), httpsPort = default(int);
if(Helpers.ReadQuestion("Do you want to use the default ports for HTTP (80) and HTTPS (443)?"))
{
httpPort = 80;
if(ssl)
{
httpsPort = 443;
}
}
else if(ssl)
{
httpsPort = 443;
if(int.TryParse(Helpers.ReadInput("HTTPS port").Trim(), out httpsPort) && httpsPort != 443)
{
url += (":" + httpsPort);
}
else
{
Console.WriteLine("Using default port.");
}
}
else
{
httpPort = 80;
if(!int.TryParse(Helpers.ReadInput("HTTP port").Trim(), out httpPort) && httpPort != 80)
{
Console.WriteLine("Using default port.");
}
}
if(Helpers.ReadQuestion("Is your installation behind a reverse proxy?"))
{
if(Helpers.ReadQuestion("Do you use the default HTTPS port (443) on your reverse proxy?"))
{
url = $"https://{domain}";
}
else
{
if(int.TryParse(Helpers.ReadInput("Proxy HTTPS port").Trim(), out var httpsReversePort)
&& httpsReversePort != 443)
{
url += (":" + httpsReversePort);
}
else
{
Console.WriteLine("Using default port.");
url = $"https://{domain}";
}
}
}
else if(!ssl)
{
Console.WriteLine("ERROR: You must use a reverse proxy if not using SSL.");
return;
}
var push = Helpers.ReadQuestion("Do you want to use push notifications?");
var nginxBuilder = new NginxConfigBuilder(domain, url, ssl, selfSignedSsl, letsEncrypt,
sslTrusted, sslDiffieHellman);
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForInstaller();
var environmentFileBuilder = new EnvironmentFileBuilder
{
DatabasePassword = Helpers.SecureRandomString(32),
Domain = domain,
IdentityCertPassword = identityCertPassword,
InstallationId = _installationId,
InstallationKey = _installationKey,
OutputDirectory = outputDir,
Push = push,
Url = url
};
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForInstaller();
var appIdBuilder = new AppIdBuilder(url);
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_hostOs, _webVersion, _coreVersion);
dockerComposeBuilder.BuildForInstaller(httpPort, httpsPort);
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForInstaller();
_context.SaveConfiguration();
}
private static void Update()
{
if(_parameters.ContainsKey("db"))
if(_context.Parameters.ContainsKey("db"))
{
MigrateDatabase();
}
@ -210,12 +105,12 @@ namespace Bit.Setup
private static void PrintEnvironment()
{
var vaultUrl = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault");
_context.LoadConfiguration();
Console.WriteLine("\nBitwarden is up and running!");
Console.WriteLine("===================================================");
Console.WriteLine("\nvisit {0}", vaultUrl);
Console.WriteLine("\nvisit {0}", _context.Config.Url);
Console.Write("to update, run ");
if(_hostOs == "win")
if(_context.HostOS == "win")
{
Console.Write("'.\\bitwarden.ps1 -updateself' and then '.\\bitwarden.ps1 -update'");
}
@ -296,13 +191,13 @@ namespace Bit.Setup
return false;
}
_installationId = installationidGuid;
_installationKey = Helpers.ReadInput("Enter your installation key");
_context.Install.InstallationId = installationidGuid;
_context.Install.InstallationKey = Helpers.ReadInput("Enter your installation key");
try
{
var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" + _installationId)
.GetAwaiter().GetResult();
var response = new HttpClient().GetAsync("https://api.bitwarden.com/installations/" +
_context.Install.InstallationId).GetAwaiter().GetResult();
if(!response.IsSuccessStatusCode)
{
@ -337,42 +232,35 @@ namespace Bit.Setup
private static void RebuildConfigs()
{
var environmentFileBuilder = new EnvironmentFileBuilder();
_context.LoadConfiguration();
var environmentFileBuilder = new EnvironmentFileBuilder(_context);
environmentFileBuilder.BuildForUpdater();
var url = Helpers.GetValueFronEnvFile("global", "globalSettings__baseServiceUri__vault");
if(!Uri.TryCreate(url, UriKind.Absolute, out Uri uri))
{
Console.WriteLine("Unable to determine existing installation url.");
return;
}
var domain = uri.Host;
var nginxBuilder = new NginxConfigBuilder(domain, url);
var nginxBuilder = new NginxConfigBuilder(_context);
nginxBuilder.BuildForUpdater();
var appIdBuilder = new AppIdBuilder(url);
var appIdBuilder = new AppIdBuilder(_context);
appIdBuilder.Build();
var dockerComposeBuilder = new DockerComposeBuilder(_hostOs, _webVersion, _coreVersion);
var dockerComposeBuilder = new DockerComposeBuilder(_context);
dockerComposeBuilder.BuildForUpdater();
_context.SaveConfiguration();
}
private static IDictionary<string, string> ParseParameters()
private static void ParseParameters()
{
var dict = new Dictionary<string, string>();
for(var i = 0; i < _args.Length; i = i + 2)
_context.Parameters = new Dictionary<string, string>();
for(var i = 0; i < _context.Args.Length; i = i + 2)
{
if(!_args[i].StartsWith("-"))
if(!_context.Args[i].StartsWith("-"))
{
continue;
}
dict.Add(_args[i].Substring(1), _args[i + 1]);
_context.Parameters.Add(_context.Args[i].Substring(1), _context.Args[i + 1]);
}
return dict;
}
}
}

View File

@ -9,12 +9,15 @@
<ItemGroup>
<EmbeddedResource Include="DbScripts\**\*.sql" />
<EmbeddedResource Include="Templates\**\*.hbs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Handlebars.Net" Version="1.9.5" />
<PackageReference Include="System.Data.SqlClient" Version="4.5.1" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="dbup" Version="3.3.5" />
<PackageReference Include="YamlDotNet" Version="5.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,15 @@
{
"trustedFacets": [
{
"version": {
"major": 1,
"minor": 0
},
"ids": [
"{{{Url}}}",
"ios:bundle-id:com.8bit.bitwarden",
"android:apk-key-hash:dUGFzUzf3lmHSLBDBIv+WaFyZMI"
]
}
]
}

View File

@ -0,0 +1,133 @@
# https://docs.docker.com/compose/compose-file/
#
# WARNING: This file is generated. Do not make changes to this file.
# They will be overwritten on update. If you want to make additions to
# this file, you can create a `docker-compose.override.yml` file in the
# same directory and it will be merged into this file at runtime.
version: '3'
services:
mssql:
image: bitwarden/mssql:{{{CoreVersion}}}
container_name: bitwarden-mssql
restart: always
volumes:
{{#if MssqlDataDockerVolume}}
- mssql_data:/var/opt/mssql/data
{{else}}
- ../mssql/data:/var/opt/mssql/data
{{/if}}
- ../logs/mssql:/var/opt/mssql/log
- ../mssql/backups:/etc/bitwarden/mssql/backups
env_file:
- mssql.env
- ../env/uid.env
- ../env/mssql.override.env
web:
image: bitwarden/web:{{{WebVersion}}}
container_name: bitwarden-web
restart: always
volumes:
- ../web:/etc/bitwarden/web
env_file:
- global.env
- ../env/uid.env
attachments:
image: bitwarden/attachments:{{{CoreVersion}}}
container_name: bitwarden-attachments
restart: always
volumes:
- ../core/attachments:/etc/bitwarden/core/attachments
env_file:
- global.env
- ../env/uid.env
api:
image: bitwarden/api:{{{CoreVersion}}}
container_name: bitwarden-api
restart: always
volumes:
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/api:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
identity:
image: bitwarden/identity:{{{CoreVersion}}}
container_name: bitwarden-identity
restart: always
volumes:
- ../identity:/etc/bitwarden/identity
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/identity:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
admin:
image: bitwarden/admin:{{{CoreVersion}}}
container_name: bitwarden-admin
restart: always
volumes:
- ../core:/etc/bitwarden/core
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/admin:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
icons:
image: bitwarden/icons:{{{CoreVersion}}}
container_name: bitwarden-icons
restart: always
volumes:
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/icons:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
notifications:
image: bitwarden/notifications:{{{CoreVersion}}}
container_name: bitwarden-notifications
restart: always
volumes:
- ../ca-certificates:/etc/bitwarden/ca-certificates
- ../logs/notifications:/etc/bitwarden/logs
env_file:
- global.env
- ../env/uid.env
- ../env/global.override.env
nginx:
image: bitwarden/nginx:{{{CoreVersion}}}
container_name: bitwarden-nginx
restart: always
ports:
{{#if HttpPort}}
- '{{{HttpPort}}}:8080'
{{/if}}
{{#if HttpsPort}}
- '{{{HttpsPort}}}:8443'
{{/if}}
volumes:
- ../nginx:/etc/bitwarden/nginx
- ../letsencrypt:/etc/letsencrypt
- ../ssl:/etc/ssl
- ../logs/nginx:/var/log/nginx
env_file:
- ../env/uid.env
{{#if MssqlDataDockerVolume}}
volumes:
mssql_data:
{{/if}}

View File

@ -0,0 +1,3 @@
{{#each Variables}}
{{{Key}}}={{{Value}}}
{{/each}}

View File

@ -0,0 +1,99 @@
# WARNING: This file is generated. Do not make changes to this file.
# They will be overwritten on update.
server {
listen 8080 default_server;
listen [::]:8080 default_server;
server_name {{{Domain}}};
{{#if Ssl}}
return 301 {{{Url}}}$request_uri;
}
server {
listen 8443 ssl http2;
listen [::]:8443 ssl http2;
server_name {{{Domain}}};
ssl_certificate {{{CertificatePath}}};
ssl_certificate_key {{{KeyPath}}};
ssl_session_timeout 30m;
ssl_session_cache shared:SSL:20m;
ssl_session_tickets off;
{{#if DiffieHellmanPath}}
# Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
ssl_dhparam {{{DiffieHellmanPath}}};
{{/if}}
# SSL protocol TLSv1.2 is allowed. Disabled SSLv3, TLSv1, and TLSv1.1
ssl_protocols TLSv1.2;
# Enable most secure cipher suites only.
ssl_ciphers "{{{SslCiphers}}}";
# Enables server-side protection from BEAST attacks
ssl_prefer_server_ciphers on;
{{#if CaPath}}
# OCSP Stapling ---
# Fetch OCSP records from URL in ssl_certificate and cache them
ssl_stapling on;
ssl_stapling_verify on;
# Verify chain of trust of OCSP response using Root CA and Intermediate certs
ssl_trusted_certificate {{{CaPath}}};
resolver 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=300s;
{{/if}}
{{/if}}
# Security headers
add_header Referrer-Policy same-origin;
#add_header X-Frame-Options SAMEORIGIN;
{{#if Ssl}}
add_header X-Content-Type-Options nosniff;
# This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack. 6 months age
add_header Strict-Transport-Security max-age=15768000;
{{/if}}
location / {
proxy_pass http://web:5000/;
# Security headers
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "{{{ContentSecurityPolicy}}}";
}
location = /app-id.json {
proxy_pass http://web:5000/app-id.json;
proxy_hide_header Content-Type;
add_header Content-Type $fido_content_type;
}
location /attachments/ {
proxy_pass http://attachments:5000/;
}
location /api/ {
proxy_pass http://api:5000/;
}
location /identity/ {
proxy_pass http://identity:5000/;
}
location /icons/ {
proxy_pass http://icons:5000/;
}
location /notifications/ {
proxy_pass http://notifications:5000/;
}
location /notifications/hub {
proxy_pass http://notifications:5000/hub;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
location /admin {
proxy_pass http://admin:5000;
}
}

111
util/Setup/YamlComments.cs Normal file
View File

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.ObjectGraphVisitors;
using YamlDotNet.Serialization.TypeInspectors;
// ref: https://github.com/aaubry/YamlDotNet/issues/152#issuecomment-349034754
namespace Bit.Setup
{
public class CommentGatheringTypeInspector : TypeInspectorSkeleton
{
private readonly ITypeInspector _innerTypeDescriptor;
public CommentGatheringTypeInspector(ITypeInspector innerTypeDescriptor)
{
_innerTypeDescriptor = innerTypeDescriptor ?? throw new ArgumentNullException(nameof(innerTypeDescriptor));
}
public override IEnumerable<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);
}
}
}
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);
}
}
}