packages feed

hprox 0.6.1 → 0.6.2

raw patch · 9 files changed

+267/−136 lines, 9 filesdep +directorydep ~basedep ~bytestringdep ~http-typesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: directory

Dependency ranges changed: base, bytestring, http-types, wai

API changes (from Hackage documentation)

+ Network.HProx: [_group] :: Config -> !Maybe String
+ Network.HProx: [_user] :: Config -> !Maybe String
- Network.HProx: CertFile :: FilePath -> FilePath -> CertFile
+ Network.HProx: CertFile :: !FilePath -> !FilePath -> CertFile
- Network.HProx: Config :: Maybe String -> Int -> [(String, CertFile)] -> Maybe FilePath -> Maybe ByteString -> [(Maybe ByteString, ByteString, ByteString)] -> Maybe String -> Bool -> Bool -> ByteString -> Maybe ByteString -> String -> LogLevel -> Config
+ Network.HProx: Config :: !Maybe String -> !Int -> ![(String, CertFile)] -> !Maybe FilePath -> !Maybe ByteString -> ![(Maybe ByteString, ByteString, ByteString)] -> !Maybe String -> !Bool -> !Bool -> !ByteString -> !Maybe ByteString -> !String -> !LogLevel -> !Maybe String -> !Maybe String -> Config
- Network.HProx: [_acme] :: Config -> Maybe ByteString
+ Network.HProx: [_acme] :: Config -> !Maybe ByteString
- Network.HProx: [_auth] :: Config -> Maybe FilePath
+ Network.HProx: [_auth] :: Config -> !Maybe FilePath
- Network.HProx: [_bind] :: Config -> Maybe String
+ Network.HProx: [_bind] :: Config -> !Maybe String
- Network.HProx: [_doh] :: Config -> Maybe String
+ Network.HProx: [_doh] :: Config -> !Maybe String
- Network.HProx: [_hide] :: Config -> Bool
+ Network.HProx: [_hide] :: Config -> !Bool
- Network.HProx: [_log] :: Config -> String
+ Network.HProx: [_log] :: Config -> !String
- Network.HProx: [_loglevel] :: Config -> LogLevel
+ Network.HProx: [_loglevel] :: Config -> !LogLevel
- Network.HProx: [_naive] :: Config -> Bool
+ Network.HProx: [_naive] :: Config -> !Bool
- Network.HProx: [_name] :: Config -> ByteString
+ Network.HProx: [_name] :: Config -> !ByteString
- Network.HProx: [_port] :: Config -> Int
+ Network.HProx: [_port] :: Config -> !Int
- Network.HProx: [_rev] :: Config -> [(Maybe ByteString, ByteString, ByteString)]
+ Network.HProx: [_rev] :: Config -> ![(Maybe ByteString, ByteString, ByteString)]
- Network.HProx: [_ssl] :: Config -> [(String, CertFile)]
+ Network.HProx: [_ssl] :: Config -> ![(String, CertFile)]
- Network.HProx: [_ws] :: Config -> Maybe ByteString
+ Network.HProx: [_ws] :: Config -> !Maybe ByteString
- Network.HProx: [certfile] :: CertFile -> FilePath
+ Network.HProx: [certfile] :: CertFile -> !FilePath
- Network.HProx: [keyfile] :: CertFile -> FilePath
+ Network.HProx: [keyfile] :: CertFile -> !FilePath

Files

