220 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			6.6 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 (progname, version)
 | 
						|
import Hledger.Web.Settings (defhost, defport, defbaseurl)
 | 
						|
 | 
						|
progname, version :: String
 | 
						|
progname = "hledger-web"
 | 
						|
#ifdef VERSION
 | 
						|
version = VERSION
 | 
						|
#else
 | 
						|
version = ""
 | 
						|
#endif
 | 
						|
prognameandversion :: String
 | 
						|
prognameandversion = progname ++ " " ++ version :: String
 | 
						|
 | 
						|
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
 |