hprox-0.7.1: src/Network/HProx/Config.hs
-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2026 Bin Jin. All Rights Reserved.
{-# LANGUAGE CPP #-}
module Network.HProx.Config
( CertFile(..)
, Config(..)
, defaultConfig
, getConfig
) where
import Data.ByteString.Char8 qualified as BS8
import Data.List (elemIndex, elemIndices)
import Data.List.NonEmpty (NonEmpty(..))
import Data.Version (showVersion)
import Options.Applicative
import Network.HProx.Log
import Network.HProx.Util
import Paths_hprox
-- | Configuration of HProx, see @hprox --help@ for details
data Config = Config
{ _bind :: !(Maybe String)
, _port :: !Int
, _ssl :: ![(String, CertFile)]
, _auth :: !(Maybe FilePath)
, _ws :: !(Maybe BS8.ByteString)
, _rev :: ![(Maybe BS8.ByteString, BS8.ByteString, BS8.ByteString)]
, _doh :: !(Maybe String)
, _hide :: !Bool
, _naive :: !Bool
, _name :: !BS8.ByteString
, _acme :: !(Maybe BS8.ByteString)
, _log :: !String
, _loglevel :: !LogLevel
#ifdef OS_UNIX
, _user :: !(Maybe String)
, _group :: !(Maybe String)
#endif
#ifdef QUIC_ENABLED
, _quic :: !(Maybe Int)
#endif
}
-- | Default value of 'Config', same as running @hprox@ without arguments
defaultConfig :: Config
defaultConfig = Config
{ _bind = Nothing
, _port = 3000
, _ssl = []
, _auth = Nothing
, _ws = Nothing
, _rev = []
, _doh = Nothing
, _hide = False
, _naive = False
, _name = "hprox"
, _acme = Nothing
, _log = "stdout"
, _loglevel = INFO
#ifdef OS_UNIX
, _user = Nothing
, _group = Nothing
#endif
#ifdef QUIC_ENABLED
, _quic = Nothing
#endif
}
-- | Certificate file pairs
data CertFile = CertFile
{ certfile :: !FilePath
, keyfile :: !FilePath
}
parser :: ParserInfo Config
parser = info (helper <*> ver <*> config) (fullDesc <> progDesc desc)
where
parseBoundedPort optionName raw =
case reads raw of
[(parsedPort, "")] | validPort parsedPort -> Right parsedPort
_ -> Left $ optionName <> " must be in range 1..65535"
validPort parsedPort = parsedPort >= (1 :: Int) && parsedPort <= 65535
parseSSL s = case splitBy ':' s of
host :| [cert, key] -> Right (host, CertFile cert key)
_otherwise -> Left "invalid format for ssl certificates"
parseRev0 s@('/':_) = do
(domain, prefix, remote) <- case elemIndices '/' s of
[] -> Nothing
indices -> let (prefix, remote) = splitAt (last indices + 1) s
in Just (Nothing, BS8.pack prefix, BS8.pack remote)
if BS8.null remote
then Nothing
else Just (domain, prefix, remote)
parseRev0 remote
| null remote = Nothing
| otherwise = Just (Nothing, "/", BS8.pack remote)
parseRev ('/':'/':s) = case elemIndex '/' s of
Nothing -> Nothing
Just 0 -> Nothing
Just ind -> let (domain, other) = splitAt ind s
in do (_, prefix, remote) <- parseRev0 other
return (Just (BS8.pack domain), prefix, remote)
parseRev s = parseRev0 s
desc = "a lightweight HTTP proxy server, and more"
ver = infoOption (showVersion version) (long "version" <> help "Display the version information")
config = Config <$> bind
<*> port
<*> ssl
<*> auth
<*> ws
<*> rev
<*> doh
<*> hide
<*> naive
<*> name
<*> acme
<*> logging
<*> loglevel
#ifdef OS_UNIX
<*> user
<*> group
#endif
#ifdef QUIC_ENABLED
<*> quic
#endif
bind = optional $ strOption
( long "bind"
<> short 'b'
<> metavar "bind_ip"
<> help "Specify the IP address to bind to (default: all interfaces)")
port = option (eitherReader (parseBoundedPort "--port"))
( long "port"
<> short 'p'
<> metavar "port"
<> value 3000
<> showDefault
<> help "Specify the port number")
ssl = many $ option (eitherReader parseSSL)
( long "tls"
<> short 's'
<> metavar "hostname:cerfile:keyfile"
<> help "Enable TLS and specify a domain with its associated TLS certificate (can be specified multiple times for multiple domains)")
auth = optional $ strOption
( long "auth"
<> short 'a'
<> metavar "userpass.txt"
<> help "Specify the password file for proxy authentication. Plaintext passwords should be in the format 'user:pass' and will be automatically Argon2-hashed by hprox. Ensure that the password file with plaintext password is writable")
ws = optional $ strOption
( long "ws"
<> metavar "remote-host:port"
<> help "Specify the remote host to handle WebSocket requests (port 443 indicates an HTTPS remote server)")
rev = many $ option (maybeReader parseRev)
( long "rev"
<> metavar "[//domain/][/prefix/]remote-host:port"
<> help "Specify the remote host for reverse proxy (port 443 indicates an HTTPS remote server). An optional '//domain/' will only process requests with the 'Host: domain' header, and an optional '/prefix/' can be specified as a prefix to be matched (and stripped in proxied request)")
doh = optional $ strOption
( long "doh"
<> metavar "dns-server:port"
<> help "Enable DNS-over-HTTPS (DoH) support (port 53 will be used if not specified)")
hide = switch
( long "hide"
<> help "Never send 'Proxy Authentication Required' response. Note that this might break the use of HTTPS proxy in browsers")
naive = switch
( long "naive"
<> help "Add naiveproxy-compatible padding (requires TLS)")
name = strOption
( long "name"
<> metavar "server-name"
<> value "hprox"
<> showDefault
<> help "Specify the server name for the 'Server' header")
acme = optional $ strOption
( long "acme"
<> metavar "ACCOUNT_THUMBPRINT"
<> help "Set the thumbprint for stateless http-01 ACME challenge as specified by RFC8555")
logging = strOption
( long "log"
<> metavar "<none|stdout|stderr|file>"
<> value "stdout"
<> showDefault
<> help "Specify the logging type")
loglevel = option (maybeReader logLevelReader)
( long "loglevel"
<> metavar "<trace|debug|info|warn|error|none>"
<> value INFO
<> help "Specify the logging level (default: info)")
#ifdef OS_UNIX
user = optional $ strOption
( long "user"
<> short 'u'
<> metavar "nobody"
<> help "Drop root priviledge and setuid to the specified user (like nobody)")
group = optional $ strOption
( long "group"
<> short 'g'
<> metavar "nogroup"
<> help "Drop root priviledge and setgid to the specified group")
#endif
#ifdef QUIC_ENABLED
quic = optional $ option (eitherReader (parseBoundedPort "--quic"))
( long "quic"
<> short 'q'
<> metavar "port"
<> help "Enable QUIC (HTTP/3) on UDP port")
#endif
-- | Read 'Config' from command line arguments
getConfig :: IO Config
getConfig = execParser parser