diff options
| author | hallgren <hallgren@chalmers.se> | 2014-10-15 21:04:29 +0000 |
|---|---|---|
| committer | hallgren <hallgren@chalmers.se> | 2014-10-15 21:04:29 +0000 |
| commit | b70dba87bab5dfc8039f0b9f69e0851f92324f8b (patch) | |
| tree | 891cda8fd263b768232f930cabaf0769fb976737 /src/compiler/GF/Server.hs | |
| parent | 393dde2eb93a975442697c177dbb161e4300bea0 (diff) | |
Rename modules GFI, GFC & GFServer...
... to GF.Interactive, GF.Compiler & GF.Server, respectively.
Diffstat (limited to 'src/compiler/GF/Server.hs')
| -rw-r--r-- | src/compiler/GF/Server.hs | 494 |
1 files changed, 494 insertions, 0 deletions
diff --git a/src/compiler/GF/Server.hs b/src/compiler/GF/Server.hs new file mode 100644 index 000000000..0fc7f0388 --- /dev/null +++ b/src/compiler/GF/Server.hs @@ -0,0 +1,494 @@ +-- | GF server mode +{-# LANGUAGE CPP #-} +module GF.Server(server) where +import Data.List(partition,stripPrefix,isInfixOf) +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(stderr,hPutStrLn) +import GF.System.Catch(try) +import Control.Exception(bracket_) +import System.IO.Error(isAlreadyExistsError) +import GF.System.Directory(doesDirectoryExist,doesFileExist,createDirectory, + setCurrentDirectory,getCurrentDirectory, + getDirectoryContents,removeFile,removeDirectory, + getModificationTime) +import Data.Time (getCurrentTime,formatTime) +import System.Locale(defaultTimeLocale,rfc822DateFormat) +import System.FilePath(dropExtension,takeExtension,takeFileName,takeDirectory, + (</>),makeRelative) +#ifndef mingw32_HOST_OS +import System.Posix.Files(getSymbolicLinkStatus,isSymbolicLink,removeLink, + createSymbolicLink) +#endif +import GF.Infra.Concurrency(newMVar,modifyMVar,newLog) +import Network.URI(URI(..)) +import Network.Shed.Httpd(initServer,Request(..),Response(..),noCache) +--import qualified Network.FastCGI as FCGI -- from hackage direct-fastcgi +import Network.CGI(handleErrors,liftIO) +import CGIUtils(handleCGIErrors)--,outputJSONP,stderrToFile +import Text.JSON(encode,showJSON,makeObj) +--import System.IO.Silently(hCapture) +import System.Process(readProcessWithExitCode) +import System.Exit(ExitCode(..)) +import Codec.Binary.UTF8.String(decodeString,encodeString) +import GF.Infra.UseIO(readBinaryFile,writeBinaryFile,ePutStrLn) +import GF.Infra.SIO(captureSIO) +import qualified PGFService as PS +import qualified ExampleService as ES +import Data.Version(showVersion) +import Paths_gf(getDataDir,version) +import GF.Infra.BuildInfo (buildInfo) +import SimpleEditor.Convert(parseModule) +import RunHTTP(cgiHandler) +import URLEncoding(decodeQuery) + +--logFile :: FilePath +--logFile = "pgf-error.log" + +debug s = logPutStrLn s + +-- | Combined FastCGI and HTTP server +server port optroot execute1 state0 = + do --stderrToFile logFile + state <- newMVar M.empty + cache <- PS.newPGFCache + datadir <- getDataDir + let root = maybe (datadir</>"www") id optroot +-- debug $ "document root="++root + setDir root +-- FCGI.acceptLoop forkIO (handle_fcgi execute1 state0 state cache) + -- if acceptLoop returns, then GF was not invoked as a FastCGI script + http_server execute1 state0 state cache root + where + -- | HTTP server + http_server execute1 state0 state cache root = + do logLn <- newLog ePutStrLn -- to avoid intertwined log messages + logLn gf_version + logLn $ "Document root = "++root + logLn $ "Starting HTTP server, open http://localhost:" + ++show port++"/ in your web browser." + initServer port (handle logLn root state0 cache execute1 state) + +gf_version = "This is GF version "++showVersion version++".\n"++buildInfo + +{- +-- | FastCGI request handler +handle_fcgi execute1 state0 stateM cache = + do Just method <- FCGI.getRequestMethod + debug $ "request method="++method + Just path <- FCGI.getPathInfo +-- debug $ "path info="++path + query <- maybe (return "") return =<< FCGI.getQueryString +-- debug $ "query string="++query + let uri = URI "" Nothing path query "" + headers <- fmap (mapFst show) FCGI.getAllRequestHeaders + body <- fmap BS.unpack FCGI.fGetContents + let req = Request method uri headers body +-- debug (show req) + (output,resp) <- liftIO $ hCapture [stdout] $ modifyMVar stateM $ handle state0 cache execute1 req + let Response code headers body = resp +-- debug output + debug $ " "++show code++" "++show headers + FCGI.setResponseStatus code + mapM_ (uncurry (FCGI.setResponseHeader . toHeader)) headers + let pbody = BS.pack body + n = BS.length pbody + FCGI.fPut pbody + 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 + +hmbracket_ :: IO () -> IO () -> HM s a -> HM s a +hmbracket_ pre post m = + do s <- get + e <- liftIO $ bracket_ pre post $ runErrorT $ runStateT m s + case e of + Left resp -> err resp + Right (a,s) -> do put s;return a + +-- | HTTP request handler +handle logLn documentroot state0 cache execute1 stateVar + rq@(Request method URI{uriPath=upath,uriQuery=q} hdrs body) = + addDate $ + case method of + "POST" -> normal_request (utf8inputs body) + "GET" -> normal_request (utf8inputs q) + _ -> return (resp501 $ "method "++method) + where + logPutStrLn msg = liftIO $ logLn msg + debug msg = logPutStrLn msg + + addDate m = + do t <- getCurrentTime + r <- m + let fmt = formatTime defaultTimeLocale rfc822DateFormat t + return r{resHeaders=("Date",fmt):resHeaders r} + + normal_request qs = + do logPutStrLn $ method++" "++upath++" "++show (mapSnd (take 100.fst) qs) + let stateful m = modifyMVar stateVar $ \ s -> run m (qs,s) + -- stateful ensures mutual exclusion, so you can use/change the cwd + case upath of + "/new" -> stateful $ new + "/gfshell" -> stateful $ inDir command + "/cloud" -> stateful $ inDir cloud +-- "/stop" -> +-- "/start" -> + "/parse" -> parse (decoded qs) + "/version" -> do (c1,c2) <- PS.listPGFCache cache + let rel = map (makeRelative documentroot) + return $ ok200 (unlines (gf_version:"":rel c1++"":rel c2)) + "/flush" -> do PS.flushPGFCache cache; return (ok200 "flushed") + '/':rpath -> + -- This code runs without mutual exclusion, so it must *not* + -- use/change the cwd. Access files by absolute paths only. + case (takeDirectory path,takeFileName path,takeExtension path) of + (_ ,_ ,".pgf") -> do --debug $ "PGF service: "++path + wrapCGI $ PS.cgiMain' cache path + (dir,"grammars.cgi",_ ) -> grammarList dir (decoded qs) + (dir ,"exb.fcgi" ,_ ) -> wrapCGI $ ES.cgiMain' root dir (fst cache) + _ -> serveStaticFile rpath path + where path = translatePath rpath + _ -> return $ resp400 upath + + root = documentroot + + translatePath rpath = root</>rpath -- hmm, check for ".." + + wrapCGI cgi = cgiHandler root (handleErrors . handleCGIErrors $ cgi) rq + + 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 ok = cd =<< look "dir" + where + cd ('/':dir@('t':'m':'p':_)) = + do cwd <- getCurrentDirectory + b <- doesDirectoryExist dir + case b of + False -> do b <- liftIO $ try $ readFile dir -- poor man's symbolic links + case b of + Left _ -> err $ resp404 dir + Right dir' -> cd dir' + True -> do --logPutStrLn $ "cd "++dir + hmInDir dir (ok dir) + cd dir = err $ resp400 $ "unacceptable directory "++dir + + -- First ensure that only one thread that depends on the cwd is running! + hmInDir dir = hmbracket_ (setDir dir) (setDir documentroot) + + new = fmap ok200 $ liftIO $ newDirectory + + command dir = + do cmd <- look "command" + state <- get_state + let st = maybe state0 id $ M.lookup dir state + (output,st') <- liftIO $ captureSIO $ execute1 st cmd + let state' = maybe state (flip (M.insert dir) state) st' + put_state state' + return $ ok200 output + + parse qs = return $ json200 (makeObj(map parseModule qs)) + + cloud dir = + do cmd <- look "command" + case cmd of + "make" -> make id dir . raw =<< get_qs + "remake" -> make skip_empty dir . raw =<< get_qs + "upload" -> upload id . raw =<< get_qs + "ls" -> jsonList . maybe ".json" fst . lookup "ext" =<< get_qs + "ls-l" -> jsonListLong . 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 + + look_file = check =<< look "file" + where + check path = + if ok_access path + then return path + else err $ resp400 $ "unacceptable path "++path + + make skip dir args = + do let (flags,files) = partition ((=="-").take 1.fst) args + _ <- upload skip files + let args = "-s":"-make":map flag flags++map fst files + flag (n,"") = n + flag (n,v) = n++"="++v + cmd = unwords ("gf":args) + logPutStrLn cmd + out@(ecode,_,_) <- liftIO $ readProcessWithExitCode "gf" args "" + logPutStrLn $ show ecode + cwd <- getCurrentDirectory + return $ json200 (jsonresult cwd ('/':dir++"/") cmd out files) + + upload skip files = + if null badpaths + then do liftIO $ mapM_ (uncurry updateFile) (skip okfiles) + return resp204 + else err $ resp404 $ "unacceptable path(s) "++unwords badpaths + where + (okfiles,badpaths) = apSnd (map fst) $ partition (ok_access.fst) files + + skip_empty = filter (not.null.snd) + + jsonList = jsonList' return + jsonListLong = jsonList' (mapM addTime) + jsonList' details ext = fmap (json200) (details =<< ls_ext "." ext) + + addTime path = + do t <- getModificationTime path + return $ makeObj ["path".=path,"time".=format t] + where + format = formatTime defaultTimeLocale rfc822DateFormat + + rm path | takeExtension path `elem` ok_to_delete = + do b <- doesFileExist path + if b + then do removeFile path + return $ ok200 "" + else err $ resp404 path + rm path = err $ resp400 $ "unacceptable extension "++path + + download path = liftIO $ serveStaticFile' path + + link_directories olddir newdir@('/':'t':'m':'p':'/':_) | old/=new = + hmInDir ".." $ liftIO $ + do logPutStrLn =<< getCurrentDirectory + logPutStrLn $ "link_dirs new="++new++", old="++old +#ifdef mingw32_HOST_OS + isDir <- doesDirectoryExist old + if isDir then removeDir old else removeFile old + writeFile old new -- poor man's symbolic links +#else + isLink <- isSymbolicLink `fmap` getSymbolicLinkStatus old + logPutStrLn $ "old is link: "++show isLink + if isLink then removeLink old else removeDir old + createSymbolicLink new old +#endif + return $ ok200 "" + where + old = takeFileName olddir + new = takeFileName newdir + link_directories olddir newdir = + err $ resp400 $ "unacceptable directories "++olddir++" "++newdir + + grammarList dir qs = + do pgfs <- ls_ext dir ".pgf" + return $ jsonp qs pgfs + + ls_ext dir ext = + do paths <- getDirectoryContents dir + return [path | path<-paths, takeExtension path==ext] + +-- * Dynamic content + +jsonresult cwd dir cmd (ecode,stdout,stderr) files = + makeObj [ + "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" + _ -> "" + + rel = unlines . map relative . lines + + -- remove absolute file paths from error messages: + relative s = case stripPrefix cwd s of + Just ('/':rest) -> rest + _ -> s + +-- * Static content + +serveStaticFile rpath path = + do --logPutStrLn $ "Serving static file "++path + b <- doesDirectoryExist path + if b + then if rpath `elem` ["","."] || last path=='/' + then serveStaticFile' (path </> "index.html") + else return (resp301 ('/':rpath++"/")) + else serveStaticFile' path + +serveStaticFile' path = + do let ext = takeExtension path + (t,rdFile) = contentTypeFromExt ext + if ext `elem` [".cgi",".fcgi",".sh",".php"] + then return $ resp400 $ "Unsupported file type: "++ext + else do b <- doesFileExist path + if b then fmap (ok200' (ct t "")) $ rdFile path + else do cwd <- getCurrentDirectory + logPutStrLn $ "Not found: "++path++" cwd="++cwd + return (resp404 path) + +-- * Logging +logPutStrLn s = ePutStrLn s + +-- * JSONP output + +jsonp qs = maybe json200 apply (lookup "jsonp" qs) + where + apply f = jsonp200' $ \ json -> f++"("++json++")" + +-- * Standard HTTP responses +ok200 = Response 200 [plainUTF8,noCache,xo] . encodeString +ok200' t = Response 200 [t,xo] +json200 x = json200' id x +json200' f = ok200' jsonUTF8 . encodeString . f . encode +jsonp200' f = ok200' jsonpUTF8 . encodeString . f . encode +html200 = ok200' htmlUTF8 . encodeString +resp204 = Response 204 [xo] "" -- no content +resp301 url = Response 301 [plain,xo,location url] $ + "Moved permanently to "++url +resp400 msg = Response 400 [plain,xo] $ "Bad request: "++msg++"\n" +resp404 path = Response 404 [plain,xo] $ "Not found: "++path++"\n" +resp500 msg = Response 500 [plain,xo] $ "Internal error: "++msg++"\n" +resp501 msg = Response 501 [plain,xo] $ "Not implemented: "++msg++"\n" + +instance Error Response where + noMsg = resp500 "no message" + strMsg = resp500 + +-- * Content types +plain = ct "text/plain" "" +plainUTF8 = ct "text/plain" csutf8 +jsonUTF8 = ct "application/json" csutf8 -- http://www.ietf.org/rfc/rfc4627.txt +jsonpUTF8 = ct "application/javascript" csutf8 +htmlUTF8 = ct "text/html" csutf8 + +ct t cs = ("Content-Type",t++cs) +csutf8 = "; charset=UTF-8" +xo = ("Access-Control-Allow-Origin","*") -- Allow cross origin requests + -- https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS +location url = ("Location",url) + +contentTypeFromExt ext = + case ext of + ".html" -> text "html" + ".htm" -> text "html" + ".xml" -> text "xml" + ".txt" -> text "plain" + ".css" -> text "css" + ".js" -> text "javascript" + ".png" -> bin "image/png" + ".jpg" -> bin "image/jpg" + _ -> bin "application/octet-stream" + where + text subtype = ("text/"++subtype++"; charset=UTF-8", + fmap encodeString . readFile) + bin t = (t,readBinaryFile) + +-- * IO utilities +updateFile path new = + do old <- try $ readBinaryFile path +-- let new = encodeString new0 + when (Right new/=old) $ do logPutStrLn $ "Updating "++path + seq (either (const 0) length old) $ + writeBinaryFile path new + +-- | Check that a path is not outside the current directory +ok_access path = + case path of + '/':_ -> False + '.':'.':'/':_ -> False + _ -> not ("/../" `isInfixOf` path) + +-- | Only delete files with these extensions +ok_to_delete = [".json",".gfstdoc",".gfo",".gf",".pgf"] + +newDirectory = + do debug "newDirectory" + loop 10 + where + loop 0 = fail "Failed to create a new directory" + loop n = maybe (loop (n-1)) return =<< once + + once = + do k <- randomRIO (1,maxBound::Int) + let path = "tmp/gfse."++show k + b <- try $ createDirectory path + case b of + Left err -> do debug (show err) ; + if isAlreadyExistsError err + then return Nothing + else ioError err + Right _ -> return (Just ('/':path)) + +-- | Remove a directory and the files in it, but not recursively +removeDir dir = + do files <- filter (`notElem` [".",".."]) `fmap` getDirectoryContents dir + mapM (removeFile . (dir</>)) files + removeDirectory dir + +setDir path = + do --logPutStrLn $ "cd "++show path + setCurrentDirectory path + +{- +-- * direct-fastcgi deficiency workaround + +--toHeader = FCGI.toHeader -- not exported, unfortuntately + +toHeader "Content-Type" = FCGI.HttpContentType -- to avoid duplicate headers +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 + +inputs ('?':q) = decodeQuery q +inputs q = decodeQuery q + +{- +-- Stay clear of queryToArgument, which uses unEscapeString, which had +-- backward incompatible changes in network-2.4.1.1, see +-- https://github.com/haskell/network/commit/f2168b1f8978b4ad9c504e545755f0795ac869ce +inputs = queryToArguments . fixplus + where + fixplus = concatMap decode + decode '+' = "%20" -- httpd-shed bug workaround + decode c = [c] +-} + +mapFst f xys = [(f x,y)|(x,y)<-xys] +mapSnd f xys = [(x,f y)|(x,y)<-xys] +mapBoth = map . apBoth +apBoth f (x,y) = (f x,f y) +apSnd f (x,y) = (x,f y) + +infix 1 .= +n .= v = (n,showJSON v) |
