From 72acd7c22a5db0bc8bbf3a53f96fef5d12c07488 Mon Sep 17 00:00:00 2001 From: Carl Richard Theodor Schneider Date: Thu, 18 Jul 2019 01:58:33 +0200 Subject: [PATCH] web: Add option --socket to use UNIX socket file This commit adds the --socket option to use hledger-web over an AF_UNIX socket file. It allows running multiple instances of hledger-web on the same system without having to manually choose a port for each instance, which is helpful for running individual instances for multiple users. In this scenario, the socket path is predictable, as it can be derived from the username. It also introduces the following dependencies: - network - Used to create the unix domain socket - unix-compat - Used to identify if the socket file is still a socket, to reduce the risk of deleting a file when cleaning up the socket --- hledger-web/Hledger/Web/Main.hs | 30 ++++++++++++++++++++++++--- hledger-web/Hledger/Web/WebOptions.hs | 14 +++++++++++-- hledger-web/hledger-web.cabal | 6 ++++-- hledger-web/hledger-web.m4.md | 18 ++++++++++++++++ hledger-web/package.yaml | 2 ++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/hledger-web/Hledger/Web/Main.hs b/hledger-web/Hledger/Web/Main.hs index c3173d103..dbbf7d66a 100644 --- a/hledger-web/Hledger/Web/Main.hs +++ b/hledger-web/Hledger/Web/Main.hs @@ -9,15 +9,19 @@ Released under GPL version 3 or later. module Hledger.Web.Main where +import Control.Exception (bracket) import Control.Monad (when) import Data.String (fromString) import qualified Data.Text as T +import Network.Socket import Network.Wai (Application) -import Network.Wai.Handler.Warp (runSettings, defaultSettings, setHost, setPort) +import Network.Wai.Handler.Warp (runSettings, runSettingsSocket, defaultSettings, setHost, setPort) import Network.Wai.Handler.Launch (runHostPortUrl) import Prelude hiding (putStrLn) -import System.Exit (exitSuccess) +import System.Directory (removeFile) +import System.Exit (exitSuccess, exitFailure) import System.IO (hFlush, stdout) +import System.PosixCompat.Files (getFileStatus, isSocket) import Text.Printf (printf) import Yesod.Default.Config import Yesod.Default.Main (defaultDevelApp) @@ -76,7 +80,27 @@ web opts j = do putStrLn "Press ctrl-c to quit" hFlush stdout let warpsettings = setHost (fromString h) (setPort p defaultSettings) - Network.Wai.Handler.Warp.runSettings warpsettings app + case socket_ opts of + Just s -> do + if isUnixDomainSocketAvailable then + bracket + (do + sock <- socket AF_UNIX Stream 0 + setSocketOption sock ReuseAddr 1 + bind sock $ SockAddrUnix s + listen sock maxListenQueue + return sock + ) + (\_ -> do + sockstat <- getFileStatus s + when (isSocket sockstat) $ removeFile s + ) + (\sock -> Network.Wai.Handler.Warp.runSettingsSocket warpsettings sock app) + else do + putStrLn "Unix domain sockets are not available on your operating system" + putStrLn "Please try again without --socket" + exitFailure + Nothing -> Network.Wai.Handler.Warp.runSettings warpsettings app else do putStrLn "This server will exit after 2m with no browser windows open (or press ctrl-c)" putStrLn "Opening web browser..." diff --git a/hledger-web/Hledger/Web/WebOptions.hs b/hledger-web/Hledger/Web/WebOptions.hs index ee83f1f88..174df1c29 100644 --- a/hledger-web/Hledger/Web/WebOptions.hs +++ b/hledger-web/Hledger/Web/WebOptions.hs @@ -43,6 +43,11 @@ webflags = (\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) @@ -110,10 +115,11 @@ data WebOpts = WebOpts , capabilities_ :: [Capability] , capabilitiesHeader_ :: Maybe (CI ByteString) , cliopts_ :: CliOpts + , socket_ :: Maybe String } deriving (Show) defwebopts :: WebOpts -defwebopts = WebOpts def def Nothing def def def def [CapView, CapAdd] Nothing def +defwebopts = WebOpts def def Nothing def def def def [CapView, CapAdd] Nothing def Nothing instance Default WebOpts where def = defwebopts @@ -131,9 +137,12 @@ rawOptsToWebOpts rawopts = Left e -> error' ("Unknown capability: " ++ T.unpack e) Right [] -> [CapView, CapAdd] Right xs -> xs + sock = stripTrailingSlash <$> maybestringopt "socket" rawopts return defwebopts - { serve_ = boolopt "serve" rawopts + { serve_ = case sock of + Just _ -> True + Nothing -> boolopt "serve" rawopts , serve_api_ = boolopt "serve-api" rawopts , cors_ = maybestringopt "cors" rawopts , host_ = h @@ -143,6 +152,7 @@ rawOptsToWebOpts 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 diff --git a/hledger-web/hledger-web.cabal b/hledger-web/hledger-web.cabal index def16214f..5b2132e09 100644 --- a/hledger-web/hledger-web.cabal +++ b/hledger-web/hledger-web.cabal @@ -1,10 +1,10 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.31.2. +-- This file has been generated from package.yaml by hpack version 0.32.0. -- -- see: https://github.com/sol/hpack -- --- hash: 194a16a30440803cd2bef5d5df0b6115ad66076c44407ed31efea9c4602e5e95 +-- hash: a9a6dea39ea5c963970cda9f595e7c4251332954aacd137fb6638a1e7640ee59 name: hledger-web version: 1.16.99 @@ -176,12 +176,14 @@ library , http-types , megaparsec >=7.0.0 && <8 , mtl >=2.2.1 + , network , semigroups , shakespeare >=2.0.2.2 , template-haskell , text >=1.2 , time >=1.5 , transformers + , unix-compat , utf8-string , wai , wai-cors diff --git a/hledger-web/hledger-web.m4.md b/hledger-web/hledger-web.m4.md index 84bf633ba..2a9b7801f 100644 --- a/hledger-web/hledger-web.m4.md +++ b/hledger-web/hledger-web.m4.md @@ -72,6 +72,10 @@ as shown in the synopsis above. `--port=PORT` : listen on this TCP port (default: 5000) +`--socket=SOCKETFILE` +: use a unix domain socket file to listen for requests instead of a TCP socket. Implies +`--serve`. It can only be used if the operating system can provide this type of socket. + `--base-url=URL` : set the base url (default: http://IPADDR:PORT). You would change this when sharing over the network, or integrating within a larger website. @@ -119,6 +123,20 @@ You can use `--host` to change this, eg `--host 0.0.0.0` to listen on all config Similarly, use `--port` to set a TCP port other than 5000, eg if you are running multiple hledger-web instances. +Both of these options are ignored when `--socket` is used. In this case, it +creates an `AF_UNIX` socket file at the supplied path and uses that for communication. +This is an alternative way of running multiple hledger-web instances behind +a reverse proxy that handles authentication for different users. +The path can be derived in a predictable way, eg by using the username within the path. +As an example, `nginx` as reverse proxy can use the variabel `$remote_user` to +derive a path from the username used in a [HTTP basic authentication](https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/). +The following `proxy_pass` directive allows access to all `hledger-web` +instances that created a socket in `/tmp/hledger/`: + +``` + proxy_pass http://unix:/tmp/hledger/${remote_user}.socket; +``` + You can use `--base-url` to change the protocol, hostname, port and path that appear in hyperlinks, useful eg for integrating hledger-web within a larger website. The default is `http://HOST:PORT/` using the server's configured host address and TCP port diff --git a/hledger-web/package.yaml b/hledger-web/package.yaml index 9870d2b85..2ad18e154 100644 --- a/hledger-web/package.yaml +++ b/hledger-web/package.yaml @@ -120,12 +120,14 @@ library: - http-types - megaparsec >=7.0.0 && <8 - mtl >=2.2.1 + - network - semigroups - shakespeare >=2.0.2.2 - template-haskell - text >=1.2 - time >=1.5 - transformers + - unix-compat - utf8-string - wai - wai-extra