Files
gf-core/src/compiler/GFServer.hs
hallgren 3488632558 Added a preliminary "gf -server" mode.
The command "gf -server" now starts a simple HTTP server on port 41295,
providing a simple web API to the GF compiler. It currently support the
follwing operations:

  * creating new temporary directories for grammar uploads,
  * uploading grammars files for use in the GF shell,
  * executing GF shell commands, and
  * accessing static files.

This means that GF now depends on some additional networking related packages,
but they should be available and easy to install on all platforms. There is
also a new configuration flag "server" in gf.cabal, so GF will be compiled
without support for server mode if the extra packages are unavailable.

Note that running gf -server while connected to the internet can be a security
risk. To prevent unauthorized access to the rest of the system, it is
advisable to run the server in GF_RESTRICTED mode and as a user with suitably
restricted file permissions.
2011-04-13 14:58:01 +00:00

149 lines
4.7 KiB
Haskell

module GFServer(server) where
import Data.List(partition)
import qualified Data.Map as M
import Control.Monad(when)
import System.Random(randomRIO)
import System.IO(stdout,stderr)
import System.IO.Error(try,ioError)
import System.Directory(doesDirectoryExist,doesFileExist,createDirectory,
setCurrentDirectory,getCurrentDirectory)
import System.FilePath(takeExtension,(</>))
import Control.Concurrent.MVar(newMVar,modifyMVar)
import Network.URI(URI(..))
import Network.Shed.Httpd(initServer,Request(..),Response(..),queryToArguments,
noCache)
import System.IO.Silently(hCapture)
import Codec.Binary.UTF8.String(encodeString)
import GF.Infra.UseIO(readBinaryFile)
-- * Configuraiton
port = 41295
documentRoot = "."
-- * HTTP server
server execute1 state0 =
do state <- newMVar M.empty
putStrLn $ "Starting server on port "++show port
initServer port (modifyMVar state . handle state0 execute1)
-- * HTTP request handler
handle state0 execute1 (Request method URI{uriPath=path,uriQuery=q} hdrs body) state =
do let qs = decodeQ $
case method of
"GET" -> queryToArguments q
"POST" -> queryToArguments body
logPutStrLn $ method++" "++path++" "++show qs
case path of
"/new" -> new
-- "/stop" ->
-- "/start" ->
"/gfshell" -> inDir qs $ look "command" . command
"/upload" -> inDir qs upload
'/':rpath -> do resp <- serveStaticFile (translatePath rpath)
return (state,resp)
_ -> return (state,resp400 path)
where
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")
inDir qs ok = look "dir" cd qs
where
cd ('/':dir@('t':'m':'p':_)) qs' =
do cwd <- getCurrentDirectory
b <- try $ setCurrentDirectory dir
case b of
Left _ -> return (state,resp404 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)
new =
do dir <- newDirectory
return (state,ok200 dir)
command dir cmd _ =
do let st = maybe state0 id $ M.lookup dir state
(output,st') <- hCapture [stdout,stderr] (execute1 st cmd)
let state' = maybe state (flip (M.insert dir) state) st'
return (state',ok200 output)
upload dir files=
do let update (name,contents)= updateFile (name++".gf") contents
mapM_ update files
return (state,resp204)
-- * Static content
translatePath path = documentRoot</>path -- hmm, check for ".."
serveStaticFile path =
do b <- doesDirectoryExist path
let path' = if b then path </> "index.html" else path
serveStaticFile' path'
serveStaticFile' path =
do b <- doesFileExist path
let (t,rdFile,encode) = contentTypeFromExt (takeExtension path)
if b then fmap (ok200' (ct t) . encode) $ rdFile path
else return (resp404 path)
-- * Logging
logPutStrLn = putStrLn
-- * Standard HTTP responses
ok200 body = Response 200 [plainUTF8,noCache] (encodeString body)
ok200' t body = Response 200 [t] body
resp204 = Response 204 [] "" -- no content
resp400 msg = Response 400 [plain] $ "Bad request: "++msg++"\n"
resp404 path = Response 404 [plain] $ "Not found: "++path++"\n"
-- * Content types
plain = ct "text/plain"
plainUTF8 = ct "text/plain; charset=UTF-8"
ct t = ("Content-Type",t)
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",readFile,encodeString)
bin t = (t,readBinaryFile,id)
-- * IO utilities
updateFile path new =
do old <- try $ readFile path
when (Right new/=old) $ do logPutStrLn $ "Updating "++path
seq (either (const 0) length old) $
writeFile path new
newDirectory =
do k <- randomRIO (1,maxBound::Int)
let path = "tmp/gfse."++show k
b <- try $ createDirectory path
case b of
Left _ -> newDirectory
Right _ -> return ('/':path)
-- * misc utils
decodeQ qs = [(decode n,decode v)|(n,v)<-qs]
decode = map decode1
decode1 '+' = ' ' -- httpd-shed bug workaround
decode1 c = c