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:
		
							parent
							
								
									c7a88b50fb
								
							
						
					
					
						commit
						72acd7c22a
					
				| @ -9,15 +9,19 @@ Released under GPL version 3 or later. | |||||||
| 
 | 
 | ||||||
| module Hledger.Web.Main where | module Hledger.Web.Main where | ||||||
| 
 | 
 | ||||||
|  | import Control.Exception (bracket) | ||||||
| import Control.Monad (when) | import Control.Monad (when) | ||||||
| import Data.String (fromString) | import Data.String (fromString) | ||||||
| import qualified Data.Text as T | import qualified Data.Text as T | ||||||
|  | import Network.Socket | ||||||
| import Network.Wai (Application) | 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 Network.Wai.Handler.Launch (runHostPortUrl) | ||||||
| import Prelude hiding (putStrLn) | import Prelude hiding (putStrLn) | ||||||
| import System.Exit (exitSuccess) | import System.Directory (removeFile) | ||||||
|  | import System.Exit (exitSuccess, exitFailure) | ||||||
| import System.IO (hFlush, stdout) | import System.IO (hFlush, stdout) | ||||||
|  | import System.PosixCompat.Files (getFileStatus, isSocket) | ||||||
| import Text.Printf (printf) | import Text.Printf (printf) | ||||||
| import Yesod.Default.Config | import Yesod.Default.Config | ||||||
| import Yesod.Default.Main (defaultDevelApp) | import Yesod.Default.Main (defaultDevelApp) | ||||||
| @ -76,7 +80,27 @@ web opts j = do | |||||||
|       putStrLn "Press ctrl-c to quit" |       putStrLn "Press ctrl-c to quit" | ||||||
|       hFlush stdout |       hFlush stdout | ||||||
|       let warpsettings = setHost (fromString h) (setPort p defaultSettings) |       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 |     else do | ||||||
|       putStrLn "This server will exit after 2m with no browser windows open (or press ctrl-c)" |       putStrLn "This server will exit after 2m with no browser windows open (or press ctrl-c)" | ||||||
|       putStrLn "Opening web browser..." |       putStrLn "Opening web browser..." | ||||||
|  | |||||||
| @ -43,6 +43,11 @@ webflags = | |||||||
|       (\s opts -> Right $ setopt "cors" s opts) |       (\s opts -> Right $ setopt "cors" s opts) | ||||||
|       "ORIGIN" |       "ORIGIN" | ||||||
|       ("allow cross-origin requests from the specified origin; setting ORIGIN to \"*\" allows requests from any 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 |   , flagReq | ||||||
|       ["host"] |       ["host"] | ||||||
|       (\s opts -> Right $ setopt "host" s opts) |       (\s opts -> Right $ setopt "host" s opts) | ||||||
| @ -110,10 +115,11 @@ data WebOpts = WebOpts | |||||||
|   , capabilities_ :: [Capability] |   , capabilities_ :: [Capability] | ||||||
|   , capabilitiesHeader_ :: Maybe (CI ByteString) |   , capabilitiesHeader_ :: Maybe (CI ByteString) | ||||||
|   , cliopts_ :: CliOpts |   , cliopts_ :: CliOpts | ||||||
|  |   , socket_ :: Maybe String | ||||||
|   } deriving (Show) |   } deriving (Show) | ||||||
| 
 | 
 | ||||||
| defwebopts :: WebOpts | 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 | instance Default WebOpts where def = defwebopts | ||||||
| 
 | 
 | ||||||
