diff --git a/README.md b/README.md index 861a3cb..2079176 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ # hastatic -[![Docker Build Status](https://img.shields.io/docker/build/abhin4v/hastatic.svg?style=flat-square)](https://hub.docker.com/r/abhin4v/hastatic/) -![MicroBadger Size](https://img.shields.io/microbadger/image-size/abhin4v/hastatic.svg?style=flat-square) +[![Docker Build Status](https://img.shields.io/docker/build/abhin4v/hastatic.svg?style=flat-square)](https://hub.docker.com/r/abhin4v/hastatic/) ![Docker Pulls](https://img.shields.io/docker/pulls/abhin4v/hastatic.svg?style=flat-square) ![MicroBadger Size](https://img.shields.io/microbadger/image-size/abhin4v/hastatic.svg?style=flat-square) _hastatic_ is a tiny static web server for Docker. ## Features -- A tiny web server, just 3.5MB in size. +- A tiny web server, just 5 MB in size. - Statically compiled binary with no dependencies. - Built for Docker. +- Supports HTTPS. - Supports custom 404 file. - Supports custom index files for URLs ending with "/". - Takes care to not serve hidden files. - Adds caching headers automatically. -- Does not support HTTPS. It is expected that you run it behind a reverse-proxy server with HTTPS support, like nginx. ## Usage @@ -30,10 +29,14 @@ CMD ["/usr/bin/hastatic"] Build and run: -``` +```bash $ docker build -t mywebsite . -$ docker run -p 8080:3000 mywebsite # run with default configs +$ # run with default configs +$ docker run -p 8080:3000 mywebsite +$ # run with custom configs $ docker run -e PORT=2000 -e NF_FILE=404.html -e IDX_FILE=index.html -p 8080:2000 mywebsite +$ # run with HTTPS support +$ docker run -e TLS_CERT_FILE=certificate.pem -e TLS_KEY_FILE=key.pem -p 443:3000 mywebsite ``` ## Configuration @@ -43,7 +46,9 @@ The Docker image supports these environment variable for configuration: - PORT: the port to run the web server on, default: 3000 - NF_FILE: name of the custom 404 file, default: `404.html` - IDX_FILE: name of the custom index files, default: `index.html` +- TLS_CERT_FILE: TLS certification file, optional, required for HTTPS support +- TLS_KEY_FILE: TLS key file, optional, required for HTTPS support ## Internals -_hastatic_ is written in Haskell, just 30 lines of it. It uses the excellent [Warp](https://hackage.haskell.org/package/warp) server underneath. +_hastatic_ is written in Haskell, just 60 lines of it. It uses the excellent [Warp](https://hackage.haskell.org/package/warp) server underneath with the [warp-tls](https://hackage.haskell.org/package/warp-tls) package for TLS support. diff --git a/hastatic.cabal b/hastatic.cabal index 62be457..24be909 100644 --- a/hastatic.cabal +++ b/hastatic.cabal @@ -2,7 +2,7 @@ -- -- see: https://github.com/sol/hpack -- --- hash: e49204f2f26a2ed8552e95f4f543b1cb4148cc509790f5d550ab98b70a722323 +-- hash: e7a5e0192bba9ed055c360b88189436d278d96883c36e66a4a17b70f134f4654 name: hastatic version: 0.9.0 @@ -31,4 +31,5 @@ executable hastatic , wai , wai-middleware-static , warp + , warp-tls default-language: Haskell2010 diff --git a/package.yaml b/package.yaml index 70837ad..89538d5 100644 --- a/package.yaml +++ b/package.yaml @@ -25,6 +25,7 @@ executables: dependencies: - wai - warp + - warp-tls - http-types - wai-middleware-static - text diff --git a/src/Main.hs b/src/Main.hs index c96593a..8e10583 100644 --- a/src/Main.hs +++ b/src/Main.hs @@ -5,12 +5,16 @@ import Data.Maybe (fromMaybe) import qualified Data.List as List import qualified Data.Text as T import Network.Wai +import qualified Network.Wai.Handler.WarpTLS as TLS import Network.Wai.Middleware.Static import Network.HTTP.Types (status404) -import Network.Wai.Handler.Warp (run) +import Network.Wai.Handler.Warp (run, defaultSettings, setPort) +import System.Exit (die) import System.Environment (lookupEnv) import Text.Read (readMaybe) +data TLS = Okay TLS.TLSSettings | Error String | None + indexHTML :: T.Text -> Middleware indexHTML indexFile app req respond = let path = pathInfo req @@ -22,22 +26,48 @@ indexHTML indexFile app req respond = [x] -> if "." `T.isInfixOf` x then [x] else [x, indexFile] (x:xs) -> x : fixPath xs +notFoundHandler :: FilePath -> Application notFoundHandler notFoundFile _ respond = respond $ responseFile status404 [("Content-Type", "text/html")] notFoundFile Nothing -main = do - mPort <- lookupEnv "PORT" - let port = fromMaybe 3000 (readMaybe =<< mPort) - mNotFoundFile <- lookupEnv "NF_FILE" - let notFoundFile = fromMaybe "404.html" mNotFoundFile - mIndexFile <- lookupEnv "IDX_FILE" - let indexFile = T.pack $ fromMaybe "index.html" mIndexFile +getTLSSettings :: IO TLS +getTLSSettings = do + tlsCertFile <- lookupEnv "TLS_CERT_FILE" + tlsKeyFile <- lookupEnv "TLS_KEY_FILE" - cache <- initCaching PublicStaticCaching - putStrLn $ "Starting server on port: " <> show port - run port - $ indexHTML indexFile - $ staticPolicy' cache (predicate noDot) - $ notFoundHandler notFoundFile + case (tlsCertFile, tlsKeyFile) of + (Nothing, Nothing) -> return None + (Just cert, Just key) -> return $ Okay $ TLS.tlsSettings cert key + _ -> return $ Error "Certificate file or Key file is missing" + +application :: [FilePath] -> IO Application +application excludedPaths = do + notFoundFile <- fromMaybe "404.html" <$> lookupEnv "NF_FILE" + indexFile <- T.pack . fromMaybe "index.html" <$> lookupEnv "IDX_FILE" + cache <- initCaching PublicStaticCaching + return + . indexHTML indexFile + . staticPolicy' cache polcy + . notFoundHandler + $ notFoundFile where noDot = not . List.isPrefixOf "." + polcy = predicate noDot >-> predicate (not . flip elem excludedPaths) + +main :: IO () +main = do + mPort <- lookupEnv "PORT" + let port = fromMaybe 3000 (readMaybe =<< mPort) + tlsSettings <- getTLSSettings + + case tlsSettings of + Okay tls -> do + app <- application [TLS.certFile tls, TLS.keyFile tls] + putStrLn $ "Starting HTTPS server on port: " <> show port + TLS.runTLS tls (setPort port defaultSettings) app + None -> do + app <- application [] + putStrLn $ "Starting HTTP server on port: " <> show port + run port app + Error msg -> + die $ "Error starting server: " <> msg