diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml new file mode 100644 index 0000000000000000000000000000000000000000..da39207161df983304d861d679111d46a9190eb6 --- /dev/null +++ b/.stylish-haskell.yaml @@ -0,0 +1,154 @@ +# stylish-haskell configuration file +# ================================== + +# The stylish-haskell tool is mainly configured by specifying steps. These steps +# are a list, so they have an order, and one specific step may appear more than +# once (if needed). Each file is processed by these steps in the given order. +steps: + # Convert some ASCII sequences to their Unicode equivalents. This is disabled + # by default. + # - unicode_syntax: + # # In order to make this work, we also need to insert the UnicodeSyntax + # # language pragma. If this flag is set to true, we insert it when it's + # # not already present. You may want to disable it if you configure + # # language extensions using some other method than pragmas. Default: + # # true. + # add_language_pragma: true + + # Import cleanup + - imports: + # There are different ways we can align names and lists. + # + # - global: Align the import names and import list throughout the entire + # file. + # + # - file: Like global, but don't add padding when there are no qualified + # imports in the file. + # + # - group: Only align the imports per group (a group is formed by adjacent + # import lines). + # + # - none: Do not perform any alignment. + # + # Default: global. + align: global + + # Folowing options affect only import list alignment. + # + # List align has following options: + # + # - after_alias: Import list is aligned with end of import including + # 'as' and 'hiding' keywords. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - with_alias: Import list is aligned with start of alias or hiding. + # + # > import qualified Data.List as List (concat, foldl, foldr, head, + # > init, last, length) + # + # - new_line: Import list starts always on new line. + # + # > import qualified Data.List as List + # > (concat, foldl, foldr, head, init, last, length) + # + # Default: after alias + list_align: after_alias + + # Long list align style takes effect when import is too long. This is + # determined by 'columns' setting. + # + # - inline: This option will put as much specs on same line as possible. + # + # - new_line: Import list will start on new line. + # + # - new_line_multiline: Import list will start on new line when it's + # short enough to fit to single line. Otherwise it'll be multiline. + # + # - multiline: One line per import list entry. + # Type with contructor list acts like single import. + # + # > import qualified Data.Map as M + # > ( empty + # > , singleton + # > , ... + # > , delete + # > ) + # + # Default: inline + long_list_align: inline + + # List padding determines indentation of import list on lines after import. + # This option affects 'list_align' and 'long_list_align'. + list_padding: 4 + + # Separate lists option affects formating of import list for type + # or class. The only difference is single space between type and list + # of constructors, selectors and class functions. + # + # - true: There is single space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable (fold, foldl, foldMap)) + # + # - false: There is no space between Foldable type and list of it's + # functions. + # + # > import Data.Foldable (Foldable(fold, foldl, foldMap)) + # + # Default: true + separate_lists: true + + # Language pragmas + - language_pragmas: + # We can generate different styles of language pragma lists. + # + # - vertical: Vertical-spaced language pragmas, one per line. + # + # - compact: A more compact style. + # + # - compact_line: Similar to compact, but wrap each line with + # `{-#LANGUAGE #-}'. + # + # Default: vertical. + style: vertical + + # Align affects alignment of closing pragma brackets. + # + # - true: Brackets are aligned in same collumn. + # + # - false: Brackets are not aligned together. There is only one space + # between actual import and closing bracket. + # + # Default: true + align: true + + # stylish-haskell can detect redundancy of some language pragmas. If this + # is set to true, it will remove those redundant pragmas. Default: true. + remove_redundant: true + + # Align the types in record declarations + - records: {} + + # Replace tabs by spaces. This is disabled by default. + # - tabs: + # # Number of spaces to use for each tab. Default: 8, as specified by the + # # Haskell report. + # spaces: 8 + + # Remove trailing whitespace + - trailing_whitespace: {} + +# A common setting is the number of columns (parts of) code will be wrapped +# to. Different steps take this into account. Default: 80. +columns: 79 + +# Sometimes, language extensions are specified in a cabal file or from the +# command line instead of using language pragmas in the file. stylish-haskell +# needs to be aware of these, so it can parse the file correctly. +# +# No language extensions are enabled by default. +# language_extensions: + # - TemplateHaskell + # - QuasiQuotes diff --git a/drafts/2016-02-15-serving-hakyll-site-with-warp.md b/drafts/2016-02-15-serving-hakyll-site-with-warp.md deleted file mode 100644 index c0a558b9e98c381cbb1729b239f5e6b0bc3e1dc0..0000000000000000000000000000000000000000 --- a/drafts/2016-02-15-serving-hakyll-site-with-warp.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: Serving a Hakyll site with Warp/Wai -date: Mon Feb 15 02:00:00 EDT 2016 -author: Eduardo Trujillo -uuid: 16b3b144-0fb7-422c-a31e-0fca75e28206 ---- - -Working with Hakyll has been great so far, but it seems the fun ends right after your site is generated and copied over to the remote server. From there, you have to deal with Nginx or Apache configuration files to give the final touches to the site, such as redirections, cache policies and additional headers. - -I've seen applications, such as XMonad, which employ Haskell scripts in-lieu of configuration files, and it's generally an enjoyable experience due to the added flexibility and customization it provides. - -With that in mind, wouldn't it be nice if the web server was in Haskell as well? After all, Hakyll has a preview function that works well enough. So it shouldn't be too hard to replicate that and add some custom logic on top of it. - -Well, that's exactly what I was wondering in the past few days, and (perhaps not too) surprisingly, it only took half a day's worth of work to get a working prototype. - -To get the job done, I added a new target to my Hakyll project, and included Warp, wai-static-app, and a bunch of WAI middleware as dependencies. The rest was merely a process of putting all the pieces together and ensuring the site behaved as expected. - -## The result - -* A fast static site server. -* Support for HTTP 2.0 -* Customized configurations for production, staging, and development. -* A more complete solution than `'hakyll preview`, since the site behaves exactly like it would on production. -* A clear path for adding more logic in the future (API, Proxy, etc). - -The server code is a few orders of magnate larger than a simple configuration file, but it's also under 200 lines of code. - -> server code - -## Lets dive in! - -Now let's dive into the details: - -### forkIO - -forkIO was one of the most interesting things I encountered while writing the server, and it also one of the last. - -If you are writing a server to run on a single port, you don't generally stop and ask yourself how you would modify it to run on multiple port simultaneously. - -Well, if you expect your site to use TLS and HTTP 2.0, you will most likely want to also redirect users from the insecure site to the secure one. This means you will need to listen on both, ports 80 and 443. - -My initial approach to this was very naive. I concluded that I could simply just run two servers by passing a flag at startup. However, this seemed cumbersome and error-prone. - -The real solution lead to learn a new part of Haskell that I had largely ignore before: Concurrency. - -I was please to find that `forkIO` allows you to create a lightweight thread that can execute another `IO ()` function. - -With it, I was able to spawn both servers at the same time: - -> Example - diff --git a/posts/2016-02-15-serving-hakyll-site-with-warp.md b/posts/2016-02-15-serving-hakyll-site-with-warp.md new file mode 100644 index 0000000000000000000000000000000000000000..2f8865cc2961caf0889ee3a107ace0ffee3cbcf0 --- /dev/null +++ b/posts/2016-02-15-serving-hakyll-site-with-warp.md @@ -0,0 +1,360 @@ +--- +title: Serving a Hakyll site with Warp/Wai +date: Mon Feb 15 02:00:00 EDT 2016 +author: Eduardo Trujillo +uuid: 16b3b144-0fb7-422c-a31e-0fca75e28206 +--- + +Working with Hakyll has been great so far, but it seems the fun ends right +after your site is generated and copied over to the remote server. From there, +you have to deal with Nginx or Apache configuration files for the final +touches, such as redirections, cache policies, or additional headers. + +I've seen applications, such as XMonad, which employ Haskell scripts in-lieu +of configuration files, and it's generally an enjoyable experience due to the +added flexibility and customization it provides. + +With that in mind, wouldn't it be nice if the web server was in Haskell as +well? After all, Hakyll has a preview function that works well enough. So, it +shouldn't be too hard to replicate that and add some custom logic on top of it. + +Well, that's exactly what I was wondering during the past few days, and +(perhaps not too) surprisingly, it only took half a day's worth of work to get +a working prototype. + +To get the job done, I added a new target to my Hakyll project's cabal file, +and included `warp`, `wai-static-app`, and a bunch of WAI middleware +(`wai-extra`) as dependencies. The rest was merely a process of putting all +the pieces together and ensuring the site behaved as expected. + +## The result + +* A fast static site server. +* Support for HTTP 2.0 and HTTPS. +* Customized configurations for production, staging, and development. +* A more complete solution than `hakyll server`, since the site behaves +exactly like it would on production. +* A clear path for adding more logic in the future (API, Proxy, etc). + +The server code is a few orders of magnitude larger than a simple configuration +file, but it's also under 200 lines of code. + +```haskell +{-# LANGUAGE OverloadedStrings #-} + +import Control.Concurrent (forkIO) +import qualified Data.ByteString as BS (ByteString, pack) +import Data.Maybe (fromMaybe, mapMaybe) +import Data.Monoid ((<>)) +import Data.String (fromString) +import qualified Data.Text as T (Text, concat, pack) +import qualified Data.Text.Encoding as TE (encodeUtf8) +import Network.Wai (Application, Middleware, + pathInfo) +import Network.Wai.Application.Static (defaultWebAppSettings, + ss404Handler, + ssAddTrailingSlash, + ssIndices, ssMaxAge, + ssRedirectToIndex, + staticApp) +import Network.Wai.Handler.Warp (defaultSettings, run, + setPort) +import Network.Wai.Handler.WarpTLS (runTLS, + tlsSettingsChain) +import Network.Wai.Middleware.AddHeaders (addHeaders) +import Network.Wai.Middleware.ForceDomain (forceDomain) +import Network.Wai.Middleware.ForceSSL (forceSSL) +import Network.Wai.Middleware.Gzip (def, gzip) +import Network.Wai.Middleware.RequestLogger (logStdout) +import Network.Wai.Middleware.Vhost (redirectTo) +import Safe (lastMay) +import System.Environment (lookupEnv) +import WaiAppStatic.Types (MaxAge (MaxAgeSeconds), + toPiece) + +-- | The core application. +-- It serves files from `_site` which is where Hakyll will place the generated +-- site. +staticSite :: Maybe String -> Application +staticSite path = staticApp + (defaultWebAppSettings $ fromString $ fromMaybe "_site" path) + { ssIndices = mapMaybe (toPiece . T.pack) ["index.html"] + , ssRedirectToIndex = False + , ssAddTrailingSlash = True + , ss404Handler = Just redirectApp + , ssMaxAge = MaxAgeSeconds 3600 + } + +-- | 404 handler. +-- We will redirect users to a 404 page if we can't locate the resource they +-- are looking for. +redirectApp :: Application +redirectApp req sendResponse = sendResponse $ redirectTo "/" + +-- | Gzip compression middleware. +gzipMiddleware :: Middleware +gzipMiddleware = gzip def + +-- | Domain redirection middleware. +-- When the site is live, we want to redirect users to the right domain name +-- regarles of whether they arrive from a www. domain, the server's IP address +-- or a spoof domain which is pointing to this server. +domainMiddleware :: Middleware +domainMiddleware = forceDomain + (\domain -> case domain of + "localhost" -> Nothing + "chromabits.com" -> Nothing + _ -> Just "chromabits.com") + +-- | Common headers middleware. +headersMiddleware :: Middleware +headersMiddleware = addHeaders + [ ("X-Frame-Options", "SAMEORIGIN") + , ("X-XSS-Protection", "1; mode=block") + , ("X-Content-Type-Options", "nosniff") + ] + +-- | Strict Transport Security middleware. +stsHeadersMiddleware :: Middleware +stsHeadersMiddleware = addHeaders + [("Strict-Transport-Security", "max-age=31536000; includeSubdomains")] + +-- | Content Security Policy middleware. +-- Here we add the CSP header which includes the policies for this blog. +cspHeadersMiddleware :: Middleware +cspHeadersMiddleware = addHeaders + [("Content-Security-Policy", TE.encodeUtf8 $ glue policies)] + where + glue :: [T.Text] -> T.Text + glue [] = "default-src 'none'" + glue [x] = x + glue xs = T.concat $ map (\x -> T.concat [x, "; "]) (init xs) ++ [last xs] + + policies :: [T.Text] + policies = [ "default-src 'self'" + , "script-src 'self' 'unsafe-inline' https://use.typekit.net" + <> " https://cdn.mathjax.org https://connect.facebook.net" + <> " https://*.twitter.com https://cdn.syndication.twimg.com" + <> " https://gist.github.com" + <> " https://*.google-analytics.com/ga.js" + , "img-src 'self' https: data: platform.twitter.com" + , "font-src 'self' data: https://use.typekit.net" + <> " https://cdn.mathjax.org" + , "style-src 'self' 'unsafe-inline' https://use.typekit.net" + <> " platform.twitter.com https://assets-cdn.github.com" + , "frame-src https://www.youtube.com https://www.slideshare.net" + <> " staticxx.facebook.com www.facebook.com" + ] + +-- | De-indefify middleware. +-- Redirects any path ending in `/index.html` to just `/`. +deindexifyMiddleware :: Middleware +deindexifyMiddleware app req sendResponse = + if lastMay (pathInfo req) == Just "index.html" + then sendResponse $ redirectTo newPath + else app req sendResponse + where + newPath :: BS.ByteString + newPath = TE.encodeUtf8 $ processPath oldPath + + processPath :: [T.Text] -> T.Text + processPath xs = case xs of + [] -> "/" + _ -> T.concat $ map prefixSlash xs + + oldPath :: [T.Text] + oldPath = init $ pathInfo req + + prefixSlash :: T.Text -> T.Text + prefixSlash x = T.concat ["/", x] + +-- | Serves a WAI Application on the specified port. +-- The target port is printed to stdout before hand, which can be useful for +-- debugging purposes. +listen :: Int -> Application -> IO () +listen port app = do + -- Inform which port we will be listening on. + putStrLn $ "Listening on port " ++ show port ++ "..." + -- Serve the WAI app using Warp + run port app + +-- | Serves a WAI Application on the specified port. +-- The target port is printed to stdout before hand, which can be useful for +-- debugging purposes. +listenTLS :: Int -> Application -> IO () +listenTLS port app = do + certPath <- lookupEnv "BLOG_TLS_CERT" + chainPath <- lookupEnv "BLOG_TLS_CHAIN" + keyPath <- lookupEnv "BLOG_TLS_KEY" + + let tlsSettings = tlsSettingsChain + (fromMaybe "cert.pem" certPath) + [fromMaybe "fullchain.pem" chainPath] + (fromMaybe "privkey.pem" keyPath) + let settings = setPort port defaultSettings + + -- Inform which port we will be listening on. + putStrLn $ "Listening on port " ++ show port ++ " (TLS)..." + -- Serve the WAI app using Warp + runTLS tlsSettings settings app + +-- | The entry point of the server application. +main :: IO () +main = do + stage <- lookupEnv "BLOG_STAGE" + path <- lookupEnv "BLOG_PATH" + + let liveMiddleware = logStdout + $ cspHeadersMiddleware + $ headersMiddleware + $ domainMiddleware + $ forceSSL + $ deindexifyMiddleware + $ gzipMiddleware + $ staticSite path + + -- Depending on the stage we will choose a different set of middleware to + -- apply to the application. + case fromMaybe "dev" stage of + -- "Production" + "live" -> do + forkIO $ listenTLS 443 $ stsHeadersMiddleware liveMiddleware + listen 80 liveMiddleware + "staging" -> do + forkIO $ listenTLS 8443 liveMiddleware + listen 8080 liveMiddleware + -- "Development" + _ -> listen 9090 (logStdout + $ headersMiddleware + $ deindexifyMiddleware + $ gzipMiddleware + $ staticSite path + ) + +``` + +## Lets dive in! + +Now let's dive into the details. The code above might not be too long, but its +doing a few interesting things that you might not usually get out of a simple +static site server: + +### forkIO + +`forkIO` (from `Control.Concurrent`) was one of the most interesting things I +encountered while writing the server, and it also one of the last. + +If you are writing a server to run on a single port, you don't generally stop +and ask yourself how you would modify it to run on multiple port +simultaneously. + +Well, if you expect your site to use TLS and HTTP 2.0, you will most likely +want to also redirect users from the insecure site to the secure one. This +means you will need to listen on both, ports 80 and 443. + +My initial approach to this was very naive. I concluded that I could simply +just run two servers by passing a flag at startup. However, this seemed +cumbersome and error-prone. + +The real solution lead to learn a new part of Haskell that I had largely ignore +before: Concurrency. + +I was please to find that `forkIO` allows you to create a lightweight thread +that can execute another `IO ()` function. + +With it, I was able to spawn both servers at the same time: + +```haskell +case fromMaybe "dev" stage of +-- "Production" +"live" -> do + forkIO $ listenTLS 443 $ stsHeadersMiddleware liveMiddleware + listen 80 liveMiddleware +``` + +### redirectTo + +Hidden away on the `Network.Wai.Middleware.Vhost` module, you can find the +`redirectTo` function, which is really useful for sending redirect (301) +responses to clients. + +On the server, I use it to define the 404 error handler: + +```haskell +redirectApp :: Application +redirectApp req sendResponse = sendResponse $ redirectTo "/" +``` + +### forceDomain and forceSSL + +To redirect users from an insecure (HTTP) site to a secure (HTTPS) site, the +you can use the `forceSSL`. + +To redirect users to the right domain name, you can use `forceDomain`, however, +you need to pass in a custom function which specifies when and to which domains +users should be redirected to. + +I prefer to use the naked domain for my blog, so I redirect anything that is +not `chromabits.com`: + +```haskell +domainMiddleware :: Middleware +domainMiddleware = forceDomain + (\domain -> case domain of + "localhost" -> Nothing + "chromabits.com" -> Nothing + _ -> Just "chromabits.com") +``` + +### No more index.html + +One thing I don't like about static sites is having `.html` extensions on the +URL. In Hakyll, I managed to partially work around this problem by simply +making sure that most paths result in `index.html` (e.g. `/archive/` is +actually `/archive/index.html`). However, you could still technically get to +the `index.html` path if you visit it directly. + +With this server, I wanted to add an additional redirect rule that would +redirect users away from paths with `index.html`. It is implemented on the +code above as `deindexifyMiddleware`. + +In the future, I might consider writing a smarter middleware capable of +handling more paths (e.g. `/archive` would simply be `/archive.html`). + +### Middleware stacks + +Something that was not immediately obvious to me from reading the `wai` +documentation was how to chain middleware together. By looking at the types, I +eventually realized that a `Middleware` takes in an `Application` and returns a +new `Application` which is wrapped with the middleware. This means that you can +simply feed each middleware to the next one and end up with an application that +is wrapped in the entire stack. + +```haskell +type Middleware = Application -> Application +``` + +However, you will need to pay special attention to the order in which they are +applied. For me, it was important that the logger middleware was at the top of +the stack so that every request was logged. + +```haskell +-- Everything is logged. +site = logStdout $ forceSSL $ app + +-- Some redirects will not be logged. +site = forceSSL $ logStdout $ app +``` + +## Overkill? Perhaps. Fun? Definitely. + +Given that I'm trying to learn more about Haskell, I'm constantly trying to +find small projects to tackle with the language. + +Writing your own server for a simple static site might sound overkill and, +most likely, take longer than dealing with a couple of Apache configuration +files. + +That is, if your goal is to just get a site up, just stick to a pre-built +server, but if you are looking to learn, writing your own might be worth your +time. diff --git a/server.hs b/server.hs index 174d9d6fbd1db77eb1b7eb81dee987d137c347ed..c82b0f2fd0a52d700fcff5117aaee0e0e66aff17 100644 --- a/server.hs +++ b/server.hs @@ -1,32 +1,34 @@ {-# LANGUAGE OverloadedStrings #-} -import Control.Concurrent (forkIO) -import Data.Monoid ((<>)) -import qualified Data.ByteString as BS (ByteString, pack) -import Data.String (fromString) -import qualified Data.Text as T (Text, concat, pack) -import qualified Data.Text.Encoding as TE (encodeUtf8) -import Data.Maybe (mapMaybe, fromMaybe) -import Safe (lastMay) -import System.Environment (lookupEnv) -import Network.Wai (Application, Middleware, pathInfo) -import Network.Wai.Handler.Warp (run, defaultSettings, setPort) -import Network.Wai.Handler.WarpTLS (runTLS, tlsSettingsChain) -import Network.Wai.Application.Static (staticApp - , defaultWebAppSettings - , ssIndices - , ssRedirectToIndex - , ssAddTrailingSlash - , ss404Handler - , ssMaxAge - ) -import WaiAppStatic.Types (toPiece, MaxAge(MaxAgeSeconds)) -import Network.Wai.Middleware.RequestLogger (logStdout) -import Network.Wai.Middleware.Gzip (gzip, def) -import Network.Wai.Middleware.ForceSSL (forceSSL) -import Network.Wai.Middleware.ForceDomain (forceDomain) -import Network.Wai.Middleware.Vhost (redirectTo) -import Network.Wai.Middleware.AddHeaders (addHeaders) +import Control.Concurrent (forkIO) +import qualified Data.ByteString as BS (ByteString, pack) +import Data.Maybe (fromMaybe, mapMaybe) +import Data.Monoid ((<>)) +import Data.String (fromString) +import qualified Data.Text as T (Text, concat, pack) +import qualified Data.Text.Encoding as TE (encodeUtf8) +import Network.Wai (Application, Middleware, + pathInfo) +import Network.Wai.Application.Static (defaultWebAppSettings, + ss404Handler, + ssAddTrailingSlash, + ssIndices, ssMaxAge, + ssRedirectToIndex, + staticApp) +import Network.Wai.Handler.Warp (defaultSettings, run, + setPort) +import Network.Wai.Handler.WarpTLS (runTLS, + tlsSettingsChain) +import Network.Wai.Middleware.AddHeaders (addHeaders) +import Network.Wai.Middleware.ForceDomain (forceDomain) +import Network.Wai.Middleware.ForceSSL (forceSSL) +import Network.Wai.Middleware.Gzip (def, gzip) +import Network.Wai.Middleware.RequestLogger (logStdout) +import Network.Wai.Middleware.Vhost (redirectTo) +import Safe (lastMay) +import System.Environment (lookupEnv) +import WaiAppStatic.Types (MaxAge (MaxAgeSeconds), + toPiece) -- | The core application. -- It serves files from `_site` whic is where Hakyll will place the generated @@ -167,7 +169,7 @@ main = do $ forceSSL $ deindexifyMiddleware $ gzipMiddleware - $ staticSite path + $ staticSite path -- Depending on the stage we will choose a different set of middleware to -- apply to the application.