Changelog.md view
@@ -1,3 +1,9 @@+## 0.6.2++- fixes to improve ssltest result+- unix: support setuid after binding port+- remove graceful close+ ## 0.6.1  - multiple certificates and SNI support for HTTP/3
+ cbits/setcap.c view
@@ -0,0 +1,60 @@+// SPDX-License-Identifier: BSD3+//+// Copyright (C) 2020 Kazu Yamamoto. All Rights Reserved.+//+// File taken from https://github.com/kazu-yamamoto/mighttpd2+// See https://kazu-yamamoto.hatenablog.jp/entry/2020/12/10/150731 for details+//+#if defined(__linux__)+#include <linux/securebits.h>+#include <signal.h>+#include <stdio.h>+#include <string.h>+#include <sys/capability.h>+#include <sys/prctl.h>+#include <sys/syscall.h>+#include <sys/types.h>+#include <unistd.h>++#include "Rts.h"++void set_capabilities (uint32_t cap) {+  cap_user_header_t header = malloc(sizeof(*header));+  header->version = _LINUX_CAPABILITY_VERSION_3;+  header->pid = 0;++  cap_user_data_t data = malloc(sizeof(*data));+  data->effective   = cap;+  data->permitted   = cap;+  data->inheritable = 0;++  capset(header,data);++  free(header);+  free(data);+}++void handler(int signum) {+  uint32_t cap = 1 << CAP_NET_BIND_SERVICE;+  set_capabilities (cap);+}++void FlagDefaultsHook () {+  if (geteuid() == 0) {+    prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS, 0L, 0L, 0L);++    struct sigaction sa;+    memset(&sa, 0, sizeof(sa));+    sa.sa_handler = handler;+    sa.sa_flags = SA_RESTART;+    sigaction(SIGUSR1, &sa, NULL);+  }+}++void send_signal (int tid, int sig) {+  int tgid = getpid();+  syscall(SYS_tgkill, tgid, tid, sig);+}+#else+void send_signal (int tid, int sig) {}+#endif
hprox.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack  name:           hprox-version:        0.6.1+version:        0.6.2 synopsis:       a lightweight HTTP proxy server, and more description:    Please see the README on GitHub at <https://github.com/bjin/hprox#readme> category:       Web@@ -87,6 +87,12 @@         http3 >=0.0.3       , quic >=0.1.15       , warp-quic+    if os(linux)+      cpp-options: -DDROP_ALL_CAPS_EXCEPT_BIND+      c-sources:+          cbits/setcap.c+      build-depends:+          directory >=1.2.5.0   if !os(windows)     cpp-options: -DOS_UNIX     build-depends:@@ -104,44 +110,11 @@       RecordWildCards   ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N   build-depends:-      async >=2.2-    , base >=4.12 && <5-    , base64-bytestring >=1.1-    , binary >=0.8-    , bytestring >=0.10-    , case-insensitive >=1.2-    , conduit >=1.3-    , conduit-extra >=1.3-    , crypton-    , data-default-class-    , dns >=4.0-    , fast-logger >=3.0+      base+    , bytestring     , hprox-    , http-client >=0.5-    , http-client-tls >=0.3.4-    , http-reverse-proxy >=0.6.0.2-    , http-types >=0.12-    , http2 >=4.0-    , optparse-applicative >=0.14-    , random >=1.2.1-    , text-    , tls >=1.5-    , tls-session-manager >=0.0.4-    , unordered-containers-    , wai >=3.2.2-    , wai-extra >=3.0-    , warp >=3.2.8-    , warp-tls >=3.2.12+    , http-types+    , wai   default-language: Haskell2010-  if flag(quic)-    cpp-options: -DQUIC_ENABLED-    build-depends:-        http3 >=0.0.3-      , quic >=0.1.15-      , warp-quic-  if !os(windows)-    cpp-options: -DOS_UNIX-    build-depends:-        unix   if flag(static)     ghc-options: -optl-static
src/Network/HProx.hs view
@@ -10,9 +10,9 @@ -}  module Network.HProx-  ( CertFile (..)-  , Config (..)-  , LogLevel (..)+  ( CertFile(..)+  , Config(..)+  , LogLevel(..)   , defaultConfig   , getConfig   , run@@ -21,9 +21,9 @@ import Data.ByteString.Char8       qualified as BS8 import Data.Default.Class          (def) import Data.HashMap.Strict         qualified as HM-import Data.List-    (elemIndex, elemIndices, find, isSuffixOf, sortOn, (\\))-import Data.Ord                    (Down (..))+import Data.List                   (elemIndex, elemIndices, find, isSuffixOf, sortOn, (\\))+import Data.List.NonEmpty          (NonEmpty(..))+import Data.Ord                    (Down(..)) import Data.String                 (fromString) import Data.Version                (showVersion) import Network.HTTP.Client.TLS     (newTlsManager)@@ -33,16 +33,14 @@ import Network.TLS.SessionManager  qualified as SM import Network.Wai                 (Application, rawPathInfo) import Network.Wai.Handler.Warp-    (InvalidRequest (..), defaultSettings, defaultShouldDisplayException,-    runSettings, setHost, setLogger, setNoParsePath, setOnException, setPort,-    setServerName)+    (InvalidRequest(..), defaultSettings, defaultShouldDisplayException, runSettings, setHost,+    setLogger, setNoParsePath, setOnException, setPort, setServerName) import Network.Wai.Handler.WarpTLS-    (OnInsecure (..), WarpTLSException, defaultTlsSettings, onInsecure, runTLS,-    tlsAllowedVersions, tlsCiphers, tlsCredentials, tlsServerHooks,-    tlsSessionManager)+    (OnInsecure(..), WarpTLSException, defaultTlsSettings, onInsecure, runTLS, tlsAllowedVersions,+    tlsCiphers, tlsCredentials, tlsServerHooks, tlsSessionManager) -import Control.Exception    (Exception (..))-import GHC.IO.Exception     (IOErrorType (..))+import Control.Exception    (Exception(..))+import GHC.IO.Exception     (IOErrorType(..)) import Network.HTTP2.Client qualified as H2 import System.IO.Error      (ioeGetErrorType) @@ -55,10 +53,19 @@ #endif  #ifdef OS_UNIX-import Network.Wai.Handler.Warp-    (setGracefulShutdownTimeout, setInstallShutdownHandler)-import System.Posix.Signals+import Control.Exception        (SomeException, catch)+import Network.Wai.Handler.Warp (setBeforeMainLoop)+import System.Exit+import System.Posix.Process     (exitImmediately)+import System.Posix.User++#ifdef DROP_ALL_CAPS_EXCEPT_BIND+import Foreign.C.Types      (CInt(..))+import System.Directory     (listDirectory)+import System.Posix.Signals (sigUSR1)+import Text.Read            (readMaybe) #endif+#endif  import Control.Monad import Data.Maybe@@ -72,35 +79,43 @@  -- | 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+  { _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+  , _quic     :: !(Maybe Int) #endif   }  -- | Default value of 'Config', same as running @hprox@ without arguments defaultConfig :: Config defaultConfig = Config Nothing 3000 [] Nothing Nothing [] Nothing False False "hprox" Nothing "stdout" INFO+#ifdef OS_UNIX+  Nothing+  Nothing+#endif #ifdef QUIC_ENABLED     Nothing #endif  -- | Certificate file pairs data CertFile = CertFile-  { certfile :: FilePath-  , keyfile  :: FilePath+  { certfile :: !FilePath+  , keyfile  :: !FilePath   }  readCert :: CertFile -> IO TLS.Credential@@ -110,8 +125,8 @@ parser = info (helper <*> ver <*> config) (fullDesc <> progDesc desc)   where     parseSSL s = case splitBy ':' s of-        [host, cert, key] -> Right (host, CertFile cert key)-        _                 -> Left "invalid format for ssl certificates"+        host :| [cert, key] -> Right (host, CertFile cert key)+        _otherwise          -> Left "invalid format for ssl certificates"      parseRev0 s@('/':_) = case elemIndices '/' s of         []      -> Nothing@@ -143,6 +158,10 @@                     <*> acme                     <*> logging                     <*> loglevel+#ifdef OS_UNIX+                    <*> user+                    <*> group+#endif #ifdef QUIC_ENABLED                     <*> quic #endif@@ -221,6 +240,20 @@        <> 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 auto         ( long "quic"@@ -235,6 +268,48 @@ getLoggerType "stderr" = LogStderr 4096 getLoggerType file     = LogFileNoRotate file 4096 +#ifdef OS_UNIX+dropRootPriviledge :: Logger -> Maybe String -> Maybe String -> IO Bool+dropRootPriviledge _ Nothing Nothing = return False+dropRootPriviledge logger user group = do+    currentUser <- getRealUserID+    currentGroup <- getRealGroupID+    if currentUser /= 0 || currentGroup /= 0+      then do+        logger WARN $ "Unable to setuid/setgid without root priviledge" <>+                      ", userID=" <> toLogStr (show currentUser) <>+                      ", groupID=" <> toLogStr (show currentGroup)+        return False+      else do+        let abort msg = logger ERROR msg >> exitImmediately (ExitFailure 1)+        forM_ group $ \group' -> do+            logger INFO $ "setgid to " <> toLogStr group'+            getGroupEntryForName group' >>= setGroupID . groupID+            changedGroup <- getRealGroupID+            when (changedGroup == currentGroup) $ abort "failed to setgid, aborting"+        forM_ user $ \user' -> do+            logger INFO $ "setuid to " <> toLogStr user'+            getUserEntryForName user' >>= setUserID . userID+            changedUser <- getRealUserID+            when (changedUser == currentUser) $ abort "failed to setuid, aborting"+        logger DEBUG "testing setuid(0), verify that root priviledge can't be regranted"+        catch (setUserID 0) $ \(_ :: SomeException) -> logger DEBUG "setuid(0) failed as expected"+        changedUser <- getRealUserID+        when (changedUser == 0) $ abort "unable to drop root priviledge, aborting"+        return True++#ifdef DROP_ALL_CAPS_EXCEPT_BIND+foreign import ccall unsafe "send_signal"+  c_send_signal :: CInt -> CInt -> IO ()++-- Taken from mighttpd2, see https://kazu-yamamoto.hatenablog.jp/entry/2020/12/10/150731 for details+dropAllCapsExceptBind :: IO ()+dropAllCapsExceptBind = do+    tids <- mapMaybe readMaybe <$> listDirectory "/proc/self/task"+    forM_ tids $ \tid -> c_send_signal tid sigUSR1+#endif+#endif+ -- | Read 'Config' from command line arguments getConfig :: IO Config getConfig = execParser parser@@ -264,17 +339,26 @@                    setLogger warpLogger $                    setOnException exceptionHandler $ #ifdef OS_UNIX-                   setGracefulShutdownTimeout (Just 3) $-                   setInstallShutdownHandler shutdownHandler $+                   setBeforeMainLoop doBeforeMainLoop $ #endif                    setNoParsePath True $                    setServerName _name defaultSettings  #ifdef OS_UNIX-        shutdownHandler closeSocket = do-            void $ installHandler sigTERM (CatchOnce $ logger INFO "Received SIGTERM signal, shutting down gracefully" >> closeSocket) Nothing-            void $ installHandler sigINT (CatchOnce $ logger INFO "Received SIGINT signal, shutting down gracefully" >> closeSocket) Nothing+        doBeforeMainLoop = do+            dropped <- dropRootPriviledge logger _user _group+#if defined(DROP_ALL_CAPS_EXCEPT_BIND)+            when dropped $ do+                logger INFO "drop all capabilities except CAP_NET_BIND_SERVICE"+                dropAllCapsExceptBind+#elif defined(QUIC_ENABLED)+            case (dropped, _quic) of+                (True, Just qport) | qport < 1024 -> logger ERROR $ "dropping root priviledge will likely break QUIC connection over UDP port " <> toLogStr (show qport)+                _ -> return ()+#else+            return () #endif+#endif          exceptionHandler req ex             | _loglevel > DEBUG                                 = return ()@@ -290,7 +374,7 @@             | Just ConnectionClosedByPeer <- fromException ex   = return ()             | otherwise                                         =                 logger DEBUG $ "exception: " <> toLogStr (displayException ex) <>-                    (if isJust req then " from: " <> logRequest (fromJust req) else "")+                    maybe "" (\req' -> " from: " <> logRequest req') req          warpLogger req status _             | rawPathInfo req == "/.hprox/health" = return ()@@ -298,17 +382,20 @@                 logger TRACE $ "(" <> toLogStr (HT.statusCode status) <> ") " <> logRequest req          -- https://www.ssllabs.com/ssltest+        -- https://github.com/haskell-tls/hs-tls/blob/master/core/Network/TLS/Extra/Cipher.hs         weak_ciphers = [ TLS.cipher_ECDHE_RSA_AES256CBC_SHA384                        , TLS.cipher_ECDHE_RSA_AES256CBC_SHA                        , TLS.cipher_AES256CCM_SHA256                        , TLS.cipher_AES256GCM_SHA384                        , TLS.cipher_AES256_SHA256                        , TLS.cipher_AES256_SHA1+                       , TLS.cipher_ECDHE_ECDSA_AES256CBC_SHA384+                       , TLS.cipher_ECDHE_ECDSA_AES256CBC_SHA                        ]          tlsset = defaultTlsSettings             { tlsServerHooks     = def { TLS.onServerNameIndication = onSNI }-            , tlsCredentials     = Just (TLS.Credentials certs)+            , tlsCredentials     = Just (TLS.Credentials [head certs])             , onInsecure         = AllowInsecure             , tlsAllowedVersions = [TLS.TLS13, TLS.TLS12]             , tlsCiphers         = TLS.ciphersuite_strong \\ weak_ciphers@@ -334,7 +421,7 @@         quicset qport = Q.defaultServerConfig             { Q.scAddresses      = [(fromString (fromMaybe "0.0.0.0" _bind), fromIntegral qport)]             , Q.scVersions       = [Q.Version1, Q.Version2]-            , Q.scCredentials    = TLS.Credentials certs+            , Q.scCredentials    = TLS.Credentials [head certs]             , Q.scCiphers        = Q.scCiphers Q.defaultServerConfig \\ weak_ciphers             , Q.scALPN           = Just alpn             , Q.scTlsHooks       = def { TLS.onServerNameIndication = onSNI }@@ -391,9 +478,9 @@                 httpProxy pset manager $                 reverseProxy pset manager fallback -    when (isJust _ws) $ logger INFO $ "websocket redirect: " <> toLogStr (fromJust _ws)+    forM_ _ws $ \ws -> logger INFO $ "websocket redirect: " <> toLogStr ws     unless (null revSorted) $ logger INFO $ "reverse proxy: " <> toLogStr (show revSorted)-    when (isJust _doh) $ logger INFO $ "DNS-over-HTTPS redirect: " <> toLogStr (fromJust _doh)+    forM_ _doh $ \doh -> logger INFO $ "DNS-over-HTTPS redirect: " <> toLogStr doh      case _doh of         Nothing  -> runner proxy
src/Network/HProx/DoH.hs view
@@ -11,7 +11,7 @@ import Data.ByteString.Char8      qualified as BS8 import Data.ByteString.Lazy       qualified as LBS import Network.DNS-    (DNSHeader (..), DNSMessage (..), Question (..), ResolvConf (..), Resolver)+    (DNSHeader(..), DNSMessage(..), Question(..), ResolvConf(..), Resolver) import Network.DNS                qualified as DNS import Network.HTTP.Types         qualified as HT @@ -47,7 +47,7 @@         dnsQuery <- getRequestBodyChunk req         case DNS.decode dnsQuery of             Right (DNSMessage { question = [q], header = DNSHeader {..} }) -> handleQuery identifier q-            _ -> respond errorResp+            _otherwise                                                     -> respond errorResp     | otherwise = respond errorResp   where     errorResp = responseLBS HT.status400 [("Content-Type", "text/plain")] "invalid dns-over-https request"
src/Network/HProx/Impl.hs view
@@ -2,8 +2,9 @@ -- -- Copyright (C) 2023 Bin Jin. All Rights Reserved. +{-# LANGUAGE ViewPatterns #-} module Network.HProx.Impl-  ( ProxySettings (..)+  ( ProxySettings(..)   , acmeProvider   , forceSSL   , healthCheckProvider@@ -18,7 +19,7 @@ import Control.Applicative        ((<|>)) import Control.Concurrent.Async   (cancel, wait, waitEither, withAsync) import Control.Exception          (SomeException, try)-import Control.Monad              (unless, void, when)+import Control.Monad              (forM_, unless, void, when) import Control.Monad.IO.Class     (liftIO) import Data.Binary.Builder        qualified as BB import Data.ByteString            qualified as BS@@ -31,9 +32,8 @@ import Data.Text.Encoding         qualified as TE import Network.HTTP.Client        qualified as HC import Network.HTTP.ReverseProxy-    (ProxyDest (..), SetIpHeader (..), WaiProxyResponse (..),-    defaultWaiProxySettings, waiProxyToSettings, wpsSetIpHeader,-    wpsUpgradeToRaw)+    (ProxyDest(..), SetIpHeader(..), WaiProxyResponse(..), defaultWaiProxySettings,+    waiProxyToSettings, wpsSetIpHeader, wpsUpgradeToRaw) import Network.HTTP.Types         qualified as HT import Network.HTTP.Types.Header  qualified as HT import System.Timeout             (timeout)@@ -48,14 +48,14 @@ import Network.HProx.Util  data ProxySettings = ProxySettings-  { proxyAuth      :: Maybe (BS.ByteString -> Bool)-  , passPrompt     :: Maybe BS.ByteString-  , wsRemote       :: Maybe BS.ByteString-  , revRemoteMap   :: [(Maybe BS.ByteString, BS.ByteString, BS.ByteString)]-  , hideProxyAuth  :: Bool-  , naivePadding   :: Bool-  , acmeThumbprint :: Maybe BS.ByteString-  , logger         :: Logger+  { proxyAuth      :: !(Maybe (BS.ByteString -> Bool))+  , passPrompt     :: !(Maybe BS.ByteString)+  , wsRemote       :: !(Maybe BS.ByteString)+  , revRemoteMap   :: ![(Maybe BS.ByteString, BS.ByteString, BS.ByteString)]+  , hideProxyAuth  :: !Bool+  , naivePadding   :: !Bool+  , acmeThumbprint :: !(Maybe BS.ByteString)+  , logger         :: !Logger   }  logRequest :: Request -> LogStr@@ -103,17 +103,23 @@ isToStripHeader h = isProxyHeader h || isForwardedHeader h || isCDNHeader h || h == "X-Real-IP" || h == "X-Scheme"  checkAuth :: ProxySettings -> Request -> Bool-checkAuth ProxySettings{..} req-    | isNothing proxyAuth = True-    | isNothing authRsp   = False-    | otherwise           =-        pureLogger logger TRACE (authMsg <> " request (credential: " <> toLogStr decodedRsp <> ") from " <> toLogStr (show (remoteHost req))) authorized+checkAuth ProxySettings{..} req = case (proxyAuth, authRsp) of+    (Nothing, _)                -> True+    (_, Nothing)                -> False+    (Just check, Just provided) ->+        let decoded = decodeLenient $ snd $ BS8.spanEnd (/=' ') provided+            authorized = check decoded+            authMsg = if authorized then "authorized" else "unauthorized"+            logMsg = authMsg <> " request (credential: " <> toLogStr decoded <> ") from "+                             <> toLogStr (show (remoteHost req))+        in pureLogger logger TRACE logMsg authorized   where     authRsp = lookup HT.hProxyAuthorization (requestHeaders req)-    decodedRsp = decodeLenient $ snd $ BS8.spanEnd (/=' ') $ fromJust authRsp -    authorized = fromJust proxyAuth decodedRsp-    authMsg = if authorized then "authorized" else "unauthorized"+parseConnectProxy :: Request -> Maybe (BS.ByteString, Int)+parseConnectProxy req+    | requestMethod req == "CONNECT" = parseHostPort (rawPathInfo req) <|> (requestHeaderHost req >>= parseHostPort)+    | otherwise                      = Nothing  redirectWebsocket :: ProxySettings -> Request -> Bool redirectWebsocket ProxySettings{..} req = wpsUpgradeToRaw defaultWaiProxySettings req && isJust wsRemote@@ -245,10 +251,11 @@                 BS.drop (BS.length rawPathPrefix) rawPath  httpConnectProxy :: ProxySettings -> Middleware-httpConnectProxy pset@ProxySettings{..} fallback req respond-    | not isConnectProxy = fallback req respond+httpConnectProxy pset@ProxySettings{..} fallback req@(parseConnectProxy -> Just (host, port)) respond     | checkAuth pset req = do-        when (isJust mPaddingType) $ logger DEBUG $ "naiveproxy padding type detected: " <> toLogStr (show (fromJust mPaddingType)) <> " for " <> logRequest req+        forM_ mPaddingType $ \paddingType ->+            logger DEBUG $ "naiveproxy padding type detected: " <> toLogStr (show paddingType) <>+                           " for " <> logRequest req         respondResponse     | hideProxyAuth      = do         logger WARN $ "unauthorized request (hidden without response): " <> logRequest req@@ -257,10 +264,6 @@         logger WARN $ "unauthorized request: " <> logRequest req         respond (proxyAuthRequiredResponse pset)   where-    hostPort' = parseHostPort (rawPathInfo req) <|> (requestHeaderHost req >>= parseHostPort)-    isConnectProxy = requestMethod req == "CONNECT" && isJust hostPort'--    Just (host, port) = hostPort'     settings = CN.clientSettings port host      backup = responseKnownLength HT.status500 [("Content-Type", "text/plain")]@@ -275,8 +278,8 @@             withAsync right $ \r -> do                 res1 <- waitEither l r                 let unfinished = case res1 of-                        Left _ -> r-                        _      -> l+                        Left _  -> r+                        Right _ -> l                 res2 <- timeout (secs * 1000000) (wait unfinished)                 when (isNothing res2) $ cancel unfinished @@ -319,3 +322,4 @@             void $ runStreams 5                 (runConduit clientToServer)                 (runConduit serverToClient)+httpConnectProxy _ fallback req respond = fallback req respond
src/Network/HProx/Log.hs view
@@ -3,11 +3,11 @@ -- Copyright (C) 2023 Bin Jin. All Rights Reserved.  module Network.HProx.Log-  ( LogLevel (..)+  ( LogLevel(..)   , LogStr-  , LogType' (..)+  , LogType'(..)   , Logger-  , ToLogStr (..)+  , ToLogStr(..)   , logLevelReader   , pureLogger   , withLogger@@ -33,7 +33,7 @@ logLevelReader "warn"  = Just WARN logLevelReader "error" = Just ERROR logLevelReader "none"  = Just NONE-loglevelReader _       = Nothing+logLevelReader _       = Nothing  logWith :: TimedFastLogger -> LogLevel -> LogStr -> IO () logWith logger level logstr = logger (\time -> toLogStr time <> " [" <> toLogStr (show level) <> "] " <> logstr <> "\n")
src/Network/HProx/Naive.hs view
@@ -3,7 +3,7 @@ -- Copyright (C) 2023 Bin Jin. All Rights Reserved.  module Network.HProx.Naive-  ( PaddingType (..)+  ( PaddingType(..)   , addPaddingConduit   , parseRequestForPadding   , prepareResponseForPadding@@ -20,8 +20,7 @@ import Data.Maybe                (mapMaybe) import Network.HTTP.Types.Header qualified as HT import System.Random             (uniformR)-import System.Random.Stateful-    (applyAtomicGen, globalStdGen, runStateGen, uniformRM)+import System.Random.Stateful    (applyAtomicGen, globalStdGen, runStateGen, uniformRM)  import Data.Conduit import Network.Wai@@ -131,4 +130,4 @@             if LBS.length bs /= len + paddingLen                 then return ()                 else yield (LBS.toStrict $ LBS.take len bs) >> removePaddingVariant1 (n - 1)-        _ -> return ()+        _otherwise   -> return ()
src/Network/HProx/Util.hs view
@@ -3,8 +3,8 @@ -- Copyright (C) 2023 Bin Jin. All Rights Reserved.  module Network.HProx.Util-  ( Password (..)-  , PasswordSalted (..)+  ( Password(..)+  , PasswordSalted(..)   , hashPasswordWithRandomSalt   , parseHostPort   , parseHostPortWithDefault@@ -18,36 +18,38 @@ import Data.ByteString       qualified as BS import Data.ByteString.Char8 qualified as BS8 import Data.ByteString.Lazy  qualified as LBS+import Data.List.NonEmpty    (NonEmpty(..), (<|))+import Data.List.NonEmpty    qualified as NE import Data.Maybe            (fromMaybe)  import Network.HTTP.Types (ResponseHeaders, Status) import Network.Wai -import Crypto.Error           (CryptoFailable (..))+import Crypto.Error           (CryptoFailable(..)) import Crypto.KDF.Argon2      qualified as Argon2-import Crypto.Random          (MonadRandom (getRandomBytes))+import Crypto.Random          (MonadRandom(getRandomBytes)) import Data.ByteString.Base64 qualified as Base64 -data Password = PlainText BS.ByteString-              | Salted BS.ByteString BS.ByteString+data Password = PlainText !BS.ByteString+              | Salted !BS.ByteString !BS.ByteString     deriving (Show, Eq) -data PasswordSalted = PasswordSalted BS.ByteString BS.ByteString+data PasswordSalted = PasswordSalted !BS.ByteString !BS.ByteString     deriving (Show, Eq) -splitBy :: Eq a => a -> [a] -> [[a]]-splitBy _ [] = [[]]+splitBy :: Eq a => a -> [a] -> NonEmpty [a]+splitBy _ [] = NE.singleton [] splitBy c (x:xs)-  | c == x    = [] : splitBy c xs-  | otherwise = let y:ys = splitBy c xs in (x:y):ys+  | c == x    = [] <| splitBy c xs+  | otherwise = let y :| ys = splitBy c xs in (x:y) :| ys  passwordReader :: BS.ByteString -> Maybe (BS.ByteString, Password) passwordReader line = case BS8.split ':' line of     [user, pass]         -> Just (user, PlainText pass)     [user, salt, hashed] -> case (Base64.decode salt, Base64.decode hashed) of                                 (Right salt', Right hashed') -> Just (user, Salted salt' hashed')-                                _                            -> Nothing-    _                    -> Nothing+                                _otherwise                   -> Nothing+    _otherwise           -> Nothing  passwordWriter :: BS.ByteString -> PasswordSalted -> BS.ByteString passwordWriter user (PasswordSalted salt hash) =