hprox 0.6.0 → 0.6.1
raw patch · 5 files changed
+107/−56 lines, 5 filesdep +data-default-classdep +textdep +unixdep ~http-reverse-proxydep ~quicPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: data-default-class, text, unix
Dependency ranges changed: http-reverse-proxy, quic
API changes (from Hackage documentation)
+ Network.HProx: [_acme] :: Config -> Maybe ByteString
- Network.HProx: Config :: Maybe String -> Int -> [(String, CertFile)] -> Maybe FilePath -> Maybe String -> [(Maybe ByteString, ByteString, ByteString)] -> Maybe String -> Bool -> Bool -> 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 -> Config
- Network.HProx: [_ws] :: Config -> Maybe String
+ Network.HProx: [_ws] :: Config -> Maybe ByteString
Files
- Changelog.md +7/−1
- README.md +19/−23
- hprox.cabal +17/−5
- src/Network/HProx.hs +42/−20
- src/Network/HProx/Impl.hs +22/−7
Changelog.md view
@@ -1,7 +1,13 @@+## 0.6.1++- multiple certificates and SNI support for HTTP/3+- install signal handler with graceful shutdown on Linux and macOS+- support ACME `http-01` challenge (RFC8555)+ ## 0.6.0 - `--rev` now supports domain matching-- fix `Content-Length` header in HTTP/2 responses.+- fix `Content-Length` header in HTTP/2 responses - passwords are now Argon2 salt-hashed ## 0.5.4
README.md view
@@ -12,60 +12,61 @@ ### Features * Basic HTTP proxy functionality.-* Simple password authentication.-* TLS encryption (requires a valid certificate). Supports TLS 1.3 and HTTP/2, also known as SPDY Proxy.-* TLS SNI validation (blocks all clients with invalid domain name).-* Provide PAC file for easy client side configuration (supports Chrome and Firefox).+* [Basic](https://en.wikipedia.org/wiki/Basic_access_authentication) password authentication.+* Enables TLS encryption, requiring a valid certificate. Supports TLS 1.3 and HTTP/2, also known as SPDY Proxy.+* TLS SNI validation (blocks all clients with an invalid domain name).+* Provides a PAC file for seamless client-side configuration, compatible with browsers like Chrome and Firefox. * Websocket redirection (compatible with [v2ray-plugin](https://github.com/shadowsocks/v2ray-plugin)).-* Reverse proxy support (redirect requests to a fallback server).+* Reverse proxy support (redirects requests to a fallback server). * DNS-over-HTTPS (DoH) support. * [naiveproxy](https://github.com/klzgrad/naiveproxy) compatible [padding](https://github.com/klzgrad/naiveproxy/#padding-protocol-an-informal-specification) (HTTP Connect proxy). * HTTP/3 (QUIC) support (`h3` protocol).-* Implemented as a middleware, compatible with any Haskell Web Application built with `wai` interface.- See [library documents](https://hackage.haskell.org/package/hprox) for details.+* ACME `http-01` challenge as specified by RFC8555, see [acme.sh stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode).+* Designed as a middleware, ensuring compatibility with any Haskell Web Application built using the `wai` interface.+ Refer to [library documents](https://hackage.haskell.org/package/hprox) for details. ### Installation -`hprox` should build and work on all unix-like OS with `ghc` support, as well as Windows.+`hprox` is designed to build and function seamlessly on all Unix-like operating systems with GHC support, as well as on Windows environments. -[stack](https://docs.haskellstack.org/en/stable/README/#how-to-install) is recommended to build `hprox`.+[stack](https://docs.haskellstack.org/en/stable/README/#how-to-install) is recommended for building `hprox`. ```sh stack setup stack install ``` -Alternatively, you also can use the statically linked binary for the [latest release](https://github.com/bjin/hprox/releases).+Alternatively, you have the option to utilize the statically linked binary available in the [latest release](https://github.com/bjin/hprox/releases). ### Usage -Use `hprox --help` to list options with detailed explanation.+Utilize `hprox --help` to view a comprehensive list of options along with detailed explanations. -* To run `hprox` on port 8080, with simple password authentication:+* To run `hprox` on port 8080 with simple password authentication (passwords will be [Argon2-hashed](https://en.wikipedia.org/wiki/Argon2) after first run): ```sh echo "user:pass" > userpass.txt hprox -p 8080 -a userpass.txt ``` -* To run `hprox` with TLS encryption on port 443, with certificate of `example.com` obtained with [acme.sh](https://acme.sh/):+* To run `hprox` with TLS encryption on port 443, with a certificate for `example.com` obtained with [acme.sh](https://acme.sh/): ```sh hprox -p 443 -s example.com:$HOME/.acme.sh/example.com/fullchain.cer:$HOME/.acme.sh/example.com/example.com.key ``` -Browsers can be configured with PAC file URL `https://example.com/.hprox/config.pac`.+Browsers can be configured with the PAC file URL `https://example.com/.hprox/config.pac`. -* To work with `v2ray-plugin`, with fallback page to [ubuntu archive](http://archive.ubuntu.com/):+* For integration with `v2ray-plugin` and a fallback page to the [Ubuntu archive](http://archive.ubuntu.com/): ```sh v2ray-plugin -server -localPort 8080 -mode websocket -host example.com -remotePort xxxx hprox -p 443 -s example.com:fullchain.pem:privkey.pem --ws 127.0.0.1:8080 --rev archive.ubuntu.com:80 ``` -Clients will be able to connect with plugin option `tls;host=example.com`.+Clients can establish connections using the plugin option `tls;host=example.com`. -* Enable HTTP/3 (QUIC) on UDP port 8443, enable DoH support (redirect to 8.8.8.8), and add `naiveproxy` compatible padding:+* Enable HTTP/3 (QUIC) on UDP port 8443, enable DoH support (redirect to 8.8.8.8), and add naiveproxy compatible padding: ```sh hprox -p 443 -q 8443 -s example.com:fullchain.pem:privkey.pem -a userpass.txt --naive --doh 8.8.8.8@@ -73,11 +74,6 @@ Then DoH can be accessed at `https://example.com/dns-query`. -### Known Issue--* HTTP/3 currently only works on the first domain as specified by `-s/--tls`, also SNI- validation is unavailable as well.- ### License -`hprox` is licensed under the Apache license. See LICENSE file for details.+`hprox` is licensed under the Apache license. Refer to the `LICENSE` file for comprehensive details.
hprox.cabal view
@@ -5,7 +5,7 @@ -- see: https://github.com/sol/hpack name: hprox-version: 0.6.0+version: 0.6.1 synopsis: a lightweight HTTP proxy server, and more description: Please see the README on GitHub at <https://github.com/bjin/hprox#readme> category: Web@@ -62,15 +62,17 @@ , conduit >=1.3 , conduit-extra >=1.3 , crypton+ , data-default-class , dns >=4.0 , fast-logger >=3.0 , http-client >=0.5 , http-client-tls >=0.3.4- , http-reverse-proxy >=0.4.0+ , 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@@ -83,8 +85,12 @@ cpp-options: -DQUIC_ENABLED build-depends: http3 >=0.0.3- , quic >=0.1.1+ , quic >=0.1.15 , warp-quic+ if !os(windows)+ cpp-options: -DOS_UNIX+ build-depends:+ unix executable hprox main-is: Main.hs@@ -107,16 +113,18 @@ , conduit >=1.3 , conduit-extra >=1.3 , crypton+ , data-default-class , dns >=4.0 , fast-logger >=3.0 , hprox , http-client >=0.5 , http-client-tls >=0.3.4- , http-reverse-proxy >=0.4.0+ , 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@@ -129,7 +137,11 @@ cpp-options: -DQUIC_ENABLED build-depends: http3 >=0.0.3- , quic >=0.1.1+ , 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
@@ -19,7 +19,10 @@ ) where 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.String (fromString) import Data.Version (showVersion)@@ -34,8 +37,9 @@ runSettings, setHost, setLogger, setNoParsePath, setOnException, setPort, setServerName) import Network.Wai.Handler.WarpTLS- (OnInsecure (..), WarpTLSException, onInsecure, runTLS, tlsAllowedVersions,- tlsCiphers, tlsServerHooks, tlsSessionManager, tlsSettings)+ (OnInsecure (..), WarpTLSException, defaultTlsSettings, onInsecure, runTLS,+ tlsAllowedVersions, tlsCiphers, tlsCredentials, tlsServerHooks,+ tlsSessionManager) import Control.Exception (Exception (..)) import GHC.IO.Exception (IOErrorType (..))@@ -50,8 +54,13 @@ import Network.Wai.Handler.WarpQUIC (runQUIC) #endif +#ifdef OS_UNIX+import Network.Wai.Handler.Warp+ (setGracefulShutdownTimeout, setInstallShutdownHandler)+import System.Posix.Signals+#endif+ import Control.Monad-import Data.List import Data.Maybe import Options.Applicative @@ -67,12 +76,13 @@ , _port :: Int , _ssl :: [(String, CertFile)] , _auth :: Maybe FilePath- , _ws :: Maybe String+ , _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 QUIC_ENABLED@@ -82,7 +92,7 @@ -- | Default value of 'Config', same as running @hprox@ without arguments defaultConfig :: Config-defaultConfig = Config Nothing 3000 [] Nothing Nothing [] Nothing False False "hprox" "stdout" INFO+defaultConfig = Config Nothing 3000 [] Nothing Nothing [] Nothing False False "hprox" Nothing "stdout" INFO #ifdef QUIC_ENABLED Nothing #endif@@ -130,6 +140,7 @@ <*> hide <*> naive <*> name+ <*> acme <*> logging <*> loglevel #ifdef QUIC_ENABLED@@ -192,6 +203,11 @@ <> 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>"@@ -237,21 +253,29 @@ smgr <- SM.newSessionManager SM.defaultConfig let isSSL = not (null certfiles)- (primaryHost, primaryCert) = head certfiles- otherCerts = tail $ zip (map fst certfiles) certs+ allCerts = zip (map fst certfiles) certs when isSSL $ do logger INFO $ "read " <> toLogStr (show $ length certs) <> " certificates"- logger INFO $ "primary domain: " <> toLogStr primaryHost- logger INFO $ "other domains: " <> toLogStr (unwords $ map fst otherCerts)+ logger INFO $ "domains: " <> toLogStr (unwords $ map fst allCerts) let settings = setHost (fromString (fromMaybe "*6" _bind)) $ setPort _port $ setLogger warpLogger $ setOnException exceptionHandler $+#ifdef OS_UNIX+ setGracefulShutdownTimeout (Just 3) $+ setInstallShutdownHandler shutdownHandler $+#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+#endif+ exceptionHandler req ex | _loglevel > DEBUG = return () | not (defaultShouldDisplayException ex) = return ()@@ -273,9 +297,6 @@ | otherwise = logger TRACE $ "(" <> toLogStr (HT.statusCode status) <> ") " <> logRequest req - tlsset' = tlsSettings (certfile primaryCert) (keyfile primaryCert)- hooks = (tlsServerHooks tlsset') { TLS.onServerNameIndication = onSNI }- -- https://www.ssllabs.com/ssltest weak_ciphers = [ TLS.cipher_ECDHE_RSA_AES256CBC_SHA384 , TLS.cipher_ECDHE_RSA_AES256CBC_SHA@@ -285,18 +306,17 @@ , TLS.cipher_AES256_SHA1 ] - tlsset = tlsset'- { tlsServerHooks = hooks+ tlsset = defaultTlsSettings+ { tlsServerHooks = def { TLS.onServerNameIndication = onSNI }+ , tlsCredentials = Just (TLS.Credentials certs) , onInsecure = AllowInsecure , tlsAllowedVersions = [TLS.TLS13, TLS.TLS12] , tlsCiphers = TLS.ciphersuite_strong \\ weak_ciphers , tlsSessionManager = Just smgr } - onSNI Nothing = fail "SNI: unspecified"- onSNI (Just host)- | checkSNI host primaryHost = return mempty- | otherwise = lookupSNI host otherCerts+ onSNI Nothing = fail "SNI: unspecified"+ onSNI (Just host) = lookupSNI host allCerts lookupSNI host [] = fail ("SNI: unknown hostname (" ++ show host ++ ")") lookupSNI host ((p, cert) : cs)@@ -314,9 +334,10 @@ 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 [head certs]+ , Q.scCredentials = TLS.Credentials certs , Q.scCiphers = Q.scCiphers Q.defaultServerConfig \\ weak_ciphers , Q.scALPN = Just alpn+ , Q.scTlsHooks = def { TLS.onServerNameIndication = onSNI } , Q.scUse0RTT = True , Q.scSessionManager = smgr }@@ -363,8 +384,9 @@ manager <- newTlsManager let revSorted = sortOn (\(a,b,_) -> Down (isJust a, BS8.length b)) _rev- pset = ProxySettings pauth (Just _name) (BS8.pack <$> _ws) revSorted _hide (_naive && isSSL) logger+ pset = ProxySettings pauth (Just _name) _ws revSorted _hide (_naive && isSSL) _acme logger proxy = healthCheckProvider $+ acmeProvider pset $ (if isSSL then forceSSL pset else id) $ httpProxy pset manager $ reverseProxy pset manager fallback
src/Network/HProx/Impl.hs view
@@ -4,6 +4,7 @@ module Network.HProx.Impl ( ProxySettings (..)+ , acmeProvider , forceSSL , healthCheckProvider , httpConnectProxy@@ -27,6 +28,7 @@ import Data.ByteString.Lazy.Char8 qualified as LBS8 import Data.CaseInsensitive qualified as CI import Data.Conduit.Network qualified as CN+import Data.Text.Encoding qualified as TE import Network.HTTP.Client qualified as HC import Network.HTTP.ReverseProxy (ProxyDest (..), SetIpHeader (..), WaiProxyResponse (..),@@ -46,13 +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- , 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@@ -122,6 +125,18 @@ "" where prompt = fromMaybe "hprox" passPrompt++acmeProvider :: ProxySettings -> Middleware+acmeProvider ProxySettings{..} app req respond+ | not (isSecure req)+ , Just thumbprint <- acmeThumbprint+ , [".well-known", "acme-challenge", token] <- pathInfo req+ = respond $ responseKnownLength+ HT.status200+ [("Content-Type", "text/plain")] $+ LBS.fromChunks [TE.encodeUtf8 token, ".", thumbprint]+ | otherwise+ = app req respond pacProvider :: Middleware pacProvider fallback req respond