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
This commit is contained in:
Carl Richard Theodor Schneider 2019-07-18 01:58:33 +02:00 committed by Simon Michael
parent c7a88b50fb
commit 72acd7c22a
5 changed files with 63 additions and 7 deletions

View File

@ -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..."

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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