on master help sign in
Merge branch 'develop' into fix-ghcid-script
commit037bb5ca698bf9cf5371b0ed578fdd1356ef70ba authorAli Abrar <aliabrar@gmail.com> dateTue 26 Nov 2019, 11:43 (7 years ago) parentccee3ea33abe00
14 files changed+482−16
ChangeLog.md+5 FAQ.md+2−2 README.md+9−1 acme.nix+397 default.nix+17−2 dep/reflex-platform/github.json+2−2 lib/command/obelisk-command.cabal+1 lib/command/src/Obelisk/Command.hs+1−1 lib/command/src/Obelisk/Command/Project.hs+3−1 lib/command/src/Obelisk/Command/Run.hs+7−3 lib/frontend/src/Obelisk/Frontend.hs+2−2 lib/run/obelisk-run.cabal+4 lib/run/src/Obelisk/Run.hs+31−1 skeleton/frontend/src/Frontend.hs+1−1
ChangeLog.md+5
@@ -9,6 +9,11 @@ This project's release branch is `master`. This log is written from the perspect
99* Add "ob doc" command, which lists paths to haddock documentation for specified packages
1010* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
1111* Add support in obelisk-route for single parameters in URL paths
12+* Bump reflex-platform so that obelisk now uses reflex-dom 0.5.2.0
13+* Use a `--pure` nix shell in `ob run` for parity with `ob repl` and more resilience against "works on my machine".
14+* Use `--keep NIX_PATH` for all pure shells so references to `<nixpkgs>` continues to work.
15+* Backport ACMEv2 support in obelisk server to regain LetsEncrypt account creation.
16+* Enable https in `ob run`
1217
1318## v0.2.0.0 - 2019-8-17
1419
FAQ.md+2−2
@@ -81,7 +81,7 @@ project's dependencies and,
8181Assuming we are in a project created with `ob init`, `ob run` calls (see
8282`lib/command/src/Obelisk/Command.hs`):
8383
84− nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
84+ nix-shell --pure --keep NIX_PATH -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
8585
8686where
8787
@@ -109,7 +109,7 @@ It is defined in `lib/run/src/Obelisk/Run.hs`:
109109 * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110110 serve static assets.
111111* Starts `runWidget`
112− which itself runs
112+ which itself runs
113113 [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114114 (the *“TCP listen loop”*):
115115 * it binds to TCP socket, and
README.md+9−1
@@ -7,6 +7,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
77 - [Hoogle](#hoogle)
88 - [Adding Packages](#adding-packages)
99 - [Adding Package Overrides](#adding-package-overrides)
10+ - [Running over https](#running-over-https)
1011- [Deploying](#deploying)
1112 - [Locally](#locally)
1213 - [EC2](#ec2)
@@ -105,7 +106,8 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
105106ob run
106107```
107108
108−Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
109+Now, with an appropriate browser, go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
110+Firefox will not be able to properly run the development website due to [issue 460](https://github.com/obsidiansystems/obelisk/issues/460). Fortunately, this problem does not occur on a fully built website.
109111
110112Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
111113
@@ -148,6 +150,12 @@ project ./. ({ pkgs, ... }: {
148150
149151For further information see [the Haskell section](https://nixos.org/nixpkgs/manual/#users-guide-to-the-haskell-infrastructure) of nixpkgs Contributors Guide.
150152
153+### Running over https
154+
155+To run your app locally over https, update the protocol in `config/common/route` to `https`, and then use `ob run` as normal.
156+
157+Since Obelisk generates a self-signed certificate for running https, the browser will issue a warning about using an invalid certificate. On Chrome, you can go to `chrome://flags/#allow-insecure-localhost` to enable invalid certificates for localhost.
158+
151159## Deploying
152160
153161### Locally
acme.nixnew file+397
@@ -0,0 +1,397 @@
1+### COPIED FROM
2+### https://github.com/NixOS/nixpkgs/blob/1d36ad6d16dbf1d3937f899a087a4360332eb141/nixos/modules/security/acme.nix
3+###
4+### to support new plugins in simp_le for ACMEv2 support.
5+### Changed lines are marked with a comment: ### CHANGED
6+
7+{ config, lib, pkgs, ... }:
8+
9+with lib;
10+
11+let
12+
13+ cfg = config.security.acme;
14+
15+ certOpts = { name, ... }: {
16+ options = {
17+ webroot = mkOption {
18+ type = types.str;
19+ example = "/var/lib/acme/acme-challenges";
20+ description = ''
21+ Where the webroot of the HTTP vhost is located.
22+ <filename>.well-known/acme-challenge/</filename> directory
23+ will be created below the webroot if it doesn't exist.
24+ <literal>http://example.org/.well-known/acme-challenge/</literal> must also
25+ be available (notice unencrypted HTTP).
26+ '';
27+ };
28+
29+ domain = mkOption {
30+ type = types.str;
31+ default = name;
32+ description = "Domain to fetch certificate for (defaults to the entry name)";
33+ };
34+
35+ email = mkOption {
36+ type = types.nullOr types.str;
37+ default = null;
38+ description = "Contact email address for the CA to be able to reach you.";
39+ };
40+
41+ user = mkOption {
42+ type = types.str;
43+ default = "root";
44+ description = "User running the ACME client.";
45+ };
46+
47+ group = mkOption {
48+ type = types.str;
49+ default = "root";
50+ description = "Group running the ACME client.";
51+ };
52+
53+ allowKeysForGroup = mkOption {
54+ type = types.bool;
55+ default = false;
56+ description = ''
57+ Give read permissions to the specified group
58+ (<option>security.acme.cert.&lt;name&gt;.group</option>) to read SSL private certificates.
59+ '';
60+ };
61+
62+ postRun = mkOption {
63+ type = types.lines;
64+ default = "";
65+ example = "systemctl reload nginx.service";
66+ description = ''
67+ Commands to run after new certificates go live. Typically
68+ the web server and other servers using certificates need to
69+ be reloaded.
70+
71+ Executed in the same directory with the new certificate.
72+ '';
73+ };
74+
75+ plugins = mkOption {
76+ type = types.listOf (types.enum [
77+ "cert.der" "cert.pem" "chain.pem" "external.sh"
78+ "fullchain.pem" "full.pem" "key.der" "key.pem" "account_key.json" "account_reg.json" ### CHANGED
79+ ]);
80+ default = [ "fullchain.pem" "full.pem" "key.pem" "account_key.json" "account_reg.json" ]; ### CHANGED
81+ description = ''
82+ Plugins to enable. With default settings simp_le will
83+ store public certificate bundle in <filename>fullchain.pem</filename>,
84+ private key in <filename>key.pem</filename> and those two previous
85+ files combined in <filename>full.pem</filename> in its state directory.
86+ '';
87+ };
88+
89+ activationDelay = mkOption {
90+ type = types.nullOr types.str;
91+ default = null;
92+ description = ''
93+ Systemd time span expression to delay copying new certificates to main
94+ state directory. See <citerefentry><refentrytitle>systemd.time</refentrytitle>
95+ <manvolnum>7</manvolnum></citerefentry>.
96+ '';
97+ };
98+
99+ preDelay = mkOption {
100+ type = types.lines;
101+ default = "";
102+ description = ''
103+ Commands to run after certificates are re-issued but before they are
104+ activated. Typically the new certificate is published to DNS.
105+
106+ Executed in the same directory with the new certificate.
107+ '';
108+ };
109+
110+ extraDomains = mkOption {
111+ type = types.attrsOf (types.nullOr types.str);
112+ default = {};
113+ example = literalExample ''
114+ {
115+ "example.org" = "/srv/http/nginx";
116+ "mydomain.org" = null;
117+ }
118+ '';
119+ description = ''
120+ A list of extra domain names, which are included in the one certificate to be issued, with their
121+ own server roots if needed.
122+ '';
123+ };
124+ };
125+ };
126+
127+in
128+
129+{
130+
131+ ###### interface
132+
133+ options = {
134+ security.acme = {
135+ directory = mkOption {
136+ default = "/var/lib/acme";
137+ type = types.str;
138+ description = ''
139+ Directory where certs and other state will be stored by default.
140+ '';
141+ };
142+
143+ validMin = mkOption {
144+ type = types.int;
145+ default = 30 * 24 * 3600;
146+ description = "Minimum remaining validity before renewal in seconds.";
147+ };
148+
149+ renewInterval = mkOption {
150+ type = types.str;
151+ default = "weekly";
152+ description = ''
153+ Systemd calendar expression when to check for renewal. See
154+ <citerefentry><refentrytitle>systemd.time</refentrytitle>
155+ <manvolnum>7</manvolnum></citerefentry>.
156+ '';
157+ };
158+
159+ preliminarySelfsigned = mkOption {
160+ type = types.bool;
161+ default = true;
162+ description = ''
163+ Whether a preliminary self-signed certificate should be generated before
164+ doing ACME requests. This can be useful when certificates are required in
165+ a webserver, but ACME needs the webserver to make its requests.
166+
167+ With preliminary self-signed certificate the webserver can be started and
168+ can later reload the correct ACME certificates.
169+ '';
170+ };
171+
172+ production = mkOption {
173+ type = types.bool;
174+ default = true;
175+ description = ''
176+ If set to true, use Let's Encrypt's production environment
177+ instead of the staging environment. The main benefit of the
178+ staging environment is to get much higher rate limits.
179+
180+ See
181+ <literal>https://letsencrypt.org/docs/staging-environment</literal>
182+ for more detail.
183+ '';
184+ };
185+
186+ certs = mkOption {
187+ default = { };
188+ type = with types; attrsOf (submodule certOpts);
189+ description = ''
190+ Attribute set of certificates to get signed and renewed.
191+ '';
192+ example = literalExample ''
193+ {
194+ "example.com" = {
195+ webroot = "/var/www/challenges/";
196+ email = "foo@example.com";
197+ extraDomains = { "www.example.com" = null; "foo.example.com" = "/var/www/foo/"; };
198+ };
199+ "bar.example.com" = {
200+ webroot = "/var/www/challenges/";
201+ email = "bar@example.com";
202+ };
203+ }
204+ '';
205+ };
206+ };
207+ };
208+
209+ ###### implementation
210+ config = mkMerge [
211+ (mkIf (cfg.certs != { }) {
212+
213+ systemd.services = let
214+ services = concatLists servicesLists;
215+ servicesLists = mapAttrsToList certToServices cfg.certs;
216+ certToServices = cert: data:
217+ let
218+ cpath = lpath + optionalString (data.activationDelay != null) ".staging";
219+ lpath = "${cfg.directory}/${cert}";
220+ rights = if data.allowKeysForGroup then "750" else "700";
221+ cmdline = [ "-v" "-d" data.domain "--default_root" data.webroot "--valid_min" cfg.validMin ]
222+ ++ optionals (data.email != null) [ "--email" data.email ]
223+ ++ concatMap (p: [ "-f" p ]) data.plugins
224+ ++ concatLists (mapAttrsToList (name: root: [ "-d" (if root == null then name else "${name}:${root}")]) data.extraDomains)
225+ ++ optionals (!cfg.production) ["--server" "https://acme-staging.api.letsencrypt.org/directory"];
226+ acmeService = {
227+ description = "Renew ACME Certificate for ${cert}";
228+ after = [ "network.target" "network-online.target" ];
229+ wants = [ "network-online.target" ];
230+ serviceConfig = {
231+ Type = "oneshot";
232+ SuccessExitStatus = [ "0" "1" ];
233+ PermissionsStartOnly = true;
234+ User = data.user;
235+ Group = data.group;
236+ PrivateTmp = true;
237+ };
238+ path = with pkgs; [ simp_le systemd ];
239+ preStart = ''
240+ mkdir -p '${cfg.directory}'
241+ chown 'root:root' '${cfg.directory}'
242+ chmod 755 '${cfg.directory}'
243+ if [ ! -d '${cpath}' ]; then
244+ mkdir '${cpath}'
245+ fi
246+ chmod ${rights} '${cpath}'
247+ chown -R '${data.user}:${data.group}' '${cpath}'
248+ mkdir -p '${data.webroot}/.well-known/acme-challenge'
249+ chown -R '${data.user}:${data.group}' '${data.webroot}/.well-known/acme-challenge'
250+ '';
251+ script = ''
252+ cd '${cpath}'
253+ set +e
254+ simp_le ${escapeShellArgs cmdline}
255+ EXITCODE=$?
256+ set -e
257+ echo "$EXITCODE" > /tmp/lastExitCode
258+ exit "$EXITCODE"
259+ '';
260+ postStop = ''
261+ cd '${cpath}'
262+
263+ if [ -e /tmp/lastExitCode ] && [ "$(cat /tmp/lastExitCode)" = "0" ]; then
264+ ${if data.activationDelay != null then ''
265+
266+ ${data.preDelay}
267+
268+ if [ -d '${lpath}' ]; then
269+ systemd-run --no-block --on-active='${data.activationDelay}' --unit acme-setlive-${cert}.service
270+ else
271+ systemctl --wait start acme-setlive-${cert}.service
272+ fi
273+ '' else data.postRun}
274+
275+ # noop ensuring that the "if" block is non-empty even if
276+ # activationDelay == null and postRun == ""
277+ true
278+ fi
279+ '';
280+
281+ before = [ "acme-certificates.target" ];
282+ wantedBy = [ "acme-certificates.target" ];
283+ };
284+ delayService = {
285+ description = "Set certificate for ${cert} live";
286+ path = with pkgs; [ rsync ];
287+ serviceConfig = {
288+ Type = "oneshot";
289+ };
290+ script = ''
291+ rsync -a --delete-after '${cpath}/' '${lpath}'
292+ '';
293+ postStop = data.postRun;
294+ };
295+ selfsignedService = {
296+ description = "Create preliminary self-signed certificate for ${cert}";
297+ path = [ pkgs.openssl ];
298+ preStart = ''
299+ if [ ! -d '${cpath}' ]
300+ then
301+ mkdir -p '${cpath}'
302+ chmod ${rights} '${cpath}'
303+ chown '${data.user}:${data.group}' '${cpath}'
304+ fi
305+ '';
306+ script =
307+ ''
308+ workdir="$(mktemp -d)"
309+
310+ # Create CA
311+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/ca.pass.key 2048
312+ openssl rsa -passin pass:xxxx -in $workdir/ca.pass.key -out $workdir/ca.key
313+ openssl req -new -key $workdir/ca.key -out $workdir/ca.csr \
314+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=Security Department/CN=example.com"
315+ openssl x509 -req -days 1 -in $workdir/ca.csr -signkey $workdir/ca.key -out $workdir/ca.crt
316+
317+ # Create key
318+ openssl genrsa -des3 -passout pass:xxxx -out $workdir/server.pass.key 2048
319+ openssl rsa -passin pass:xxxx -in $workdir/server.pass.key -out $workdir/server.key
320+ openssl req -new -key $workdir/server.key -out $workdir/server.csr \
321+ -subj "/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=example.com"
322+ openssl x509 -req -days 1 -in $workdir/server.csr -CA $workdir/ca.crt \
323+ -CAkey $workdir/ca.key -CAserial $workdir/ca.srl -CAcreateserial \
324+ -out $workdir/server.crt
325+
326+ # Copy key to destination
327+ cp $workdir/server.key ${cpath}/key.pem
328+
329+ # Create fullchain.pem (same format as "simp_le ... -f fullchain.pem" creates)
330+ cat $workdir/{server.crt,ca.crt} > "${cpath}/fullchain.pem"
331+
332+ # Create full.pem for e.g. lighttpd
333+ cat $workdir/{server.key,server.crt,ca.crt} > "${cpath}/full.pem"
334+
335+ # Give key acme permissions
336+ chown '${data.user}:${data.group}' "${cpath}/"{key,fullchain,full}.pem
337+ chmod ${rights} "${cpath}/"{key,fullchain,full}.pem
338+ '';
339+ serviceConfig = {
340+ Type = "oneshot";
341+ PermissionsStartOnly = true;
342+ PrivateTmp = true;
343+ User = data.user;
344+ Group = data.group;
345+ };
346+ unitConfig = {
347+ # Do not create self-signed key when key already exists
348+ ConditionPathExists = "!${cpath}/key.pem";
349+ };
350+ before = [
351+ "acme-selfsigned-certificates.target"
352+ ];
353+ wantedBy = [
354+ "acme-selfsigned-certificates.target"
355+ ];
356+ };
357+ in (
358+ [ { name = "acme-${cert}"; value = acmeService; } ]
359+ ++ optional cfg.preliminarySelfsigned { name = "acme-selfsigned-${cert}"; value = selfsignedService; }
360+ ++ optional (data.activationDelay != null) { name = "acme-setlive-${cert}"; value = delayService; }
361+ );
362+ servicesAttr = listToAttrs services;
363+ injectServiceDep = {
364+ after = [ "acme-selfsigned-certificates.target" ];
365+ wants = [ "acme-selfsigned-certificates.target" "acme-certificates.target" ];
366+ };
367+ in
368+ servicesAttr //
369+ (if config.services.nginx.enable then { nginx = injectServiceDep; } else {}) //
370+ (if config.services.lighttpd.enable then { lighttpd = injectServiceDep; } else {});
371+
372+ systemd.timers = flip mapAttrs' cfg.certs (cert: data: nameValuePair
373+ ("acme-${cert}")
374+ ({
375+ description = "Renew ACME Certificate for ${cert}";
376+ wantedBy = [ "timers.target" ];
377+ timerConfig = {
378+ OnCalendar = cfg.renewInterval;
379+ Unit = "acme-${cert}.service";
380+ Persistent = "yes";
381+ AccuracySec = "5m";
382+ RandomizedDelaySec = "1h";
383+ };
384+ })
385+ );
386+
387+ systemd.targets."acme-selfsigned-certificates" = mkIf cfg.preliminarySelfsigned {};
388+ systemd.targets."acme-certificates" = {};
389+ })
390+
391+ ];
392+
393+ meta = {
394+ maintainers = with lib.maintainers; [ abbradar fpletz globin ];
395+ #doc = ./acme.xml; ### CHANGED
396+ };
397+}
default.nix+17−2
@@ -1,3 +1,9 @@
1+let
2+ nixos1909 = import (builtins.fetchTarball {
3+ url = https://github.com/NixOS/nixpkgs-channels/archive/nixos-19.09.tar.gz;
4+ sha256 = "0ysb2017n8g0bpkxy3lsnlf6mcya5gqwggmwdjxlfnj1ilj3lnqz";
5+ }) {};
6+in
17{ system ? builtins.currentSystem
28, profiling ? false
39, iosSdkVersion ? "10.2"
@@ -53,7 +59,7 @@ let
5359 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (cleanSource ./lib/asset/serve-snap) {};
5460 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
5561 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
56− obelisk-command = self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {};
62+ obelisk-command = haskellLib.overrideCabal (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}) { librarySystemDepends = [ pkgs.nix ]; };
5763 obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
5864 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
5965 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
@@ -69,7 +75,7 @@ let
6975 # Dynamic linking with split objects dramatically increases startup time (about
7076 # 0.5 seconds on a decent machine with SSD), so we do `justStaticExecutables`.
7177 obelisk-command = haskellLib.overrideCabal
72− (haskellLib.addOptparseApplicativeCompletionScripts "ob"
78+ (haskellLib.generateOptparseApplicativeCompletion "ob"
7379 (haskellLib.justStaticExecutables super.obelisk-command))
7480 (drv: {
7581 buildTools = (drv.buildTools or []) ++ [ pkgs.buildPackages.makeWrapper ];
@@ -237,6 +243,15 @@ in rec {
237243 imports = [
238244 (serverModules.mkBaseEc2 args)
239245 (serverModules.mkObeliskApp args)
246+ ./acme.nix
247+ ];
248+ disabledModules = [
249+ (pkgs.path + /nixos/modules/security/acme.nix)
250+ ];
251+ nixpkgs.overlays = [
252+ (self: super: {
253+ simp_le = nixos1909.simp_le;
254+ })
240255 ];
241256 };
242257 };
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "b75dfe258c31eb1688090d121f33b62a07071589",
6− "sha256": "0isvqn6y8nnfb4a3nabjsafrz4mfa01vhkn6i61idh6zfb8gciqf"
5+ "rev": "aa8a9d1ac3d41ad51fbe04e575d4350da65cf3db",
6+ "sha256": "0hffy5rcfy6ay06ww3cxykqzp024jfcdg6w1yrmnf0h80sjfgdcm"
77}
lib/command/obelisk-command.cabal+1
@@ -46,6 +46,7 @@ library
4646 , transformers
4747 , unix
4848 , unordered-containers
49+ , which
4950 , yaml
5051 exposed-modules:
5152 Obelisk.App
lib/command/src/Obelisk/Command.hs+1−1
@@ -106,7 +106,7 @@ data ObInternal
106106inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
107107inNixShell' p = withProjectRoot "." $ \root -> do
108108 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
109− projectShell root False "ghc" (Just cmd)
109+ projectShell root True "ghc" (Just cmd)
110110 where
111111 mkCmd = do
112112 argsCfg <- getArgsConfig
lib/command/src/Obelisk/Command/Project.hs+3−1
@@ -206,7 +206,9 @@ projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String ->
206206projectShell root isPure shellName command = do
207207 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208208 [ "default.nix"] <>
209− [ "--pure" | isPure ] <>
209+ -- Keep $NIX_PATH in the env for --pure shells so '<nixpkgs>' works in sub-commands
210+ -- TODO: Don't use <nixpkgs> for anything!
211+ (if isPure then [ "--pure", "--keep", "NIX_PATH" ] else []) <>
210212 [ "-A"
211213 , "shells." <> shellName
212214 ] <> case command of
lib/command/src/Obelisk/Command/Run.hs+7−3
@@ -3,6 +3,7 @@
33{-# LANGUAGE MultiParamTypeClasses #-}
44{-# LANGUAGE OverloadedStrings #-}
55{-# LANGUAGE ScopedTypeVariables #-}
6+{-# LANGUAGE TemplateHaskell #-}
67module Obelisk.Command.Run where
78
89import Control.Exception (bracket)
@@ -32,6 +33,7 @@ import System.Directory
3233import System.FilePath
3334import System.Process (proc)
3435import System.IO.Temp (withSystemTempDirectory)
36+import System.Which (staticWhich)
3537import Data.ByteString (ByteString)
3638
3739import Obelisk.App (MonadObelisk, ObeliskT)
@@ -56,6 +58,8 @@ data CabalPackageInfo = CabalPackageInfo
5658-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
5759run :: ObeliskT IO ()
5860run = do
61+ let nixPath = $(staticWhich "nix")
62+ nixBuildPath = $(staticWhich "nix-build")
5963 pkgs <- getLocalPkgs
6064 withGhciScript pkgs $ \dotGhciPath -> do
6165 freePort <- getFreePort
@@ -64,7 +68,7 @@ run = do
6468 then root
6569 else "./" <> root
6670 isDerivation <- readProcessAndLogStderr Debug $
67− proc "nix"
71+ proc nixPath
6872 [ "eval"
6973 , "-f"
7074 , root
@@ -76,12 +80,12 @@ run = do
7680 -- Check whether the impure static files are a derivation (and so must be built)
7781 if isDerivation == "1"
7882 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
79− proc "nix-build"
83+ proc nixBuildPath
8084 [ "--no-out-link"
8185 , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
8286 ]
8387 else readProcessAndLogStderr Debug $
84− proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
88+ proc nixPath ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
8589 putLog Debug $ "Assets impurely loaded from: " <> assets
8690 runGhcid dotGhciPath $ Just $ unwords
8791 [ "Obelisk.Run.run"
lib/frontend/src/Obelisk/Frontend.hs+2−2
@@ -229,7 +229,7 @@ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = d
229229renderFrontendHtml
230230 :: ( t ~ DomTimeline
231231 , MonadIO m
232− , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
232+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (HydratableT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))))
233233 )
234234 => Map Text ByteString
235235 -> Cookies
@@ -241,7 +241,7 @@ renderFrontendHtml
241241 -> m ByteString
242242renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
243243 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
244− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
244+ html <- fmap snd $ liftIO $ renderStatic $ runHydratableT $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
245245 el "html" $ do
246246 el "head" $ do
247247 baseTag
lib/run/obelisk-run.cabal+4
@@ -15,6 +15,7 @@ library
1515 , dependent-sum
1616 , dependent-sum-template
1717 , ghcjs-dom
18+ , HsOpenSSL
1819 , http-client
1920 , http-reverse-proxy
2021 , http-types
@@ -36,10 +37,13 @@ library
3637 , snap-core
3738 , streaming-commons
3839 , text
40+ , time
3941 , universe
42+ , utf8-string
4043 , wai
4144 , wai-websockets
4245 , warp
46+ , warp-tls
4347 , websockets
4448 exposed-modules:
4549 Obelisk.Run
lib/run/src/Obelisk/Run.hs+31−1
@@ -23,6 +23,7 @@ import Data.ByteString (ByteString)
2323import qualified Data.ByteString as BS
2424import qualified Data.ByteString.Char8 as BSC
2525import qualified Data.ByteString.Lazy.Char8 as BSLC
26+import qualified Data.ByteString.UTF8 as BSUTF8
2627import Data.Functor.Identity
2728import Data.List (uncons)
2829import Data.Map (Map)
@@ -34,6 +35,7 @@ import Data.String (fromString)
3435import Data.Text (Text)
3536import qualified Data.Text as T
3637import qualified Data.Text.Encoding as T
38+import Data.Time.Clock (getCurrentTime, addUTCTime)
3739import Language.Javascript.JSaddle.Run (syncPoint)
3840import Language.Javascript.JSaddle.WebSockets
3941import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -43,6 +45,7 @@ import Network.Socket
4345import Network.Wai (Application)
4446import qualified Network.Wai as W
4547import Network.Wai.Handler.Warp
48+import Network.Wai.Handler.WarpTLS
4649import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
4750import Network.WebSockets (ConnectionOptions)
4851import Network.WebSockets.Connection (defaultConnectionOptions)
@@ -50,6 +53,10 @@ import qualified Obelisk.Asset.Serve.Snap as Snap
5053import Obelisk.Backend
5154import Obelisk.Frontend
5255import Obelisk.Route.Frontend
56+import qualified OpenSSL.PEM as PEM
57+import qualified OpenSSL.RSA as RSA
58+import qualified OpenSSL.X509 as X509
59+import qualified OpenSSL.X509.Request as X509Request
5360import Reflex.Dom.Core
5461import Snap.Core (Snap)
5562import System.Environment
@@ -113,13 +120,36 @@ runWidget conf configs frontend validFullEncoder = do
113120 beforeMainLoop = do
114121 putStrLn $ "Frontend running on " <> T.unpack (URI.render uri)
115122 settings = setBeforeMainLoop beforeMainLoop (setPort port (setTimeout 3600 defaultSettings))
123+ -- Providing TLS here will also incidentally provide it to proxied requests to the backend.
124+ prepareRunner = case (uri ^? uriScheme . _Just . unRText) of
125+ Just "https" -> do
126+ -- Generate a private key and self-signed certificate for TLS
127+ privateKey <- RSA.generateRSAKey' 2048 3
128+
129+ certRequest <- X509Request.newX509Req
130+ _ <- X509Request.setPublicKey certRequest privateKey
131+ _ <- X509Request.signX509Req certRequest privateKey Nothing
132+
133+ cert <- X509.newX509 >>= X509Request.makeX509FromReq certRequest
134+ _ <- X509.setPublicKey cert privateKey
135+ now <- getCurrentTime
136+ _ <- X509.setNotBefore cert $ addUTCTime (-1) now
137+ _ <- X509.setNotAfter cert $ addUTCTime (365 * 24 * 60 * 60) now
138+ _ <- X509.signX509 cert privateKey Nothing
139+
140+ certByteString <- BSUTF8.fromString <$> PEM.writeX509 cert
141+ privateKeyByteString <- BSUTF8.fromString <$> PEM.writePKCS8PrivateKey privateKey Nothing
142+
143+ return $ runTLSSocket (tlsSettingsMemory certByteString privateKeyByteString)
144+ _ -> return runSettingsSocket
145+ runner <- prepareRunner
116146 bracket
117147 (bindPortTCPRetry settings (logPortBindErr port) (_runConfig_retryTimeout conf))
118148 close
119149 (\skt -> do
120150 man <- newManager defaultManagerSettings
121151 app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
122− runSettingsSocket settings skt app)
152+ runner settings skt app)
123153
124154obeliskApp
125155 :: forall frontendRoute backendRoute
skeleton/frontend/src/Frontend.hs+1−1
@@ -25,6 +25,6 @@ frontend = Frontend
2525 exampleConfig <- getConfig "common/example"
2626 case exampleConfig of
2727 Nothing -> text "No config file found in config/common/example"
28− Just s -> text (T.decodeUtf8 s)
28+ Just s -> text $ T.decodeUtf8 s
2929 return ()
3030 }