mathblog-0.6: src/MB/Server.hs
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 =
"<script type=\"text/javascript\">\n\
\function waitForReload()\n\
\{\n\
\ var req = new XMLHttpRequest();\n\
\ req.onreadystatechange = function() {\n\
\ if (req.readyState == 4) {\n\
\ if (req.status == 200 || req.status == 302) {\n\
\ location.reload();\n\
\ } else {\n\
\ setTimeout(waitForReload, 5000);\n\
\ }\n\
\ }\n\
\ };\n\
\ req.open(\"GET\", \"/__reload\", true);\n\
\ req.timeout = 5000;\n\
\ req.ontimeout = waitForReload;\n\
\ req.send();\n\
\}\n\
\waitForReload();\n\
\</script>"
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
}