module MB.Server ( withServing ) where import Prelude hiding (catch) import Control.Exception (SomeException, finally, catch) import Control.Concurrent import Control.Monad import Data.List (isPrefixOf) import System.Directory import System.Exit import System.FilePath (()) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BSC import Network.HTTP import Network.HTTP.Server import Network.HTTP.Server.Logger import qualified Network.URL as URL import Network.Socket (SockAddr) import System.IO.Temp import MB.Util (copyContents) import MB.Types reloadJS :: String reloadJS = "" serverProcessor :: Processor serverProcessor = nullProcessor { pageHead = Just reloadJS } withServing :: StartupConfig -> (StartupConfig -> (Blog -> Blog) -> IO () -> IO ()) -> IO () withServing conf act = do (hostname, portNum) <- case listenAddr conf of Nothing -> putStrLn "BUG: configuration did not provide a listen address!" >> exitFailure Just (h, p) -> return (h, p) tmpDir <- createTempDirectory "/tmp" "mbhtml.tmp" outputDir <- canonicalizePath tmpDir let noExists :: SomeException -> IO Bool noExists _ = return False ex <- (doesDirectoryExist $ htmlOutputDirectory conf) `catch` noExists when ex $ copyContents (htmlOutputDirectory conf) outputDir reloadChan <- newChan let genSignalAct = writeChan reloadChan () -- Set up the temp output directoryy and modify the config and blog. serverConf = conf { htmlOutputDirectory = outputDir } url = "http://" ++ hostname ++ ":" ++ show portNum ++ "/" blogTrans b = b { baseUrl = url , processors = processors b ++ [serverProcessor] , outputFS = blogOutputFS serverConf } httpConfig = defaultConfig { srvHost = hostname , srvPort = portNum , srvLog = quietLogger } cleanup = removeDirectoryRecursive (htmlOutputDirectory serverConf) handleError :: SomeException -> IO () handleError e = putStrLn $ "Error running server on " ++ url ++ ": " ++ show e -- Start the blog generation thread. _ <- forkIO $ (act serverConf blogTrans genSignalAct `catch` handleError) -- Wait for a successful blog generation, then start serving. readChan reloadChan putStrLn "Blog generation complete." putStrLn $ "Web server listening on " ++ url serverWith httpConfig (requestHandler outputDir reloadChan) `catch` handleError `finally` cleanup requestHandler :: FilePath -> Chan () -> SockAddr -> URL.URL -> Request BS.ByteString -> IO (Response BS.ByteString) requestHandler docRoot reloadChan _addr url _req = do -- If the request is for the special reload control URL, defer to -- the reload handler if URL.url_path url == "__reload" then reloadHandler reloadChan else fileHandler docRoot url reloadHandler :: Chan () -> IO (Response BS.ByteString) reloadHandler reloadChan = do -- Duplicate the channel. This effectively resets the read position -- on the channel, so only messages added after the time of -- duplication will be read. This way, reload handlers who are -- still waiting (from past page loads) even though their clients -- have long since disconnected will not consume events intended for -- current waiters. ch <- dupChan reloadChan readChan ch let bytes = BSC.pack "" return $ (respond Found :: Response BS.ByteString) { rspHeaders = [ Header HdrContentLength $ show $ BS.length bytes , Header HdrCacheControl "no-cache" , Header HdrConnection "close" ] , rspBody = bytes } fileHandler :: FilePath -> URL.URL -> IO (Response BS.ByteString) fileHandler docRoot url = do -- Concatenate the url_path of the URL to the output directory. -- Then attempt to canonicalize the result. If it succeeds and is -- not contained within the output directory, return a 404. let handleError :: SomeException -> IO (Response BS.ByteString) handleError = const $ return (resp404 url) serveFile path = do realPath <- canonicalizePath path if not (docRoot `isPrefixOf` realPath) then return (resp404 url) else do -- If the request was for a directory, rewrite it so we -- look for index.html instead e <- doesFileExist realPath if e then fileToResponse realPath else serveFile (path "index.html") serveFile (docRoot URL.url_path url) `catch` handleError resp404 :: URL.URL -> Response BS.ByteString resp404 url = let msg = BSC.pack $ "Not found: " ++ URL.exportURL url in (err_response NotFound :: Response BS.ByteString) { rspHeaders = [ Header HdrContentType "text/plain" , Header HdrContentLength $ show $ BS.length msg ] , rspBody = msg } fileToResponse :: FilePath -> IO (Response BS.ByteString) fileToResponse f = do bytes <- BS.readFile f return $ (respond Found :: Response BS.ByteString) { rspHeaders = [ Header HdrContentLength $ show $ BS.length bytes , Header HdrCacheControl "no-cache" ] , rspBody = bytes }