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