forked from GitHub/gf-core
GFServer.hs: code improvements
Use a monad in the request handler to make the code a bit more modular and readable.
This commit is contained in:
@@ -5,6 +5,8 @@ import Data.List(partition,stripPrefix,tails,isInfixOf)
|
|||||||
import Data.Maybe(mapMaybe)
|
import Data.Maybe(mapMaybe)
|
||||||
import qualified Data.Map as M
|
import qualified Data.Map as M
|
||||||
import Control.Monad(when)
|
import Control.Monad(when)
|
||||||
|
import Control.Monad.State(StateT(..),get,gets,put)
|
||||||
|
import Control.Monad.Error(ErrorT(..),Error(..))
|
||||||
import System.Random(randomRIO)
|
import System.Random(randomRIO)
|
||||||
import System.IO(stdout,stderr,hPutStrLn)
|
import System.IO(stdout,stderr,hPutStrLn)
|
||||||
import System.IO.Error(try,ioError,isAlreadyExistsError)
|
import System.IO.Error(try,ioError,isAlreadyExistsError)
|
||||||
@@ -41,7 +43,7 @@ import RunHTTP(cgiHandler)
|
|||||||
--logFile :: FilePath
|
--logFile :: FilePath
|
||||||
--logFile = "pgf-error.log"
|
--logFile = "pgf-error.log"
|
||||||
|
|
||||||
debug s = liftIO (logPutStrLn s)
|
debug s = logPutStrLn s
|
||||||
|
|
||||||
-- | Combined FastCGI and HTTP server
|
-- | Combined FastCGI and HTTP server
|
||||||
server port execute1 state0 =
|
server port execute1 state0 =
|
||||||
@@ -91,127 +93,151 @@ handle_fcgi execute1 state0 stateM cache =
|
|||||||
debug $ "done "++show n
|
debug $ "done "++show n
|
||||||
-}
|
-}
|
||||||
|
|
||||||
|
-- * Request handler
|
||||||
|
-- | Handler monad
|
||||||
|
type HM s a = StateT (Q,s) (ErrorT Response IO) a
|
||||||
|
run :: HM s Response -> (Q,s) -> IO (s,Response)
|
||||||
|
run m s = either bad ok =<< runErrorT (runStateT m s)
|
||||||
|
where
|
||||||
|
bad resp = return (snd s,resp)
|
||||||
|
ok (resp,(qs,state)) = return (state,resp)
|
||||||
|
|
||||||
|
get_qs :: HM s Q
|
||||||
|
get_qs = gets fst
|
||||||
|
get_state :: HM s s
|
||||||
|
get_state = gets snd
|
||||||
|
put_qs qs = do state <- get_state; put (qs,state)
|
||||||
|
put_state state = do qs <- get_qs; put (qs,state)
|
||||||
|
|
||||||
|
err :: Response -> HM s a
|
||||||
|
err e = StateT $ \ s -> ErrorT $ return $ Left e
|
||||||
|
|
||||||
|
hmtry :: HM s a -> HM s (Either (Either IOError Response) a)
|
||||||
|
hmtry m = do s <- get
|
||||||
|
e <- liftIO $ try $ runErrorT $ runStateT m s
|
||||||
|
case e of
|
||||||
|
Left ioerror -> return (Left (Left ioerror))
|
||||||
|
Right (Left resp) -> return (Left (Right resp))
|
||||||
|
Right (Right (a,s)) -> do put s;return (Right a)
|
||||||
|
|
||||||
-- | HTTP request handler
|
-- | HTTP request handler
|
||||||
handle state0 cache execute1
|
handle state0 cache execute1
|
||||||
rq@(Request method URI{uriPath=upath,uriQuery=q} hdrs body) state =
|
rq@(Request method URI{uriPath=upath,uriQuery=q} hdrs body) state =
|
||||||
case method of
|
case method of
|
||||||
"POST" -> normal_request (utf8inputs body)
|
"POST" -> run normal_request (utf8inputs body,state)
|
||||||
"GET" -> normal_request (utf8inputs q)
|
"GET" -> run normal_request (utf8inputs q,state)
|
||||||
_ -> return (state,resp501 $ "method "++method)
|
_ -> return (state,resp501 $ "method "++method)
|
||||||
where
|
where
|
||||||
normal_request qs =
|
normal_request =
|
||||||
do logPutStrLn $ method++" "++upath++" "++show (mapSnd (take 100.fst) qs)
|
do qs <- get_qs
|
||||||
|
logPutStrLn $ method++" "++upath++" "++show (mapSnd (take 100.fst) qs)
|
||||||
case upath of
|
case upath of
|
||||||
"/new" -> new
|
"/new" -> new
|
||||||
-- "/stop" ->
|
-- "/stop" ->
|
||||||
-- "/start" ->
|
-- "/start" ->
|
||||||
"/gfshell" -> inDir qs $ look "command" . command
|
"/gfshell" -> inDir command
|
||||||
"/parse" -> parse (decoded qs)
|
"/parse" -> parse (decoded qs)
|
||||||
"/cloud" -> inDir qs $ look "command" . cloud
|
"/cloud" -> inDir cloud
|
||||||
'/':rpath ->
|
'/':rpath ->
|
||||||
case (takeDirectory path,takeFileName path,takeExtension path) of
|
case (takeDirectory path,takeFileName path,takeExtension path) of
|
||||||
(_ ,_ ,".pgf") -> wrapCGI $ PS.cgiMain' cache path
|
(_ ,_ ,".pgf") -> wrapCGI $ PS.cgiMain' cache path
|
||||||
(dir,"grammars.cgi",_ ) -> grammarList dir (decoded qs)
|
(dir,"grammars.cgi",_ ) -> grammarList dir (decoded qs)
|
||||||
(dir ,"exb.fcgi" ,_ ) -> wrapCGI $ ES.cgiMain' root dir cache
|
(dir ,"exb.fcgi" ,_ ) -> wrapCGI $ ES.cgiMain' root dir cache
|
||||||
_ -> do resp <- serveStaticFile path
|
_ -> liftIO $ serveStaticFile path
|
||||||
return (state,resp)
|
|
||||||
where path = translatePath rpath
|
where path = translatePath rpath
|
||||||
_ -> return (state,resp400 upath)
|
_ -> err $ resp400 upath
|
||||||
|
|
||||||
root = "."
|
root = "."
|
||||||
|
|
||||||
translatePath rpath = root</>rpath -- hmm, check for ".."
|
translatePath rpath = root</>rpath -- hmm, check for ".."
|
||||||
|
|
||||||
wrapCGI cgi =
|
wrapCGI cgi =
|
||||||
do resp <- cgiHandler root (handleErrors . handleCGIErrors $ cgi) rq
|
liftIO $ cgiHandler root (handleErrors . handleCGIErrors $ cgi) rq
|
||||||
return (state,resp)
|
|
||||||
|
|
||||||
look field ok qs =
|
look field =
|
||||||
case partition ((==field).fst) qs of
|
do qs <- get_qs
|
||||||
((_,(value,_)):qs1,qs2) -> ok value (qs1++qs2)
|
case partition ((==field).fst) qs of
|
||||||
_ -> bad
|
((_,(value,_)):qs1,qs2) -> do put_qs (qs1++qs2)
|
||||||
where
|
return value
|
||||||
bad = return (state,resp400 $ "no "++field++" in request")
|
_ -> err $ resp400 $ "no "++field++" in request"
|
||||||
|
|
||||||
inDir qs ok = look "dir" cd qs
|
inDir ok = cd =<< look "dir"
|
||||||
where
|
where
|
||||||
cd ('/':dir@('t':'m':'p':_)) qs' =
|
cd ('/':dir@('t':'m':'p':_)) =
|
||||||
do cwd <- getCurrentDirectory
|
do cwd <- liftIO $ getCurrentDirectory
|
||||||
b <- try $ setCurrentDirectory dir
|
b <- liftIO $ try $ setCurrentDirectory dir
|
||||||
case b of
|
case b of
|
||||||
Left _ -> do b <- try $ readFile dir -- poor man's symbolic links
|
Left _ -> do b <- liftIO $ try $ readFile dir -- poor man's symbolic links
|
||||||
case b of
|
case b of
|
||||||
Left _ -> return (state,resp404 dir)
|
Left _ -> err $ resp404 dir
|
||||||
Right dir' -> cd dir' qs'
|
Right dir' -> cd dir'
|
||||||
Right _ -> do logPutStrLn $ "cd "++dir
|
Right _ -> do logPutStrLn $ "cd "++dir
|
||||||
r <- try (ok dir qs')
|
r <- hmtry (ok dir)
|
||||||
setCurrentDirectory cwd
|
liftIO $ setCurrentDirectory cwd
|
||||||
either ioError return r
|
either (either (liftIO . ioError) err) return r
|
||||||
cd dir _ = return (state,resp400 $ "unacceptable directory "++dir)
|
cd dir = err $ resp400 $ "unacceptable directory "++dir
|
||||||
|
|
||||||
new =
|
new = fmap ok200 $ liftIO $ newDirectory
|
||||||
do dir <- newDirectory
|
|
||||||
return (state,ok200 dir)
|
|
||||||
|
|
||||||
command dir cmd _ =
|
command dir =
|
||||||
do let st = maybe state0 id $ M.lookup dir state
|
do cmd <- look "command"
|
||||||
(output,st') <- hCapture [stdout,stderr] (execute1 st cmd)
|
state <- get_state
|
||||||
|
let st = maybe state0 id $ M.lookup dir state
|
||||||
|
(output,st') <- liftIO $ hCapture [stdout,stderr] (execute1 st cmd)
|
||||||
let state' = maybe state (flip (M.insert dir) state) st'
|
let state' = maybe state (flip (M.insert dir) state) st'
|
||||||
return (state',ok200 output)
|
put_state state'
|
||||||
|
return $ ok200 output
|
||||||
|
|
||||||
parse qs =
|
parse qs = return $ json200 (makeObj(map parseModule qs))
|
||||||
return (state,json200 (makeObj(map parseModule qs)))
|
|
||||||
|
|
||||||
cloud dir cmd qs =
|
cloud dir =
|
||||||
case cmd of
|
do cmd <- look "command"
|
||||||
"make" -> make dir (raw qs)
|
case cmd of
|
||||||
"upload" -> upload (raw qs)
|
"make" -> make dir . raw =<< get_qs
|
||||||
"ls" -> jsonList (maybe ".json" fst $ lookup "ext" qs)
|
"upload" -> upload . raw =<< get_qs
|
||||||
"rm" -> with_file qs rm
|
"ls" -> jsonList . maybe ".json" fst . lookup "ext" =<< get_qs
|
||||||
"download" -> with_file qs download
|
"rm" -> rm =<< look_file
|
||||||
"link_directories" -> look "newdir" (link_directories dir) qs
|
"download" -> download =<< look_file
|
||||||
_ -> return (state,resp400 $ "cloud command "++cmd)
|
"link_directories" -> link_directories dir =<< look "newdir"
|
||||||
|
_ -> err $ resp400 $ "cloud command "++cmd
|
||||||
|
|
||||||
with_file qs f = look "file" check qs
|
look_file = check =<< look "file"
|
||||||
where
|
where
|
||||||
check path qs =
|
check path =
|
||||||
if ok_access path
|
if ok_access path
|
||||||
then f path qs
|
then return path
|
||||||
else return (state,resp400 $ "unacceptable path "++path)
|
else err $ resp400 $ "unacceptable path "++path
|
||||||
|
|
||||||
make dir files =
|
make dir files =
|
||||||
do (state,_) <- upload files
|
do _ <- upload files
|
||||||
let args = "-s":"-make":map fst files
|
let args = "-s":"-make":map fst files
|
||||||
cmd = unwords ("gf":args)
|
cmd = unwords ("gf":args)
|
||||||
out <- readProcessWithExitCode "gf" args ""
|
out <- liftIO $ readProcessWithExitCode "gf" args ""
|
||||||
cwd <- getCurrentDirectory
|
cwd <- liftIO $ getCurrentDirectory
|
||||||
return (state,json200 (jsonresult cwd ('/':dir++"/") cmd out files))
|
return $ json200 (jsonresult cwd ('/':dir++"/") cmd out files)
|
||||||
|
|
||||||
upload files =
|
upload files =
|
||||||
if null badpaths
|
if null badpaths
|
||||||
then do mapM_ (uncurry updateFile) okfiles
|
then do liftIO $ mapM_ (uncurry updateFile) okfiles
|
||||||
return (state,resp204)
|
return resp204
|
||||||
else return (state,resp404 $ "unacceptable path(s) "++unwords badpaths)
|
else err $ resp404 $ "unacceptable path(s) "++unwords badpaths
|
||||||
where
|
where
|
||||||
(okfiles,badpaths) = apSnd (map fst) $ partition (ok_access.fst) files
|
(okfiles,badpaths) = apSnd (map fst) $ partition (ok_access.fst) files
|
||||||
|
|
||||||
jsonList ext =
|
jsonList ext = fmap (json200) (ls_ext "." ext)
|
||||||
do jsons <- ls_ext "." ext
|
|
||||||
return (state,json200 jsons)
|
|
||||||
|
|
||||||
ok_to_delete = [".json",".gfstdoc",".gfo",".gf",".pgf"]
|
rm path | takeExtension path `elem` ok_to_delete =
|
||||||
|
do b <- liftIO $ doesFileExist path
|
||||||
rm path _ | takeExtension path `elem` ok_to_delete =
|
|
||||||
do b <- doesFileExist path
|
|
||||||
if b
|
if b
|
||||||
then do removeFile path
|
then do liftIO $ removeFile path
|
||||||
return (state,ok200 "")
|
return $ ok200 ""
|
||||||
else return (state,resp404 path)
|
else err $ resp404 path
|
||||||
rm path _ = return (state,resp400 $ "unacceptable extension "++path)
|
rm path = err $ resp400 $ "unacceptable extension "++path
|
||||||
|
|
||||||
download path _ = (,) state `fmap` serveStaticFile path
|
download path = liftIO $ serveStaticFile path
|
||||||
|
|
||||||
link_directories olddir newdir@('/':'t':'m':'p':'/':_) _ | old/=new =
|
link_directories olddir newdir@('/':'t':'m':'p':'/':_) | old/=new =
|
||||||
|
liftIO $
|
||||||
do setCurrentDirectory ".."
|
do setCurrentDirectory ".."
|
||||||
logPutStrLn =<< getCurrentDirectory
|
logPutStrLn =<< getCurrentDirectory
|
||||||
logPutStrLn $ "link_dirs new="++new++", old="++old
|
logPutStrLn $ "link_dirs new="++new++", old="++old
|
||||||
@@ -225,29 +251,29 @@ handle state0 cache execute1
|
|||||||
if isLink then removeLink old else removeDir old
|
if isLink then removeLink old else removeDir old
|
||||||
createSymbolicLink new old
|
createSymbolicLink new old
|
||||||
#endif
|
#endif
|
||||||
return (state,ok200 "")
|
return $ ok200 ""
|
||||||
where
|
where
|
||||||
old = takeFileName olddir
|
old = takeFileName olddir
|
||||||
new = takeFileName newdir
|
new = takeFileName newdir
|
||||||
link_directories olddir newdir _ =
|
link_directories olddir newdir =
|
||||||
return (state,resp400 $ "unacceptable directories "++olddir++" "++newdir)
|
err $ resp400 $ "unacceptable directories "++olddir++" "++newdir
|
||||||
|
|
||||||
grammarList dir qs =
|
grammarList dir qs =
|
||||||
do pgfs <- ls_ext dir ".pgf"
|
do pgfs <- ls_ext dir ".pgf"
|
||||||
return (state,jsonp qs pgfs)
|
return $ jsonp qs pgfs
|
||||||
|
|
||||||
ls_ext dir ext =
|
ls_ext dir ext =
|
||||||
do paths <- getDirectoryContents dir
|
do paths <- liftIO $ getDirectoryContents dir
|
||||||
return [path | path<-paths, takeExtension path==ext]
|
return [path | path<-paths, takeExtension path==ext]
|
||||||
|
|
||||||
-- * Dynamic content
|
-- * Dynamic content
|
||||||
|
|
||||||
jsonresult cwd dir cmd (ecode,stdout,stderr) files =
|
jsonresult cwd dir cmd (ecode,stdout,stderr) files =
|
||||||
makeObj [
|
makeObj [
|
||||||
prop "errorcode" (if ecode==ExitSuccess then "OK" else "Error"),
|
"errorcode" .= if ecode==ExitSuccess then "OK" else "Error",
|
||||||
prop "command" cmd,
|
"command" .= cmd,
|
||||||
prop "output" (unlines [rel stderr,rel stdout]),
|
"output" .= unlines [rel stderr,rel stdout],
|
||||||
prop "minibar_url" ("/minibar/minibar.html?"++dir++pgf)]
|
"minibar_url" .= "/minibar/minibar.html?"++dir++pgf]
|
||||||
where
|
where
|
||||||
pgf = case files of
|
pgf = case files of
|
||||||
(abstract,_):_ -> "%20"++dropExtension abstract++".pgf"
|
(abstract,_):_ -> "%20"++dropExtension abstract++".pgf"
|
||||||
@@ -324,7 +350,7 @@ serveStaticFile' path =
|
|||||||
else return (resp404 path)
|
else return (resp404 path)
|
||||||
|
|
||||||
-- * Logging
|
-- * Logging
|
||||||
logPutStrLn = hPutStrLn stderr
|
logPutStrLn s = liftIO . hPutStrLn stderr $ s
|
||||||
|
|
||||||
-- * JSONP output
|
-- * JSONP output
|
||||||
|
|
||||||
@@ -341,8 +367,13 @@ html200 = ok200' htmlUTF8 . encodeString
|
|||||||
resp204 = Response 204 [] "" -- no content
|
resp204 = Response 204 [] "" -- no content
|
||||||
resp400 msg = Response 400 [plain] $ "Bad request: "++msg++"\n"
|
resp400 msg = Response 400 [plain] $ "Bad request: "++msg++"\n"
|
||||||
resp404 path = Response 404 [plain] $ "Not found: "++path++"\n"
|
resp404 path = Response 404 [plain] $ "Not found: "++path++"\n"
|
||||||
|
resp500 msg = Response 500 [plain] $ "Internal error: "++msg++"\n"
|
||||||
resp501 msg = Response 501 [plain] $ "Not implemented: "++msg++"\n"
|
resp501 msg = Response 501 [plain] $ "Not implemented: "++msg++"\n"
|
||||||
|
|
||||||
|
instance Error Response where
|
||||||
|
noMsg = resp500 "no message"
|
||||||
|
strMsg = resp500
|
||||||
|
|
||||||
-- * Content types
|
-- * Content types
|
||||||
plain = ct "text/plain"
|
plain = ct "text/plain"
|
||||||
plainUTF8 = ct "text/plain; charset=UTF-8"
|
plainUTF8 = ct "text/plain; charset=UTF-8"
|
||||||
@@ -381,6 +412,9 @@ ok_access path =
|
|||||||
'.':'.':'/':_ -> False
|
'.':'.':'/':_ -> False
|
||||||
_ -> not ("/../" `isInfixOf` path)
|
_ -> not ("/../" `isInfixOf` path)
|
||||||
|
|
||||||
|
-- | Only delete files with these extensions
|
||||||
|
ok_to_delete = [".json",".gfstdoc",".gfo",".gf",".pgf"]
|
||||||
|
|
||||||
newDirectory =
|
newDirectory =
|
||||||
do debug "newDirectory"
|
do debug "newDirectory"
|
||||||
loop 10
|
loop 10
|
||||||
@@ -416,6 +450,8 @@ toHeader s = FCGI.HttpExtensionHeader s -- cheating a bit
|
|||||||
-- * misc utils
|
-- * misc utils
|
||||||
|
|
||||||
--utf8inputs = mapBoth decodeString . inputs
|
--utf8inputs = mapBoth decodeString . inputs
|
||||||
|
type Q = [(String,(String,String))]
|
||||||
|
utf8inputs :: String -> Q
|
||||||
utf8inputs q = [(decodeString k,(decodeString v,v))|(k,v)<-inputs q]
|
utf8inputs q = [(decodeString k,(decodeString v,v))|(k,v)<-inputs q]
|
||||||
decoded = mapSnd fst
|
decoded = mapSnd fst
|
||||||
raw = mapSnd snd
|
raw = mapSnd snd
|
||||||
@@ -432,4 +468,5 @@ mapBoth = map . apBoth
|
|||||||
apBoth f (x,y) = (f x,f y)
|
apBoth f (x,y) = (f x,f y)
|
||||||
apSnd f (x,y) = (x,f y)
|
apSnd f (x,y) = (x,f y)
|
||||||
|
|
||||||
prop n v = (n,showJSON v)
|
infix 1 .=
|
||||||
|
n .= v = (n,showJSON v)
|
||||||
|
|||||||
Reference in New Issue
Block a user