hprox-0.1.0: Main.hs
-- SPDX-License-Identifier: Apache-2.0
--
-- Copyright (C) 2019 Bin Jin. All Rights Reserved.
{-# LANGUAGE OverloadedStrings #-}
module Main where
import qualified Data.ByteString.Char8 as BS8
import Data.String (fromString)
import qualified Network.HTTP.Client as HC
import Network.TLS as TLS
import Network.Wai.Handler.Warp (HostPreference, defaultSettings,
runSettings, setBeforeMainLoop,
setHost, setNoParsePath, setPort,
setServerName)
import Network.Wai.Handler.WarpTLS (OnInsecure (..), onInsecure,
runTLS, tlsServerHooks,
tlsSettings)
import Network.Wai.Middleware.Gzip (def, gzip)
import System.Posix.User (UserEntry (..),
getUserEntryForName, setUserID)
import Data.Maybe
import Data.Monoid ((<>))
import Options.Applicative
import HProx (ProxySettings (..), dumbApp,
forceSSL, httpProxy, reverseProxy)
data Opts = Opts
{ _bind :: Maybe HostPreference
, _port :: Int
, _ssl :: [(String, CertFile)]
, _user :: Maybe String
, _auth :: Maybe FilePath
, _ws :: Maybe String
, _rev :: Maybe String
}
data CertFile = CertFile
{ certfile :: FilePath
, keyfile :: FilePath
}
readCert :: CertFile -> IO TLS.Credential
readCert (CertFile c k) = either error id <$> TLS.credentialLoadX509 c k
splitBy :: Eq a => a -> [a] -> [[a]]
splitBy _ [] = [[]]
splitBy c (x:xs)
| c == x = [] : splitBy c xs
| otherwise = let y:ys = splitBy c xs in (x:y):ys
parser :: ParserInfo Opts
parser = info (helper <*> opts) fullDesc
where
parseSSL s = case splitBy ':' s of
[host, cert, key] -> Right (host, CertFile cert key)
_ -> Left "invalid format for ssl certificates"
opts = Opts <$> bind
<*> (fromMaybe 3000 <$> port)
<*> ssl
<*> user
<*> auth
<*> ws
<*> rev
bind = optional $ fromString <$> strOption
( long "bind"
<> short 'b'
<> metavar "bind_ip"
<> help "The address to bind on (default: all interfaces)")
port = optional $ option auto
( long "port"
<> short 'p'
<> metavar "port"
<> help "port number (default 3000)")
ssl = many $ option (eitherReader parseSSL)
( long "ssl"
<> short 's'
<> metavar "hostname:cerfile:keyfile"
<> help "enable SSL and specify a SSL certificates")
user = optional $ strOption
( long "user"
<> short 'u'
<> metavar "nobody"
<> help "setuid after binding port")
auth = optional $ strOption
( long "auth"
<> short 'a'
<> metavar "users.txt"
<> help "password file for proxy authentication (each line with a colon separated user/pass pair)")
ws = optional $ strOption
( long "ws"
<> metavar "remote-host:80"
<> help "remote host to handle websocket requests")
rev = optional $ strOption
( long "rev"
<> metavar "remote-host:80"
<> help "remote host for revere proxy")
setuid :: String -> IO ()
setuid user = getUserEntryForName user >>= setUserID . userID
main :: IO ()
main = do
opts <- execParser parser
let certfiles = _ssl opts
certs <- mapM (readCert.snd) certfiles
let isSSL = not (null certfiles)
(primaryHost, primaryCert) = head certfiles
otherCerts = tail $ zip (map fst certfiles) certs
settings = setNoParsePath True $
setServerName "Apache" $
maybe id (setBeforeMainLoop . setuid) (_user opts)
defaultSettings
tlsset' = tlsSettings (certfile primaryCert) (keyfile primaryCert)
hooks = (tlsServerHooks tlsset') { onServerNameIndication = onSNI }
tlsset = tlsset' { tlsServerHooks = hooks, onInsecure = AllowInsecure }
failSNI = fail "SNI" >> return mempty
onSNI Nothing = failSNI
onSNI (Just host)
| host == primaryHost = return mempty
| otherwise = case lookup host otherCerts of
Nothing -> failSNI
Just cert -> return (TLS.Credentials [cert])
runner | isSSL = runTLS tlsset
| otherwise = runSettings
pauth <- case _auth opts of
Nothing -> return Nothing
Just f -> Just . flip elem . filter (isJust . BS8.elemIndex ':') . BS8.lines <$> BS8.readFile f
manager <- HC.newManager HC.defaultManagerSettings
let pset = ProxySettings pauth Nothing (BS8.pack <$> _ws opts) (BS8.pack <$> _rev opts)
proxy = (if isSSL then forceSSL else id) $ gzip def $ httpProxy pset manager $ reverseProxy pset manager dumbApp
port = _port opts
runner (setHost (fromMaybe "*6" (_bind opts)) $ setPort port settings) proxy