| @ -131,9 +137,12 @@ rawOptsToWebOpts rawopts = | |||||||
|           Left e -> error' ("Unknown capability: " ++ T.unpack e) |           Left e -> error' ("Unknown capability: " ++ T.unpack e) | ||||||
|           Right [] -> [CapView, CapAdd] |           Right [] -> [CapView, CapAdd] | ||||||
|           Right xs -> xs |           Right xs -> xs | ||||||
|  |         sock = stripTrailingSlash <$> maybestringopt "socket" rawopts | ||||||
|     return |     return | ||||||
|       defwebopts |       defwebopts | ||||||
|       { serve_ = boolopt "serve" rawopts |       { serve_ = case sock of | ||||||
|  |           Just _ -> True | ||||||
|  |           Nothing -> boolopt "serve" rawopts | ||||||
|       , serve_api_ = boolopt "serve-api" rawopts |       , serve_api_ = boolopt "serve-api" rawopts | ||||||
|       , cors_ = maybestringopt "cors" rawopts |       , cors_ = maybestringopt "cors" rawopts | ||||||
|       , host_ = h |       , host_ = h | ||||||
| @ -143,6 +152,7 @@ rawOptsToWebOpts rawopts = | |||||||
|       , capabilities_ = caps |       , capabilities_ = caps | ||||||
|       , capabilitiesHeader_ = mk . BC.pack <$> maybestringopt "capabilities-header" rawopts |       , capabilitiesHeader_ = mk . BC.pack <$> maybestringopt "capabilities-header" rawopts | ||||||
|       , cliopts_ = cliopts |       , cliopts_ = cliopts | ||||||
|  |       , socket_ = sock | ||||||
|       } |       } | ||||||
|   where |   where | ||||||
|     stripTrailingSlash = reverse . dropWhile (== '/') . reverse -- yesod don't like it |     stripTrailingSlash = reverse . dropWhile (== '/') . reverse -- yesod don't like it | ||||||
|  | |||||||
| @ -1,10 +1,10 @@ | |||||||
| cabal-version: 1.12 | 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 | -- see: https://github.com/sol/hpack | ||||||
| -- | -- | ||||||
| -- hash: 194a16a30440803cd2bef5d5df0b6115ad66076c44407ed31efea9c4602e5e95 | -- hash: a9a6dea39ea5c963970cda9f595e7c4251332954aacd137fb6638a1e7640ee59 | ||||||
| 
 | 
 | ||||||
| name:           hledger-web | name:           hledger-web | ||||||
| version:        1.16.99 | version:        1.16.99 | ||||||
| @ -176,12 +176,14 @@ library | |||||||
|     , http-types |     , http-types | ||||||
|     , megaparsec >=7.0.0 && <8 |     , megaparsec >=7.0.0 && <8 | ||||||
|     , mtl >=2.2.1 |     , mtl >=2.2.1 | ||||||
|  |     , network | ||||||
|     , semigroups |     , semigroups | ||||||
|     , shakespeare >=2.0.2.2 |     , shakespeare >=2.0.2.2 | ||||||
|     , template-haskell |     , template-haskell | ||||||
|     , text >=1.2 |     , text >=1.2 | ||||||
|     , time >=1.5 |     , time >=1.5 | ||||||
|     , transformers |     , transformers | ||||||
|  |     , unix-compat | ||||||
|     , utf8-string |     , utf8-string | ||||||
|     , wai |     , wai | ||||||
|     , wai-cors |     , wai-cors | ||||||
|  | |||||||
| @ -72,6 +72,10 @@ as shown in the synopsis above. | |||||||
| `--port=PORT` | `--port=PORT` | ||||||
| : listen on this TCP port (default: 5000) | : 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` | `--base-url=URL` | ||||||
| : set the base url (default: http://IPADDR:PORT). | : set the base url (default: http://IPADDR:PORT). | ||||||
| You would change this when sharing over the network, or integrating within a larger website. | 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 | Similarly, use `--port` to set a TCP port other than 5000, eg if you are | ||||||
| running multiple hledger-web instances. | 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, | 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. | 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 | The default is `http://HOST:PORT/` using the server's configured host address and TCP port | ||||||
|  | |||||||
| @ -120,12 +120,14 @@ library: | |||||||
|   - http-types |   - http-types | ||||||
|   - megaparsec >=7.0.0 && <8 |   - megaparsec >=7.0.0 && <8 | ||||||
|   - mtl >=2.2.1 |   - mtl >=2.2.1 | ||||||
|  |   - network | ||||||
|   - semigroups |   - semigroups | ||||||
|   - shakespeare >=2.0.2.2 |   - shakespeare >=2.0.2.2 | ||||||
|   - template-haskell |   - template-haskell | ||||||
|   - text >=1.2 |   - text >=1.2 | ||||||
|   - time >=1.5 |   - time >=1.5 | ||||||
|   - transformers |   - transformers | ||||||
|  |   - unix-compat | ||||||
|   - utf8-string |   - utf8-string | ||||||
|   - wai |   - wai | ||||||
|   - wai-extra |   - wai-extra | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user