Skip to content
Browse files

web: Add option --socket to use UNIX socket file

This commit adds the --socket option to use hledger-web over an AF_UNIX socket
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
  • Loading branch information
CRTified authored and simonmichael committed Jul 17, 2019
1 parent c7a88b5 commit 72acd7c22a5db0bc8bbf3a53f96fef5d12c07488
@@ -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
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"
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..."
@@ -43,6 +43,11 @@ webflags =
(\s opts -> Right $ setopt "cors" s opts)
("allow cross-origin requests from the specified origin; setting ORIGIN to \"*\" allows requests from any origin")
, flagReq
(\s opts -> Right $ setopt "socket" s opts)
"use the given socket instead of the given IP and port (implies --serve)"
, flagReq
(\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
{ 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
stripTrailingSlash = reverse . dropWhile (== '/') . reverse -- yesod don't like it
@@ -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:
-- 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 >=
, template-haskell
, text >=1.2
, time >=1.5
, transformers
, unix-compat
, utf8-string
, wai
, wai-cors
@@ -72,6 +72,10 @@ as shown in the synopsis above.
: listen on this TCP port (default: 5000)

: 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.

: 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` 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](
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
@@ -120,12 +120,14 @@ library:
- http-types
- megaparsec >=7.0.0 && <8
- mtl >=2.2.1
- network
- semigroups
- shakespeare >=
- template-haskell
- text >=1.2
- time >=1.5
- transformers
- unix-compat
- utf8-string
- wai
- wai-extra

0 comments on commit 72acd7c

Please sign in to comment.
You can’t perform that action at this time.