├── src └── Giraffe.Website │ ├── Assets │ ├── Public │ │ ├── favicon.ico │ │ ├── giraffe.png │ │ ├── giraffe-head.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ └── manifest.json │ └── Private │ │ └── main.css │ ├── Dockerfile │ ├── Giraffe.Website.fsproj │ ├── Env.fs │ ├── Common.fs │ └── Program.fs ├── README.md ├── RELEASE_NOTES.md ├── .github └── workflows │ └── build.yml ├── giraffe-website.sln ├── .gitignore └── LICENSE /src/Giraffe.Website/Assets/Public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/favicon.ico -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/giraffe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/giraffe.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/giraffe-head.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/giraffe-head.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/favicon-16x16.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/favicon-32x32.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/android-chrome-192x192.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giraffe-fsharp/giraffe-website/master/src/Giraffe.Website/Assets/Public/android-chrome-512x512.png -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Public/manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"Giraffe","short_name":"Giraffe","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Giraffe](https://giraffe.wiki/) 2 | 3 | Code for the official [giraffe.wiki](https://giraffe.wiki) website. 4 | 5 | ![Build and Deploy](https://github.com/giraffe-fsharp/giraffe-website/workflows/Build%20and%20Deploy/badge.svg?branch=master) 6 | 7 | [![Build History](https://buildstats.info/github/chart/giraffe-fsharp/giraffe-website?branch=master)](https://github.com/giraffe-fsharp/giraffe-website/actions?query=branch%3Amaster) -------------------------------------------------------------------------------- /src/Giraffe.Website/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build 2 | 3 | ARG version=0.0.0-undefined 4 | 5 | WORKDIR /app 6 | 7 | # Copy everything and build 8 | COPY src/ ./ 9 | RUN dotnet publish /p:Version=$version Giraffe.Website/Giraffe.Website.fsproj -c Release -o published 10 | 11 | # Build runtime image 12 | FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime 13 | 14 | # Change the HTTP port that the server process is listening 15 | # https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/8.0/aspnet-port 16 | ENV ASPNETCORE_HTTP_PORTS=5000 17 | 18 | WORKDIR /app 19 | COPY --from=build /app/published . 20 | ENTRYPOINT ["dotnet", "Giraffe.Website.dll"] -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ============= 3 | 4 | ## 2.0.0 5 | 6 | - [Minor CSS change](https://github.com/giraffe-fsharp/giraffe-website/pull/3) - Credits @m-rinaldi 7 | - [(CI) GitHub actions version update](https://github.com/giraffe-fsharp/giraffe-website/pull/5) - Credits @64J0 8 | - [Update to .NET 8](https://github.com/giraffe-fsharp/giraffe-website/pull/2) - Credits @64J0 9 | 10 | ## 1.6.0 11 | 12 | Unkown. 13 | 14 | ## 1.5.0 15 | 16 | CSS changes. 17 | 18 | ## 1.4.0 19 | 20 | - Fixed versioning 21 | - Smaller Docker image 22 | - Toggle for HTTPS redirection 23 | 24 | ## 1.3.0 25 | 26 | Updated project to .NET 5 and latest NuGet dependencies. 27 | 28 | ## Older versions 29 | 30 | No release notes available. -------------------------------------------------------------------------------- /src/Giraffe.Website/Giraffe.Website.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Major 6 | false 7 | $(MSBuildThisFileDirectory) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | PreserveNewest 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: [ develop, master ] 6 | pull_request: 7 | branches: [ develop, master ] 8 | release: 9 | types: 10 | - published 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup .NET Core 18 | uses: actions/setup-dotnet@v4 19 | with: 20 | dotnet-version: 8.x 21 | - name: Restore 22 | run: dotnet restore src/Giraffe.Website/Giraffe.Website.fsproj 23 | - name: Build 24 | run: dotnet build --configuration Release --no-restore src/Giraffe.Website/Giraffe.Website.fsproj 25 | 26 | deploy: 27 | needs: build 28 | if: github.event_name == 'release' 29 | runs-on: ubuntu-latest 30 | env: 31 | PROJECT: giraffefsharp 32 | IMAGE: giraffe-website 33 | 34 | steps: 35 | # Checkout repo 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | # Build the Docker image 40 | - name: Build Docker image 41 | run: | 42 | PATTERN="refs/tags/v" 43 | SUB="" 44 | TAG="${GITHUB_REF/$PATTERN/$SUB}" 45 | docker build --build-arg version=$TAG -t "$PROJECT"/"$IMAGE":"$TAG" -f src/Giraffe.Website/Dockerfile . 46 | 47 | # Auth with Docker Hub 48 | - name: Login to Docker Hub 49 | uses: docker/login-action@v3 50 | with: 51 | username: ${{ secrets.DOCKER_USERNAME }} 52 | password: ${{ secrets.DOCKER_ACCESS_TOKEN }} 53 | 54 | # Push the Docker image to Docker Hub 55 | - name: Publish Docker image 56 | run: | 57 | PATTERN="refs/tags/v" 58 | SUB="" 59 | TAG="${GITHUB_REF/$PATTERN/$SUB}" 60 | docker push $PROJECT/$IMAGE:$TAG -------------------------------------------------------------------------------- /giraffe-website.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E393E185-2C82-44DD-8B02-58C8DA4E1ED7}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "root", "root", "{26D17125-4933-4486-9868-BDFF3504913C}" 9 | ProjectSection(SolutionItems) = preProject 10 | global.json = global.json 11 | .gitignore = .gitignore 12 | LICENSE = LICENSE 13 | EndProjectSection 14 | EndProject 15 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Website", "src\Giraffe.Website\Giraffe.Website.fsproj", "{6A1F9906-64C1-42C8-852D-0850A326A199}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Debug|x64 = Debug|x64 21 | Debug|x86 = Debug|x86 22 | Release|Any CPU = Release|Any CPU 23 | Release|x64 = Release|x64 24 | Release|x86 = Release|x86 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(NestedProjects) = preSolution 30 | {6A1F9906-64C1-42C8-852D-0850A326A199} = {E393E185-2C82-44DD-8B02-58C8DA4E1ED7} 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|x64.ActiveCfg = Debug|Any CPU 36 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|x64.Build.0 = Debug|Any CPU 37 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|x86.ActiveCfg = Debug|Any CPU 38 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Debug|x86.Build.0 = Debug|Any CPU 39 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|x64.ActiveCfg = Release|Any CPU 42 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|x64.Build.0 = Release|Any CPU 43 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|x86.ActiveCfg = Release|Any CPU 44 | {6A1F9906-64C1-42C8-852D-0850A326A199}.Release|x86.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /src/Giraffe.Website/Assets/Private/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Inter, Helvetica Neue, Helvetica, Arial, sans-serif; 3 | color: #333; 4 | -ms-word-wrap: break-word; 5 | word-wrap: break-word; 6 | font-size: 1.1em; 7 | font-variant-ligatures: none; 8 | } 9 | 10 | header, main, nav, footer { 11 | margin: 0 auto; 12 | max-width: 900px; 13 | padding: 10px; 14 | } 15 | 16 | main { 17 | margin: 2em auto; 18 | } 19 | 20 | /*Fixing monospace handling across browsers*/ 21 | pre, 22 | code, 23 | kbd, 24 | samp { 25 | font-family: monospace, monospace; 26 | font-size: 1em; 27 | } 28 | 29 | code{ 30 | display: inline-block; 31 | padding: .1em .4em; 32 | -ms-border-radius: 4px; 33 | border-radius: 4px; 34 | font-family: 'Roboto Mono', Menlo, Consolas, "Courier New", monospace; 35 | font-size: .8em; 36 | line-height: 1.5em; 37 | text-align: left; 38 | white-space: pre-wrap; 39 | word-wrap: break-word; 40 | background-color: #e1e1e1; 41 | color: #cd5c5c; 42 | } 43 | 44 | pre { 45 | width: auto; 46 | overflow-x: auto; 47 | display: block; 48 | padding: .5em 1em; 49 | border-radius: 3px; 50 | font-family: 'Roboto Mono', Menlo, Consolas, "Courier New", monospace; 51 | line-height: 1.2em; 52 | background: #444; 53 | color: #fff; 54 | } 55 | 56 | pre > code { 57 | display: block; 58 | padding: .2em 0 .2em .5em; 59 | font-size: .75em; 60 | background: #444; 61 | color: #fff; 62 | } 63 | 64 | p { 65 | line-height: 1.5em; 66 | } 67 | 68 | h1 { 69 | font-size: 3em; 70 | } 71 | 72 | h2 { 73 | font-size: 2.4em; 74 | color: #603504; 75 | } 76 | 77 | h3 { 78 | font-size: 2em; 79 | } 80 | 81 | h4 { 82 | font-size: 1.6em; 83 | } 84 | 85 | h5 { 86 | font-size: 1.2em; 87 | font-weight: bold; 88 | } 89 | 90 | h6 { 91 | font-size: 1em; 92 | font-weight: bold; 93 | } 94 | 95 | a { 96 | text-decoration: none; 97 | color: #ba6a02; 98 | } 99 | 100 | a:hover { 101 | text-decoration: none; 102 | color: #ffbb02; 103 | } 104 | 105 | ul li { 106 | margin: .6em 0; 107 | } 108 | 109 | ul li a { 110 | color: #603504; 111 | } 112 | 113 | nav { 114 | display: flex; 115 | justify-content: space-around; 116 | padding: 0 0 1em 0; 117 | text-align: center; 118 | border-bottom: 1px solid #f1f1f1; 119 | } 120 | 121 | nav ul { 122 | list-style: none; 123 | margin: 0; 124 | padding: 0; 125 | } 126 | 127 | nav ul li { 128 | display: inline-block; 129 | margin: .7em; 130 | } 131 | 132 | nav a { 133 | font-size: .9em; 134 | padding-bottom: 3px; 135 | text-transform: uppercase; 136 | color: #825e34cc !important; 137 | font-weight: 500; 138 | } 139 | 140 | nav a:hover { 141 | text-decoration: none; 142 | color: #ffbb02 !important; 143 | } 144 | 145 | footer { 146 | font-size: .8em; 147 | } 148 | 149 | table { 150 | display: block; 151 | width: 100%; 152 | box-sizing: border-box; 153 | border: 1px solid #efefef; 154 | border-radius: 3px; 155 | -moz-border-radius: 3px; 156 | padding: 5px; 157 | } 158 | 159 | tbody, thead, tr { 160 | width: 100%; 161 | box-sizing: border-box; 162 | } 163 | 164 | td, th { 165 | text-align: left; 166 | font-size: .95em; 167 | padding: .5em; 168 | } 169 | 170 | td code { 171 | background: transparent; 172 | } 173 | 174 | tr:nth-child(odd) { background-color: #fff; } 175 | tr:nth-child(even) { background-color: #f1f1f1; } 176 | 177 | #inner-footer { 178 | border-top: 1px solid #e1e1e1; 179 | text-align: center; 180 | } 181 | 182 | #inner-footer h5 { 183 | font-size: .9em; 184 | margin-bottom: .5em; 185 | } 186 | 187 | #logo { 188 | display: block; 189 | margin: 2em auto; 190 | max-width: 300px; 191 | } 192 | -------------------------------------------------------------------------------- /src/Giraffe.Website/Env.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe.Website 2 | 3 | [] 4 | module Env = 5 | open System 6 | open System.IO 7 | open System.Diagnostics 8 | open Logfella 9 | 10 | [] 11 | module private Keys = 12 | let APP_NAME = "APP_NAME" 13 | let ENV_NAME = "ASPNETCORE_ENVIRONMENT" 14 | let LOG_LEVEL = "LOG_LEVEL" 15 | let SENTRY_DSN = "SENTRY_DSN" 16 | let DOMAIN_NAME = "DOMAIN_NAME" 17 | let FORCE_HTTPS = "FORCE_HTTPS" 18 | let ENABLE_REQUEST_LOGGING = "ENABLE_REQUEST_LOGGING" 19 | let ENABLE_ERROR_ENDPOINT = "ENABLE_ERROR_ENDPOINT" 20 | let PROXY_COUNT = "PROXY_COUNT" 21 | let KNOWN_PROXIES = "KNOWN_PROXIES" 22 | let KNOWN_PROXY_NETWORKS = "KNOWN_PROXY_NETWORKS" 23 | 24 | let userHomeDir = Environment.GetEnvironmentVariable "HOME" 25 | let defaultAppName = "Giraffe" 26 | 27 | let appRoot = Directory.GetCurrentDirectory() 28 | let publicAssetsDir = Path.Combine(appRoot, "Assets/Public") 29 | 30 | let appName = 31 | Config.environmentVarOrDefault 32 | Keys.APP_NAME 33 | defaultAppName 34 | 35 | let appVersion = 36 | System.Reflection.Assembly.GetExecutingAssembly().Location 37 | |> FileVersionInfo.GetVersionInfo 38 | |> fun v-> v.ProductVersion 39 | 40 | let name = 41 | Config.environmentVarOrDefault 42 | Keys.ENV_NAME 43 | "Unknown" 44 | 45 | let isProduction = 46 | name.Equals( 47 | "Production", 48 | StringComparison.OrdinalIgnoreCase) 49 | 50 | let logLevel = 51 | Config.environmentVarOrDefault 52 | Keys.LOG_LEVEL 53 | "info" 54 | 55 | let logSeverity = 56 | logLevel.ParseSeverity() 57 | 58 | let sentryDsn = 59 | Config.environmentVarOrDefault 60 | Keys.SENTRY_DSN 61 | "" 62 | |> Str.toOption 63 | 64 | let domainName = 65 | Config.environmentVarOrDefault 66 | Keys.DOMAIN_NAME 67 | "giraffe.wiki" 68 | 69 | let forceHttps = 70 | Config.typedEnvironmentVarOrDefault 71 | None 72 | Keys.FORCE_HTTPS 73 | false 74 | 75 | let baseUrl = 76 | match isProduction with 77 | | true -> sprintf "https://%s" domainName 78 | | false -> "http://localhost:5000" 79 | 80 | let enableRequestLogging = 81 | Config.InvariantCulture.typedEnvironmentVarOrDefault 82 | Keys.ENABLE_REQUEST_LOGGING 83 | false 84 | 85 | let enableErrorEndpoint = 86 | Config.InvariantCulture.typedEnvironmentVarOrDefault 87 | Keys.ENABLE_ERROR_ENDPOINT 88 | false 89 | 90 | let proxyCount = 91 | Config.InvariantCulture.typedEnvironmentVarOrDefault 92 | Keys.PROXY_COUNT 93 | 0 94 | 95 | let knownProxies = 96 | Keys.KNOWN_PROXIES 97 | |> Config.environmentVarList 98 | |> Array.map Network.tryParseIPAddress 99 | |> Array.filter Option.isSome 100 | |> Array.map Option.get 101 | 102 | let knownProxyNetworks = 103 | Keys.KNOWN_PROXY_NETWORKS 104 | |> Config.environmentVarList 105 | |> Array.map Network.tryParseNetworkAddress 106 | |> Array.filter Option.isSome 107 | |> Array.map Option.get 108 | 109 | let summary = 110 | dict [ 111 | "App", dict [ 112 | "App", appName 113 | "Version", appVersion 114 | ] 115 | "Directories", dict [ 116 | "App", appRoot 117 | "Public Assets", publicAssetsDir 118 | ] 119 | "Logging", dict [ 120 | "Environment", name 121 | "Log Level", logLevel 122 | "Sentry DSN", sentryDsn.ToSecret() 123 | ] 124 | "Redirection", dict [ 125 | "Force HTTPS", forceHttps.ToString() 126 | ] 127 | "URLs", dict [ 128 | "Domain", domainName 129 | "Base URL", baseUrl 130 | ] 131 | "Proxies", dict [ 132 | "Proxy count", proxyCount.ToString() 133 | "Known proxies", knownProxies.ToPrettyString() 134 | "Known proxy networks", knownProxyNetworks.ToPrettyString() 135 | ] 136 | "Debugging", dict [ 137 | "Request logging enabled", enableRequestLogging.ToString() 138 | "Error endpoint enabled", enableErrorEndpoint.ToString() 139 | ] 140 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | .DS_Store 353 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/Giraffe.Website/Common.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe.Website 2 | 3 | // --------------------------------- 4 | // Str 5 | // --------------------------------- 6 | 7 | [] 8 | module Str = 9 | open System 10 | 11 | let private ignoreCase = StringComparison.InvariantCultureIgnoreCase 12 | let equals (s1 : string) (s2 : string) = s1.Equals s2 13 | let equalsCi (s1 : string) (s2 : string) = s1.Equals(s2, ignoreCase) 14 | 15 | let isNullOrEmpty str = String.IsNullOrEmpty str 16 | 17 | let toOption str = 18 | match isNullOrEmpty str with 19 | | true -> None 20 | | false -> Some str 21 | 22 | // --------------------------------- 23 | // Config 24 | // --------------------------------- 25 | 26 | [] 27 | module Config = 28 | open System 29 | open System.ComponentModel 30 | open System.Globalization 31 | 32 | let private strOption str = 33 | match String.IsNullOrEmpty str with 34 | | true -> None 35 | | false -> Some str 36 | 37 | let private strSplitArray (str : string) = 38 | str.Split([| ' '; ','; ';' |], StringSplitOptions.RemoveEmptyEntries) 39 | 40 | let private tryConvertFromString<'T when 'T : struct> (cultureInfo : CultureInfo option) (value : string) = 41 | let culture = defaultArg cultureInfo CultureInfo.CurrentCulture 42 | let converter = TypeDescriptor.GetConverter (typeof<'T>) 43 | try Some (converter.ConvertFromString(null, culture, value) :?> 'T) 44 | with _ -> None 45 | 46 | let environmentVar key = 47 | Environment.GetEnvironmentVariable key 48 | |> strOption 49 | 50 | let environmentVarOrDefault key defaultValue = 51 | environmentVar key 52 | |> Option.defaultValue defaultValue 53 | 54 | let typedEnvironmentVar<'T when 'T : struct> culture key = 55 | Environment.GetEnvironmentVariable key 56 | |> strOption 57 | |> Option.bind (tryConvertFromString<'T> culture) 58 | 59 | let typedEnvironmentVarOrDefault<'T when 'T : struct> culture key defaultValue = 60 | typedEnvironmentVar<'T> culture key 61 | |> Option.defaultValue defaultValue 62 | 63 | let environmentVarList key = 64 | environmentVar key 65 | |> function 66 | | None -> [||] 67 | | Some v -> strSplitArray v 68 | 69 | module CurrentCulture = 70 | let typedEnvironmentVar<'T when 'T : struct> key = 71 | Environment.GetEnvironmentVariable key 72 | |> strOption 73 | |> Option.bind (tryConvertFromString<'T> (Some CultureInfo.CurrentCulture)) 74 | 75 | let typedEnvironmentVarOrDefault<'T when 'T : struct> (key : string) defaultValue = 76 | typedEnvironmentVar<'T> key 77 | |> Option.defaultValue defaultValue 78 | 79 | module InvariantCulture = 80 | let typedEnvironmentVar<'T when 'T : struct> key = 81 | Environment.GetEnvironmentVariable key 82 | |> strOption 83 | |> Option.bind (tryConvertFromString<'T> (Some CultureInfo.InvariantCulture)) 84 | 85 | let typedEnvironmentVarOrDefault<'T when 'T : struct> (key : string) defaultValue = 86 | typedEnvironmentVar<'T> key 87 | |> Option.defaultValue defaultValue 88 | 89 | // --------------------------------- 90 | // Dev Config 91 | // --------------------------------- 92 | 93 | [] 94 | module DevConfig = 95 | open System.IO 96 | open System.Text.Json 97 | open System.Collections.Generic 98 | 99 | let load jsonFile = 100 | jsonFile 101 | |> File.Exists 102 | |> function 103 | | false -> Dictionary() 104 | | true -> 105 | jsonFile 106 | |> File.ReadAllText 107 | |> JsonSerializer.Deserialize> 108 | 109 | // --------------------------------- 110 | // Network 111 | // --------------------------------- 112 | 113 | [] 114 | module Network = 115 | open System 116 | open System.Net 117 | open Microsoft.AspNetCore.HttpOverrides 118 | 119 | let tryParseIPAddress (str : string) = 120 | match IPAddress.TryParse str with 121 | | true, ipAddress -> Some ipAddress 122 | | false, _ -> None 123 | 124 | let tryParseNetworkAddress (str : string) = 125 | let ipAddr, cidrLen = 126 | match str.Split('/', StringSplitOptions.RemoveEmptyEntries) with 127 | | arr when arr.Length = 2 -> arr.[0], Some arr.[1] 128 | | arr -> arr.[0], None 129 | 130 | match IPAddress.TryParse ipAddr with 131 | | false, _ -> None 132 | | true, ipAddress -> 133 | let cidrMask = 134 | match cidrLen with 135 | | None -> None 136 | | Some len -> 137 | match Int32.TryParse len with 138 | | true, mask -> Some mask 139 | | false, _ -> None 140 | match cidrMask with 141 | | Some mask -> Some (IPNetwork(ipAddress, mask)) 142 | | None -> Some (IPNetwork(ipAddress, 32)) 143 | 144 | [] 145 | module NetworkExtensions = 146 | open System 147 | open System.Net 148 | open System.Collections.Generic 149 | open Microsoft.AspNetCore.Builder 150 | open Microsoft.AspNetCore.HttpOverrides 151 | open Microsoft.AspNetCore.Http 152 | open Microsoft.AspNetCore.Hosting.Server.Features 153 | open Microsoft.Extensions.DependencyInjection 154 | 155 | type IPNetwork with 156 | member this.ToPrettyString() = 157 | sprintf "%s/%s" 158 | (this.Prefix.ToString()) 159 | (this.PrefixLength.ToString()) 160 | 161 | type IPAddress with 162 | member this.ToPrettyString() = 163 | this.MapToIPv4().ToString() 164 | 165 | type IEnumerable<'T> with 166 | member this.ToPrettyString() = 167 | this 168 | |> Seq.map (fun t -> 169 | match box t with 170 | | :? IPAddress as ip -> ip.ToPrettyString() 171 | | :? IPNetwork as nw -> nw.ToPrettyString() 172 | | _ -> t.ToString()) 173 | |> String.concat ", " 174 | 175 | type IServiceCollection with 176 | member this.AddProxies (proxyCount : int, 177 | proxyNetworks : IPNetwork[], 178 | proxies : IPAddress[]) = 179 | 180 | this.Configure( 181 | fun (cfg : ForwardedHeadersOptions) -> 182 | proxyNetworks 183 | |> Array.iter cfg.KnownNetworks.Add 184 | proxies 185 | |> Array.iter cfg.KnownProxies.Add 186 | cfg.RequireHeaderSymmetry <- false 187 | cfg.ForwardLimit <- Nullable proxyCount 188 | cfg.ForwardedHeaders <- ForwardedHeaders.All) 189 | 190 | type HttpContext with 191 | member this.GetHttpsPort() = 192 | let defaultPort = 443 193 | let envVarPort = 194 | Config.environmentVarOrDefault 195 | "HTTPS_PORT" 196 | (Config.environmentVarOrDefault 197 | "ASPNETCORE_HTTPS_PORT" 198 | (Config.environmentVarOrDefault "ANCM_HTTPS_PORT" "")) 199 | |> Str.toOption 200 | match envVarPort with 201 | | Some port -> int port 202 | | None -> 203 | match this.RequestServices.GetService() with 204 | | null -> 205 | match Str.equalsCi this.Request.Host.Host "localhost" with 206 | | true -> 5001 207 | | false -> defaultPort 208 | | server -> 209 | server.Addresses 210 | |> Seq.map BindingAddress.Parse 211 | |> Seq.tryFind(fun a -> Str.equalsCi a.Scheme "https") 212 | |> function 213 | | Some a -> a.Port 214 | | None -> defaultPort 215 | 216 | type IApplicationBuilder with 217 | member this.UseTrailingSlashRedirection() = 218 | this.Use( 219 | fun (ctx: HttpContext) (next: RequestDelegate) -> 220 | let hasTrailingSlash = 221 | ctx.Request.Path.HasValue 222 | && ctx.Request.Path.Value.EndsWith "/" 223 | && ctx.Request.Path.Value.Length > 1 224 | match hasTrailingSlash with 225 | | true -> 226 | ctx.Request.Path <- PathString(ctx.Request.Path.Value.TrimEnd '/') 227 | if Str.equalsCi ctx.Request.Scheme "https" then 228 | ctx.Request.Host <- HostString(ctx.Request.Host.Host, ctx.GetHttpsPort()) 229 | let url = Microsoft.AspNetCore.Http.Extensions.UriHelper.GetEncodedUrl ctx.Request 230 | ctx.Response.Redirect(url, true) 231 | Threading.Tasks.Task.CompletedTask 232 | | false -> next.Invoke(ctx)) 233 | 234 | member this.UseHttpsRedirection (isEnabled : bool, domainName : string) = 235 | match isEnabled with 236 | | true -> 237 | this.Use( 238 | fun (ctx: HttpContext) (next: RequestDelegate) -> 239 | let host = ctx.Request.Host.Host 240 | // Only HTTPS redirect for the chosen domain: 241 | let mustUseHttps = 242 | host = domainName 243 | || host.EndsWith ("." + domainName) 244 | // Otherwise prevent the HTTP redirection middleware 245 | // to redirect by force setting the scheme to https: 246 | if not mustUseHttps then 247 | ctx.Request.Scheme <- "https" 248 | ctx.Request.IsHttps <- true 249 | next.Invoke(ctx)) 250 | .UseHttpsRedirection() 251 | | false -> this 252 | 253 | // --------------------------------- 254 | // Logging 255 | // --------------------------------- 256 | 257 | [] 258 | module Logging = 259 | open System.Text 260 | open System.Collections.Generic 261 | open Logfella 262 | 263 | let outputEnvironmentSummary (summary : IDictionary>) = 264 | let categories = summary.Keys |> Seq.toList 265 | let keyLength = 266 | categories 267 | |> List.fold( 268 | fun (len : int) (category : string) -> 269 | summary.[category].Keys 270 | |> Seq.toList 271 | |> List.map(fun k -> k.Length) 272 | |> List.sortByDescending (id) 273 | |> List.head 274 | |> max len 275 | ) 0 276 | let output = 277 | (categories 278 | |> List.fold( 279 | fun (sb : StringBuilder) (category : string) -> 280 | summary.[category] 281 | |> Seq.fold( 282 | fun (sb : StringBuilder) (kvp) -> 283 | let key = kvp.Key.PadLeft(keyLength, ' ') 284 | let value = kvp.Value 285 | sprintf "%s : %s" key value 286 | |> sb.AppendLine 287 | ) (sb.AppendLine("") 288 | .AppendLine((sprintf "%s :" (category.ToUpper())).PadLeft(keyLength + 2, ' ')) 289 | .AppendLine("-----".PadRight(keyLength + 2, '-'))) 290 | ) (StringBuilder() 291 | .AppendLine("") 292 | .AppendLine("") 293 | .AppendLine("..:: Environment Summary ::.."))) 294 | .ToString() 295 | Log.Notice( 296 | output, 297 | ("categoryName", "startupInfo" :> obj)) 298 | 299 | [] 300 | module LoggingExtensions = 301 | open System 302 | open Microsoft.AspNetCore.Hosting 303 | 304 | let private secretMask = "******" 305 | 306 | type String with 307 | member this.ToSecret() = 308 | match this with 309 | | str when String.IsNullOrEmpty str -> "" 310 | | str when str.Length <= 10 -> secretMask 311 | | str -> str.Substring(0, str.Length / 2) + secretMask 312 | 313 | type Option<'T> with 314 | member this.ToSecret() = 315 | match this with 316 | | None -> "" 317 | | Some obj -> obj.ToString().ToSecret() 318 | 319 | type IWebHostBuilder with 320 | member this.ConfigureSentry (sentryDsn : string option, 321 | environmentName : string, 322 | appVersion : string) = 323 | 324 | match sentryDsn with 325 | | None -> this 326 | | Some dsn -> 327 | this.UseSentry( 328 | fun sentry -> 329 | sentry.Debug <- false 330 | sentry.Environment <- environmentName 331 | sentry.Release <- appVersion 332 | sentry.AttachStacktrace <- true 333 | sentry.Dsn <- dsn) 334 | 335 | // --------------------------------- 336 | // Hashing 337 | // --------------------------------- 338 | 339 | [] 340 | module Hash = 341 | open System.Text 342 | open System.Security.Cryptography 343 | 344 | let sha1 (str : string) = 345 | str 346 | |> Encoding.UTF8.GetBytes 347 | |> SHA1.Create().ComputeHash 348 | |> Array.map (fun b -> b.ToString "x2") 349 | |> String.concat "" -------------------------------------------------------------------------------- /src/Giraffe.Website/Program.fs: -------------------------------------------------------------------------------- 1 | namespace Giraffe.Website 2 | open Microsoft.AspNetCore.Http 3 | 4 | [] 5 | module Css = 6 | open System.IO 7 | open System.Text 8 | open NUglify 9 | 10 | type BundledCss = 11 | { 12 | Content : string 13 | Hash : string 14 | Path : string 15 | } 16 | static member FromContent (name: string) (content : string) = 17 | let hash = Hash.sha1 content 18 | { 19 | Content = content 20 | Hash = hash 21 | Path = sprintf "/%s.%s.css" name hash 22 | } 23 | 24 | let private getErrorMsg (errors : seq) = 25 | let msg = 26 | errors 27 | |> Seq.fold (fun (sb : StringBuilder) t -> 28 | sprintf "Error: %s, File: %s" t.Message t.File 29 | |> sb.AppendLine 30 | ) (StringBuilder("Couldn't uglify content.")) 31 | msg.ToString() 32 | 33 | let minify (css : string) = 34 | css 35 | |> Uglify.Css 36 | |> (fun res -> 37 | match res.HasErrors with 38 | | true -> failwith (getErrorMsg res.Errors) 39 | | false -> res.Code) 40 | 41 | let getMinifiedContent (fileName : string) = 42 | fileName 43 | |> File.ReadAllText 44 | |> minify 45 | 46 | let getBundledContent (bundleName : string) (fileNames : string list) = 47 | let result = 48 | fileNames 49 | |> List.fold( 50 | fun (sb : StringBuilder) fileName -> 51 | fileName 52 | |> getMinifiedContent 53 | |> sb.AppendLine 54 | ) (StringBuilder()) 55 | result.ToString() 56 | |> BundledCss.FromContent bundleName 57 | 58 | [] 59 | module Url = 60 | let create (route : string) = 61 | route.TrimStart [| '/' |] 62 | |> sprintf "%s/%s" Env.baseUrl 63 | 64 | // --------------------------------- 65 | // Views 66 | // --------------------------------- 67 | 68 | [] 69 | module Views = 70 | open System 71 | open Giraffe.ViewEngine 72 | 73 | let private twitterCard (key : string) (value : string) = 74 | meta [ _name (sprintf "twitter:%s" key); _content value ] 75 | 76 | let private openGraph (key : string) (value : string) = 77 | meta [ attr "property" (sprintf "og:%s" key); attr "content" value ] 78 | 79 | let private css (url : string) = link [ _rel "stylesheet"; _type "text/css"; _href url ] 80 | 81 | let private internalLink (path : string) (title : string) = 82 | a [ _href (Url.create path) ] [ str title ] 83 | 84 | let private externalLink (url : string) (title : string) = 85 | a [ _href url; _target "blank" ] [ str title ] 86 | 87 | let minifiedCss = 88 | Css.getBundledContent 89 | "bundle" 90 | [ 91 | "Assets/Private/main.css" 92 | ] 93 | 94 | let private masterView 95 | (subject : string option) 96 | (permalink : string option) 97 | (headerContent : XmlNode list option) 98 | (bodyContent : XmlNode list) = 99 | let websiteName = "Giraffe" 100 | let pageTitle = 101 | match subject with 102 | | Some s -> sprintf "%s - %s" s websiteName 103 | | None -> websiteName 104 | html [] [ 105 | head [] [ 106 | // Metadata 107 | meta [ _charset "utf-8" ] 108 | meta [ _name "viewport"; _content "width=device-width, initial-scale=1.0" ] 109 | meta [ _name "description"; _content pageTitle ] 110 | 111 | // Title 112 | title [] [ encodedText pageTitle ] 113 | 114 | // Favicon 115 | link [ _rel "apple-touch-icon"; _sizes "180x180"; _href (Url.create "/apple-touch-icon.png") ] 116 | link [ _rel "icon"; _type "image/png"; _sizes "32x32"; _href (Url.create "/favicon-32x32.png") ] 117 | link [ _rel "icon"; _type "image/png"; _sizes "16x16"; _href (Url.create "/favicon-16x16.png") ] 118 | link [ _rel "manifest"; _href (Url.create "/manifest.json") ] 119 | link [ _rel "shortcut icon"; _href (Url.create "/favicon.ico") ] 120 | meta [ _name "apple-mobile-web-app-title"; _content websiteName ] 121 | meta [ _name "application-name"; _content websiteName ] 122 | meta [ _name "theme-color"; _content "#ffffff" ] 123 | 124 | if permalink.IsSome then 125 | // Twitter card tags 126 | twitterCard "card" "summary" 127 | twitterCard "site" "@dustinmoris" 128 | twitterCard "creator" "@dustinmoris" 129 | 130 | // Open Graph tags 131 | openGraph "title" pageTitle 132 | openGraph "url" permalink.Value 133 | openGraph "type" "website" 134 | openGraph "image" (Url.create "/giraffe.png") 135 | openGraph "image:alt" websiteName 136 | openGraph "image:width" "1094" 137 | openGraph "image:height" "729" 138 | 139 | // Google Fonts 140 | link [ _rel "preconnect"; _href "https://fonts.gstatic.com" ] 141 | link [ _href "https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap"; _rel "stylesheet" ] 142 | 143 | // Minified & bundled CSS 144 | css (Url.create minifiedCss.Path) 145 | 146 | // Google Analytics 147 | //if Env.isProduction then googleAnalytics 148 | 149 | // Additional (optional) header content 150 | if headerContent.IsSome then yield! headerContent.Value 151 | ] 152 | body [] [ 153 | header [] [ 154 | div [ _id "inner-header" ] [ 155 | img [ _id "logo"; _src (Url.create "/giraffe.png") ] 156 | ] 157 | ] 158 | nav [] [ 159 | div [ _id "inner-nav" ] [ 160 | ul [ _id "nav-links" ] [ 161 | li [] [ internalLink "/" "Home" ] 162 | li [] [ internalLink "/docs" "Documentation" ] 163 | li [] [ internalLink "/view-engine" "View Engine" ] 164 | li [] [ externalLink "https://github.com/giraffe-fsharp" "GitHub"] 165 | li [] [ externalLink "https://github.com/giraffe-fsharp/Giraffe/releases" "Releases" ] 166 | ] 167 | ] 168 | ] 169 | main [] bodyContent 170 | footer [] [ 171 | div [ _id "inner-footer" ] [ 172 | h5 [] [ rawText (sprintf "Copyright © %i, %s" DateTime.Now.Year "Dustin Moris Gorski") ] 173 | p [] [ 174 | rawText (sprintf "All content on this website, such as text, graphics, logos and images is the property of the Giraffe open source project and Dustin Moris Gorski.") 175 | ] 176 | ] 177 | ] 178 | ] 179 | ] 180 | 181 | let markdownView (title : string) (permalink : string) (content : XmlNode) = 182 | masterView (Some title) (Some permalink) None [ content ] 183 | 184 | // --------------------------------- 185 | // Markdown Parsing 186 | // --------------------------------- 187 | 188 | [] 189 | module MarkDog = 190 | open Markdig 191 | open Markdig.Extensions.AutoIdentifiers 192 | 193 | let private pipeline = 194 | MarkdownPipelineBuilder() 195 | .UseAutoIdentifiers(AutoIdentifierOptions.GitHub) 196 | .UsePipeTables() 197 | .Build() 198 | 199 | let toHtml (value : string) = 200 | Markdown.ToHtml(value, pipeline) 201 | 202 | // --------------------------------- 203 | // Web app 204 | // --------------------------------- 205 | 206 | [] 207 | module WebApp = 208 | open System 209 | open System.Net.Http 210 | open Microsoft.Extensions.Logging 211 | open Microsoft.Net.Http.Headers 212 | open Giraffe 213 | open Giraffe.EndpointRouting 214 | open Giraffe.ViewEngine 215 | 216 | let private allowCaching (duration : TimeSpan) : HttpHandler = 217 | publicResponseCaching (int duration.TotalSeconds) (Some "Accept-Encoding") 218 | 219 | let private cssHandler : HttpHandler = 220 | let eTag = EntityTagHeaderValue.FromString false Views.minifiedCss.Hash 221 | validatePreconditions (Some eTag) None 222 | >=> allowCaching (TimeSpan.FromDays 365.0) 223 | >=> setHttpHeader "Content-Type" "text/css" 224 | >=> setBodyFromString Views.minifiedCss.Content 225 | 226 | let private markdownHandler 227 | (markdownUrl : string) 228 | (title : string) 229 | (permalink : string) 230 | (lineStart : int) 231 | (linkReplacements : Map) : HttpHandler = 232 | fun next ctx -> 233 | task { 234 | let client = new HttpClient() 235 | let! allContent = client.GetStringAsync(markdownUrl) 236 | let content = 237 | linkReplacements 238 | |> (Map.fold(fun (c : string) key sub -> c.Replace(key, sub)) allContent) 239 | |> fun c -> c.Replace(markdownUrl, permalink) 240 | |> fun c -> c.Split([| Environment.NewLine |], StringSplitOptions.None) 241 | |> Array.skip lineStart 242 | |> String.concat Environment.NewLine 243 | let response = 244 | content 245 | |> MarkDog.toHtml 246 | |> rawText 247 | |> Views.markdownView title permalink 248 | |> htmlView 249 | return! response next ctx 250 | } 251 | 252 | let linkReplacements = 253 | [ 254 | "https://github.com/giraffe-fsharp/Giraffe/blob/master/README.md", (Url.create "/") 255 | "https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md", (Url.create "/docs") 256 | "https://github.com/giraffe-fsharp/Giraffe.ViewEngine/blob/master/README.md", (Url.create "/view-engine") 257 | ] |> Map.ofList 258 | 259 | let private indexHandler = 260 | allowCaching (TimeSpan.FromDays(1.0)) >=> 261 | markdownHandler 262 | "https://raw.githubusercontent.com/giraffe-fsharp/Giraffe/master/README.md" 263 | "Home" 264 | (Url.create "/") 265 | 4 266 | linkReplacements 267 | 268 | let private docsHandler = 269 | allowCaching (TimeSpan.FromDays(1.0)) >=> 270 | markdownHandler 271 | "https://raw.githubusercontent.com/giraffe-fsharp/Giraffe/master/DOCUMENTATION.md" 272 | "Documentation" 273 | (Url.create "/docs") 274 | 0 275 | linkReplacements 276 | 277 | let private viewEngineHandler = 278 | allowCaching (TimeSpan.FromDays(1.0)) >=> 279 | markdownHandler 280 | "https://raw.githubusercontent.com/giraffe-fsharp/Giraffe.ViewEngine/master/README.md" 281 | "View Engine" 282 | (Url.create "/view-engine") 283 | 2 284 | linkReplacements 285 | 286 | let private pingPongHandler : HttpHandler = 287 | noResponseCaching >=> text "pong" 288 | 289 | let private versionHandler : HttpHandler = 290 | noResponseCaching 291 | >=> json {| version = Env.appVersion |} 292 | 293 | let endpoints = 294 | [ 295 | GET_HEAD [ 296 | routef "/bundle.%s.css" (fun _ -> cssHandler) 297 | route "/" indexHandler 298 | route "/docs" docsHandler 299 | route "/view-engine" viewEngineHandler 300 | route "/ping" pingPongHandler 301 | route "/version" versionHandler 302 | ] 303 | ] 304 | 305 | let notFound = 306 | "Not Found" 307 | |> text 308 | |> RequestErrors.notFound 309 | 310 | let errorHandler (ex : Exception) (logger : ILogger) = 311 | logger.LogError(ex, "An unhandled exception has occurred while executing the request.") 312 | clearResponse >=> setStatusCode 500 >=> text ex.Message 313 | 314 | // --------------------------------- 315 | // Config and Main 316 | // --------------------------------- 317 | 318 | module Main = 319 | open System 320 | open System.Collections.Generic 321 | open Microsoft.AspNetCore.Builder 322 | open Microsoft.AspNetCore.Hosting 323 | open Microsoft.Extensions.Hosting 324 | open Microsoft.Extensions.DependencyInjection 325 | open Giraffe 326 | open Giraffe.EndpointRouting 327 | open Logfella 328 | open Logfella.LogWriters 329 | open Logfella.Adapters 330 | open Logfella.AspNetCore 331 | 332 | let private muteFilter = 333 | Func, exn, bool>( 334 | fun severity msg data ex -> 335 | msg.StartsWith "The response could not be cached for this request") 336 | 337 | let private createLogWriter (ctx : HttpContext option) = 338 | match Env.isProduction with 339 | | false -> ConsoleLogWriter(Env.logSeverity).AsLogWriter() 340 | | true -> 341 | let basic = 342 | GoogleCloudLogWriter 343 | .Create(Env.logSeverity) 344 | .AddServiceContext( 345 | Env.appName, 346 | Env.appVersion) 347 | .UseGoogleCloudTimestamp() 348 | .AddLabels( 349 | dict [ 350 | "appName", Env.appName 351 | "appVersion", Env.appVersion 352 | ]) 353 | let final = 354 | match ctx with 355 | | None -> basic 356 | | Some ctx -> 357 | basic 358 | .AddHttpContext(ctx) 359 | .AddCorrelationId(Guid.NewGuid().ToString("N")) 360 | Mute.When(muteFilter) 361 | .Otherwise(final) 362 | 363 | let private createReqLogWriter = 364 | Func(Some >> createLogWriter) 365 | 366 | let private toggleRequestLogging = 367 | Action( 368 | fun x -> x.IsEnabled <- Env.enableRequestLogging) 369 | 370 | let configureApp (appBuilder : IApplicationBuilder) = 371 | appBuilder 372 | .UseGiraffeErrorHandler(WebApp.errorHandler) 373 | .UseRequestScopedLogWriter(createReqLogWriter) 374 | .UseRequestLogging(toggleRequestLogging) 375 | .UseForwardedHeaders() 376 | .UseHttpsRedirection(Env.forceHttps, Env.domainName) 377 | .UseTrailingSlashRedirection() 378 | .UseStaticFiles() 379 | .UseResponseCaching() 380 | .UseResponseCompression() 381 | .UseRouting() 382 | .UseGiraffe(WebApp.endpoints) 383 | .UseGiraffe(WebApp.notFound) 384 | |> ignore 385 | 386 | let configureServices (services : IServiceCollection) = 387 | services 388 | .AddProxies( 389 | Env.proxyCount, 390 | Env.knownProxyNetworks, 391 | Env.knownProxies) 392 | .AddMemoryCache() 393 | .AddResponseCaching() 394 | .AddResponseCompression() 395 | .AddRouting() 396 | .AddGiraffe() 397 | |> ignore 398 | 399 | [] 400 | let main args = 401 | try 402 | Log.SetDefaultLogWriter(createLogWriter None) 403 | Logging.outputEnvironmentSummary Env.summary 404 | 405 | Host.CreateDefaultBuilder(args) 406 | .UseLogfella() 407 | .ConfigureWebHost( 408 | fun webHostBuilder -> 409 | webHostBuilder 410 | .ConfigureSentry( 411 | Env.sentryDsn, 412 | Env.name, 413 | Env.appVersion) 414 | .UseKestrel( 415 | fun k -> k.AddServerHeader <- false) 416 | .UseContentRoot(Env.appRoot) 417 | .UseWebRoot(Env.publicAssetsDir) 418 | .Configure(configureApp) 419 | .ConfigureServices(configureServices) 420 | |> ignore) 421 | .Build() 422 | .Run() 423 | 0 424 | with ex -> 425 | Log.Emergency("Host terminated unexpectedly.", ex) 426 | 1 --------------------------------------------------------------------------------