227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			6.7 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
| {-# LANGUAGE CPP #-}
 | |
| {-# LANGUAGE OverloadedStrings #-}
 | |
| 
 | |
| module Hledger.Web.WebOptions where
 | |
| 
 | |
| import Data.ByteString (ByteString)
 | |
| import qualified Data.ByteString.Char8 as BC
 | |
| import Data.ByteString.UTF8 (fromString)
 | |
| import Data.CaseInsensitive (CI, mk)
 | |
| import Control.Monad (join)
 | |
| import Data.Default (Default(def))
 | |
| import Data.Maybe (fromMaybe)
 | |
| import qualified Data.Text as T
 | |
| import Data.Text (Text)
 | |
| import System.Environment (getArgs)
 | |
| import Network.Wai as WAI
 | |
| import Network.Wai.Middleware.Cors
 | |
| 
 | |
| import Hledger.Cli hiding (packageversion, progname, prognameandversion)
 | |
| import Hledger.Web.Settings (defhost, defport, defbaseurl)
 | |
| 
 | |
| -- cf Hledger.Cli.Version
 | |
| 
 | |
| packageversion :: String
 | |
| #ifdef VERSION
 | |
| packageversion = VERSION
 | |
| #else
 | |
| packageversion = ""
 | |
| #endif
 | |
| 
 | |
| progname :: String
 | |
| progname = "hledger-web"
 | |
| 
 | |
| prognameandversion :: String
 | |
| prognameandversion = versionStringFor  progname
 | |
| 
 | |
| 
 | |
| webflags :: [Flag RawOpts]
 | |
| webflags =
 | |
|   [ flagNone
 | |
|       ["serve", "server"]
 | |
|       (setboolopt "serve")
 | |
|       "serve and log requests, don't browse or auto-exit"
 | |
|   , flagNone
 | |
|       ["serve-api"]
 | |
|       (setboolopt "serve-api")
 | |
|       "like --serve, but serve only the JSON web API, without the server-side web UI"
 | |
|   , flagReq
 | |
|       ["cors"]
 | |
|       (\s opts -> Right $ setopt "cors" s opts)
 | |
|       "ORIGIN"
 | |
|       ("allow cross-origin requests from the specified origin; setting ORIGIN to \"*\" allows requests from any origin")
 | |
|   , flagReq
 | |
|       ["socket"]
 | |
|       (\s opts -> Right $ setopt "socket" s opts)
 | |
|       "SOCKET"
 | |
|       "use the given socket instead of the given IP and port (implies --serve)"
 | |
|   , flagReq
 | |
|       ["host"]
 | |
|       (\s opts -> Right $ setopt "host" s opts)
 | |
|       "IPADDR"
 | |
|       ("listen on this IP address (default: " ++ defhost ++ ")")
 | |
|   , flagReq
 | |
|       ["port"]
 | |
|       (\s opts -> Right $ setopt "port" s opts)
 | |
|       "PORT"
 | |
|       ("listen on this TCP port (default: " ++ show defport ++ ")")
 | |
|   , flagReq
 | |
|       ["base-url"]
 | |
|       (\s opts -> Right $ setopt "base-url" s opts)
 | |
|       "BASEURL"
 | |
|       "set the base url (default: http://IPADDR:PORT)"
 | |
|   , flagReq
 | |
|       ["file-url"]
 | |
|       (\s opts -> Right $ setopt "file-url" s opts)
 | |
|       "FILEURL"
 | |
|       "set the static files url (default: BASEURL/static)"
 | |
|   , flagReq
 | |
|       ["capabilities"]
 | |
|       (\s opts -> Right $ setopt "capabilities" s opts)
 | |
|       "CAP[,CAP..]"
 | |
|       "enable the view, add, and/or manage capabilities (default: view,add)"
 | |
|   , flagReq
 | |
|       ["capabilities-header"]
 | |
|       (\s opts -> Right $ setopt "capabilities-header" s opts)
 | |
|       "HTTPHEADER"
 | |
|       "read capabilities to enable from a HTTP header, like X-Sandstorm-Permissions (default: disabled)"
 | |
|   , flagNone
 | |
|       ["test"]
 | |
|       (setboolopt "test")
 | |
|       "run hledger-web's tests and exit. hspec test runner args may follow a --, eg: hledger-web --test -- --help"
 | |
|   ]
 | |
| 
 | |
| webmode :: Mode RawOpts
 | |
| webmode =
 | |
|   (mode
 | |
|      "hledger-web"
 | |
|      (setopt "command" "web" def)
 | |
|      "start serving the hledger web interface"
 | |
|      (argsFlag "[PATTERNS]")
 | |
|      [])
 | |
|   { modeGroupFlags =
 | |
|       Group
 | |
|       { groupUnnamed = webflags
 | |
|       , groupHidden =
 | |
|           hiddenflags 
 | |
|           -- ++
 | |
|           -- [ flagNone
 | |
|           --     ["binary-filename"]
 | |
|           --     (setboolopt "binary-filename")
 | |
|           --     "show the download filename for this executable, and exit"
 | |
|           -- ]
 | |
|       , groupNamed = [generalflagsgroup1]
 | |
|       }
 | |
|   , modeHelpSuffix = []
 | |
|   }
 | |
| 
 | |
| -- hledger-web options, used in hledger-web and above
 | |
| data WebOpts = WebOpts
 | |
|   { serve_ :: Bool
 | |
|   , serve_api_ :: Bool
 | |
|   , cors_ :: Maybe String
 | |
|   , host_ :: String
 | |
|   , port_ :: Int
 | |
|   , base_url_ :: String
 | |
|   , file_url_ :: Maybe String
 | |
|   , capabilities_ :: [Capability]
 | |
|   , capabilitiesHeader_ :: Maybe (CI ByteString)
 | |
|   , cliopts_ :: CliOpts
 | |
|   , socket_ :: Maybe String
 | |
|   } deriving (Show)
 | |
| 
 | |
| defwebopts :: WebOpts
 | |
| defwebopts = WebOpts
 | |
|   { serve_              = False
 | |
|   , serve_api_          = False
 | |
|   , cors_               = Nothing
 | |
|   , host_               = ""
 | |
|   , port_               = def
 | |
|   , base_url_           = ""
 | |
|   , file_url_           = Nothing
 | |
|   , capabilities_       = [CapView, CapAdd]
 | |
|   , capabilitiesHeader_ = Nothing
 | |
|   , cliopts_            = def
 | |
|   , socket_             = Nothing
 | |
|   }
 | |
| 
 | |
| instance Default WebOpts where def = defwebopts
 | |
| 
 | |
| rawOptsToWebOpts :: RawOpts -> IO WebOpts
 | |
| rawOptsToWebOpts rawopts =
 | |
|   checkWebOpts <$> do
 | |
|     cliopts <- rawOptsToCliOpts rawopts
 | |
|     let h = fromMaybe defhost $ maybestringopt "host" rawopts
 | |
|         p = fromMaybe defport $ maybeposintopt "port" rawopts
 | |
|         b =
 | |
|           maybe (defbaseurl h p) stripTrailingSlash $
 | |
|           maybestringopt "base-url" rawopts
 | |
|         caps' = join $ T.splitOn "," . T.pack <$> listofstringopt "capabilities" rawopts
 | |
|         caps = case traverse capabilityFromText caps' of
 | |
|           Left e -> error' ("Unknown capability: " ++ T.unpack e)  -- PARTIAL:
 | |
|           Right [] -> [CapView, CapAdd]
 | |
|           Right xs -> xs
 | |
|         sock = stripTrailingSlash <$> maybestringopt "socket" rawopts
 | |
|     return
 | |
|       defwebopts
 | |
|       { serve_ = case sock of
 | |
|           Just _ -> True
 | |
|           Nothing -> boolopt "serve" rawopts
 | |
|       , serve_api_ = boolopt "serve-api" rawopts
 | |
|       , cors_ = maybestringopt "cors" rawopts
 | |
|       , host_ = h
 | |
|       , port_ = p
 | |
|       , base_url_ = b
 | |
|       , file_url_ = stripTrailingSlash <$> maybestringopt "file-url" rawopts
 | |
|       , capabilities_ = caps
 | |
|       , capabilitiesHeader_ = mk . BC.pack <$> maybestringopt "capabilities-header" rawopts
 | |
|       , cliopts_ = cliopts
 | |
|       , socket_ = sock
 | |
|       }
 | |
|   where
 | |
|     stripTrailingSlash = reverse . dropWhile (== '/') . reverse -- yesod don't like it
 | |
| 
 | |
| checkWebOpts :: WebOpts -> WebOpts
 | |
| checkWebOpts = id
 | |
| 
 | |
| getHledgerWebOpts :: IO WebOpts
 | |
| getHledgerWebOpts = do
 | |
|   args <- fmap replaceNumericFlags . expandArgsAt =<< getArgs
 | |
|   rawOptsToWebOpts . either usageError id $ process webmode args
 | |
| 
 | |
| data Capability
 | |
|   = CapView
 | |
|   | CapAdd
 | |
|   | CapManage
 | |
|   deriving (Eq, Ord, Bounded, Enum, Show)
 | |
| 
 | |
| capabilityFromText :: Text -> Either Text Capability
 | |
| capabilityFromText "view" = Right CapView
 | |
| capabilityFromText "add" = Right CapAdd
 | |
| capabilityFromText "manage" = Right CapManage
 | |
| capabilityFromText x = Left x
 | |
| 
 | |
| capabilityFromBS :: ByteString -> Either ByteString Capability
 | |
| capabilityFromBS "view" = Right CapView
 | |
| capabilityFromBS "add" = Right CapAdd
 | |
| capabilityFromBS "manage" = Right CapManage
 | |
| capabilityFromBS x = Left x
 | |
| 
 | |
| simplePolicyWithOrigin :: Origin -> CorsResourcePolicy
 | |
| simplePolicyWithOrigin origin =
 | |
|     simpleCorsResourcePolicy { corsOrigins = Just ([origin], False) }
 | |
| 
 | |
| 
 | |
| corsPolicyFromString :: String -> WAI.Middleware
 | |
| corsPolicyFromString origin =
 | |
|   let
 | |
|     policy = case origin of
 | |
|         "*" -> simpleCorsResourcePolicy
 | |
|         url -> simplePolicyWithOrigin $ fromString url
 | |
|   in
 | |
|     cors (const $ Just policy)
 | |
| 
 | |
| corsPolicy :: WebOpts -> (Application -> Application)
 | |
| corsPolicy opts =
 | |
|   maybe id corsPolicyFromString $ cors_ opts
 |