diff --git a/.dockerignore b/.dockerignore index f181d71c..ea060dc7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,2 +1,3 @@ **/bin **/obj +**/node_modules diff --git a/.gitignore b/.gitignore index f78b6c99..bf874ca3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,15 @@ dist/ build/ !dev-server.shared.pem config/local.json + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ diff --git a/bitwarden-web.sln b/bitwarden-web.sln new file mode 100644 index 00000000..123c0ab4 --- /dev/null +++ b/bitwarden-web.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "dotnet-src\Web\Web.csproj", "{D0B6D8EB-21F0-400A-91E5-2C4722B9D170}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0B6D8EB-21F0-400A-91E5-2C4722B9D170}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/docker/Dockerfile b/docker/Dockerfile index 433099da..a4d61652 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,7 @@ ############################################### # Build stage # ############################################### -FROM node:16-slim AS build +FROM node:16-slim AS node-build RUN apt-get update \ && apt-get install -y --no-install-recommends \ @@ -14,19 +14,50 @@ COPY . . RUN npm ci RUN npm run dist:oss:selfhost +############################################### +# Build stage # +############################################### +FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS dotnet-build + +# Add packages +RUN apk add --update-cache \ + npm \ + && rm -rf /var/cache/apk/* + +# Copy csproj files as distinct layers +WORKDIR /source +COPY dotnet-src/Web/*.csproj ./src/Web/ +#COPY Directory.Build.props . + +# Restore project dependencies and tools +WORKDIR /source/src/Web +RUN dotnet restore + +# Copy required project files +WORKDIR /source +COPY dotnet-src/Web/. ./src/Web/ + +# Build app +WORKDIR /source/src/Web +RUN dotnet publish -c release -o /app --no-restore + ############################################### # App stage # ############################################### -FROM bitwarden/server:latest - +FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine LABEL com.bitwarden.product="bitwarden" LABEL com.bitwarden.project="web" ENV ASPNETCORE_ENVIRONMENT=Production ENV ASPNETCORE_URLS http://+:5000 EXPOSE 5000 +# Add packages +RUN apk add --update-cache \ + curl \ + && rm -rf /var/cache/apk/* + +# Create required directories RUN mkdir -p /etc/bitwarden/web -RUN chown -R bitwarden:bitwarden /etc/bitwarden COPY docker/confd/app-id.toml /etc/confd/conf.d/ COPY docker/confd/app-id.conf.tmpl /etc/confd/templates/ @@ -34,15 +65,20 @@ COPY docker/confd/app-id.conf.tmpl /etc/confd/templates/ RUN wget -O /usr/local/bin/confd https://github.com/kelseyhightower/confd/releases/download/v0.16.0/confd-0.16.0-linux-amd64 RUN chmod +x /usr/local/bin/confd +# Copy Web server from dotnet-build stage +COPY --from=dotnet-build /app /server + # Copy app from build stage WORKDIR /app -COPY --from=build /source/build ./ -RUN chown -R bitwarden:bitwarden /app +COPY --from=node-build /source/build ./ # Copy entrypoint script and make it executable COPY docker/entrypoint.sh / RUN chmod +x /entrypoint.sh +# Create non-root user to run app +RUN adduser -s /bin/false -D bitwarden && chown -R bitwarden:bitwarden /app /server /etc/bitwarden + USER bitwarden:bitwarden HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1 ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 2a04bb2e..bb22126e 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,4 +4,4 @@ cp /etc/bitwarden/web/app-id.json /app/app-id.json -exec dotnet /bitwarden_server/Server.dll /contentRoot=/app /webRoot=. /serveUnknown=false /webVault=true +exec dotnet /server/Web.dll /contentRoot=/app /webRoot=. diff --git a/dotnet-src/Web/Program.cs b/dotnet-src/Web/Program.cs new file mode 100644 index 00000000..2a56cc8d --- /dev/null +++ b/dotnet-src/Web/Program.cs @@ -0,0 +1,46 @@ +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace Bit.Web +{ + public class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .Build(); + + var builder = new WebHostBuilder() + .UseConfiguration(config) + .UseKestrel() + .UseStartup() + .ConfigureLogging((hostingContext, logging) => + { + logging.AddConsole().AddDebug(); + }) + .ConfigureKestrel((context, options) => { }); + + var contentRoot = config.GetValue("contentRoot"); + if (!string.IsNullOrWhiteSpace(contentRoot)) + { + builder.UseContentRoot(contentRoot); + } + else + { + builder.UseContentRoot(Directory.GetCurrentDirectory()); + } + + var webRoot = config.GetValue("webRoot"); + if (string.IsNullOrWhiteSpace(webRoot)) + { + builder.UseWebRoot(webRoot); + } + + var host = builder.Build(); + host.Run(); + } + } +} diff --git a/dotnet-src/Web/Properties/launchSettings.json b/dotnet-src/Web/Properties/launchSettings.json new file mode 100644 index 00000000..c73e6c1b --- /dev/null +++ b/dotnet-src/Web/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Server": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:53910/" + } + } +} diff --git a/dotnet-src/Web/Startup.cs b/dotnet-src/Web/Startup.cs new file mode 100644 index 00000000..fab6209a --- /dev/null +++ b/dotnet-src/Web/Startup.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Web +{ + public class Startup + { + private readonly List _longCachedPaths = new List + { + "/app/", "/locales/", "/fonts/", "/connectors/", "/scripts/" + }; + private readonly List _mediumCachedPaths = new List + { + "/images/" + }; + + public Startup() + { + CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("en-US"); + } + + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + public void Configure( + IApplicationBuilder app, + IConfiguration configuration) + { + // TODO: This should be removed when asp.net natively support avif + var provider = new FileExtensionContentTypeProvider { Mappings = { [".avif"] = "image/avif" } }; + + var options = new DefaultFilesOptions(); + options.DefaultFileNames.Clear(); + options.DefaultFileNames.Add("index.html"); + app.UseDefaultFiles(options); + app.UseStaticFiles(new StaticFileOptions + { + ContentTypeProvider = provider, + OnPrepareResponse = ctx => + { + if (!ctx.Context.Request.Path.HasValue || + ctx.Context.Response.Headers.ContainsKey("Cache-Control")) + { + return; + } + var path = ctx.Context.Request.Path.Value; + if (_longCachedPaths.Any(ext => path.StartsWith(ext))) + { + // 14 days + ctx.Context.Response.Headers.Append("Cache-Control", "max-age=1209600"); + } + if (_mediumCachedPaths.Any(ext => path.StartsWith(ext))) + { + // 7 days + ctx.Context.Response.Headers.Append("Cache-Control", "max-age=604800"); + } + } + }); + app.UseRouting(); + app.UseEndpoints(endpoints => + { + endpoints.MapGet("/alive", + async context => await context.Response.WriteAsJsonAsync(System.DateTime.UtcNow)); + endpoints.MapGet("/version", + async context => await context.Response.WriteAsJsonAsync(Assembly.GetEntryAssembly() + .GetCustomAttribute().InformationalVersion)); + }); + } + } +} diff --git a/dotnet-src/Web/Web.csproj b/dotnet-src/Web/Web.csproj new file mode 100644 index 00000000..fd947339 --- /dev/null +++ b/dotnet-src/Web/Web.csproj @@ -0,0 +1,11 @@ + + + + false + net5.0 + 1.47.2 + Bit.$(MSBuildProjectName) + true + + + diff --git a/dotnet-src/Web/build.sh b/dotnet-src/Web/build.sh new file mode 100755 index 00000000..3991acdf --- /dev/null +++ b/dotnet-src/Web/build.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -e + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +echo -e "\n## Building Web" + +echo -e "\nBuilding app" +echo ".NET Core version $(dotnet --version)" +echo "Restore" +dotnet restore "$DIR/Web.csproj" +echo "Clean" +dotnet clean "$DIR/Web.csproj" -c "Release" -o "$DIR/obj/build-output/publish" +echo "Publish" +dotnet publish "$DIR/Web.csproj" -c "Release" -o "$DIR/obj/build-output/publish" diff --git a/dotnet-src/Web/packages.lock.json b/dotnet-src/Web/packages.lock.json new file mode 100644 index 00000000..3d63b020 --- /dev/null +++ b/dotnet-src/Web/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": {} + } +}