hprox (empty) → 0.1.0
raw patch · 6 files changed
+729/−0 lines, 6 filesdep +asyncdep +basedep +base64-bytestringsetup-changed
Dependencies added: async, base, base64-bytestring, binary, bytestring, case-insensitive, conduit, conduit-extra, http-client, http-reverse-proxy, http-types, optparse-applicative, tls, unix, wai, wai-extra, warp, warp-tls
Files
+ HProx.hs view
@@ -0,0 +1,287 @@+-- SPDX-License-Identifier: Apache-2.0+--+-- Copyright (C) 2019 Bin Jin. All Rights Reserved.+{-# LANGUAGE OverloadedStrings #-}++module HProx+ ( ProxySettings(..)+ , httpProxy+ , pacProvider+ , httpGetProxy+ , httpConnectProxy+ , reverseProxy+ , forceSSL+ , dumbApp+ ) where++import Control.Applicative ((<|>))+import Control.Concurrent.Async (concurrently)+import Control.Exception (SomeException, try)+import Control.Monad (unless, void, when)+import Control.Monad.IO.Class (liftIO)+import qualified Data.Binary.Builder as BB+import qualified Data.ByteString as BS+import Data.ByteString.Base64 (decodeLenient)+import qualified Data.ByteString.Char8 as BS8+import qualified Data.ByteString.Lazy.Char8 as LBS8+import qualified Data.CaseInsensitive as CI+import qualified Data.Conduit.Network as CN+import Data.Maybe (fromJust, fromMaybe, isJust,+ isNothing)+import qualified Network.HTTP.Client as HC+import Network.HTTP.ReverseProxy (ProxyDest (..), SetIpHeader (..),+ WaiProxyResponse (..),+ defaultWaiProxySettings,+ waiProxyToSettings, wpsSetIpHeader,+ wpsUpgradeToRaw)+import qualified Network.HTTP.Types as HT+import qualified Network.HTTP.Types.Header as HT+import Network.Wai.Internal (getRequestBodyChunk)++import Data.Conduit+import Network.Wai++data ProxySettings = ProxySettings+ { proxyAuth :: Maybe (BS.ByteString -> Bool)+ , passPrompt :: Maybe BS.ByteString+ , wsRemote :: Maybe BS.ByteString+ , revRemote :: Maybe BS.ByteString+ }++dumbApp :: Application+dumbApp _req respond =+ respond $ responseLBS+ HT.status200+ [("Content-Type", "text/html")] $+ LBS8.unlines [ "<html><body><h1>It works!</h1>"+ , "<p>This is the default web page for this server.</p>"+ , "<p>The web server software is running but no content has been added, yet.</p>"+ , "</body></html>"+ ]++httpProxy :: ProxySettings -> HC.Manager -> Middleware+httpProxy set mgr = pacProvider . httpGetProxy set mgr . httpConnectProxy set++forceSSL :: Middleware+forceSSL app req respond+ | isSecure req = app req respond+ | otherwise = redirectToSSL req respond++redirectToSSL :: Application+redirectToSSL req respond+ | Just host <- requestHeaderHost req = respond $ responseLBS+ HT.status301+ [("Location", "https://" `BS.append` host)]+ ""+ | otherwise = respond $ responseLBS+ (HT.mkStatus 426 "Upgrade Required")+ [("Upgrade", "TLS/1.0, HTTP/1.1"), ("Connection", "Upgrade")]+ ""++parseHostPort :: BS.ByteString -> Maybe (BS.ByteString, Int)+parseHostPort hostPort = do+ lastColon <- BS8.elemIndexEnd ':' hostPort+ port <- BS8.readInt (BS.drop (lastColon+1) hostPort) >>= checkPort+ return (BS.take lastColon hostPort, port)+ where+ checkPort (p, bs)+ | BS.null bs && 1 <= p && p <= 65535 = Just p+ | otherwise = Nothing++parseHostPortWithDefault :: Int -> BS.ByteString -> (BS.ByteString, Int)+parseHostPortWithDefault defaultPort hostPort =+ fromMaybe (hostPort, defaultPort) $ parseHostPort hostPort++isProxyHeader :: HT.HeaderName -> Bool+isProxyHeader k+ | BS.length bs <= 4 = False+ | c0 /= 112 && c0 /= 80 = False -- 'p'+ | c1 /= 114 && c1 /= 82 = False -- 'r'+ | c2 /= 111 && c2 /= 79 = False -- 'o'+ | c3 /= 120 && c3 /= 88 = False -- 'x'+ | c4 /= 121 && c4 /= 89 = False -- 'y'+ | otherwise = True+ where+ bs = CI.original k+ idx = BS.index bs++ c0 = idx 0+ c1 = idx 1+ c2 = idx 2+ c3 = idx 3+ c4 = idx 4++isForwardedHeader :: HT.HeaderName -> Bool+isForwardedHeader k+ | BS.length bs <= 10 = False+ | c0 /= 120 && c0 /= 88 = False -- 'x'+ | c1 /= 45 = False -- '-'+ | c2 /= 102 && c2 /= 70 = False -- 'f'+ | c3 /= 111 && c3 /= 79 = False -- 'o'+ | c4 /= 114 && c4 /= 82 = False -- 'r'+ | c5 /= 119 && c5 /= 87 = False -- 'w'+ | c6 /= 97 && c6 /= 65 = False -- 'a'+ | c7 /= 114 && c7 /= 82 = False -- 'r'+ | c8 /= 100 && c8 /= 68 = False -- 'd'+ | c9 /= 101 && c9 /= 69 = False -- 'e'+ | ca /= 100 && ca /= 68 = False -- 'd'+ | otherwise = True+ where+ bs = CI.original k+ idx = BS.index bs++ c0 = idx 0+ c1 = idx 1+ c2 = idx 2+ c3 = idx 3+ c4 = idx 4+ c5 = idx 5+ c6 = idx 6+ c7 = idx 7+ c8 = idx 8+ c9 = idx 9+ ca = idx 10++isToStripHeader :: HT.HeaderName -> Bool+isToStripHeader h = isProxyHeader h || isForwardedHeader h || h == "X-Real-IP" || h == "X-Scheme"++checkAuth :: ProxySettings -> Request -> Bool+checkAuth pset req+ | isNothing pauth = True+ | isNothing authRsp = False+ | otherwise = fromJust pauth decodedRsp+ where+ pauth = proxyAuth pset+ authRsp = lookup HT.hProxyAuthorization (requestHeaders req)++ decodedRsp = decodeLenient $ snd $ BS8.spanEnd (/=' ') $ fromJust authRsp++proxyAuthRequiredResponse :: ProxySettings -> Response+proxyAuthRequiredResponse pset = responseLBS+ HT.status407+ [(HT.hProxyAuthenticate, "Basic realm=\"" `BS.append` prompt `BS.append` "\"")]+ ""+ where+ prompt = fromMaybe "hprox" (passPrompt pset)++pacProvider :: Middleware+pacProvider fallback req respond+ | pathInfo req == ["get", "hprox.pac"],+ Just host' <- lookup "x-forwarded-host" (requestHeaders req) <|> requestHeaderHost req =+ let issecure = case lookup "x-forwarded-proto" (requestHeaders req) of+ Just proto -> proto == "https"+ Nothing -> isSecure req+ scheme = if issecure then "HTTPS" else "PROXY"+ defaultPort = if issecure then ":443" else ":80"+ host | 58 `BS.elem` host' = host' -- ':'+ | otherwise = host' `BS.append` defaultPort+ in respond $ responseLBS+ HT.status200+ [("Content-Type", "application/x-ns-proxy-autoconfig")] $+ LBS8.unlines [ "function FindProxyForURL(url, host) {"+ , LBS8.fromChunks [" return \"", scheme, " ", host, "\";"]+ , "}"+ ]+ | otherwise = fallback req respond++reverseProxy :: ProxySettings -> HC.Manager -> Middleware+reverseProxy pset mgr fallback+ | isReverseProxy = waiProxyToSettings (return.proxyResponseFor) settings mgr+ | otherwise = fallback+ where+ settings = defaultWaiProxySettings { wpsSetIpHeader = SIHNone }++ isReverseProxy = isJust (revRemote pset)+ (revHost, revPort) = parseHostPortWithDefault 80 (fromJust (revRemote pset))++ proxyResponseFor req = WPRModifiedRequest nreq (ProxyDest revHost revPort)+ where+ nreq = req+ { requestHeaders = hdrs+ , requestHeaderHost = Just revHost+ }++ hdrs = (HT.hHost, revHost) : [ (hdn, hdv)+ | (hdn, hdv) <- requestHeaders req+ , not (isToStripHeader hdn) && hdn /= HT.hHost+ ]++httpGetProxy :: ProxySettings -> HC.Manager -> Middleware+httpGetProxy pset mgr fallback = waiProxyToSettings (return.proxyResponseFor) settings mgr+ where+ settings = defaultWaiProxySettings { wpsSetIpHeader = SIHNone }++ proxyResponseFor req+ | redirectWebsocket = WPRProxyDest (ProxyDest wsHost wsPort)+ | not isGetProxy = WPRApplication fallback+ | checkAuth pset req = WPRModifiedRequest nreq (ProxyDest host port)+ | otherwise = WPRResponse (proxyAuthRequiredResponse pset)+ where+ isWebsocket = wpsUpgradeToRaw defaultWaiProxySettings req+ redirectWebsocket = isWebsocket && isJust (wsRemote pset)+ (wsHost, wsPort) = parseHostPortWithDefault 80 (fromJust (wsRemote pset))++ notCONNECT = requestMethod req /= "CONNECT"+ rawPath = rawPathInfo req+ rawPathPrefix = "http://"+ defaultPort = 80+ hostHeader = parseHostPortWithDefault defaultPort <$> requestHeaderHost req++ isRawPathProxy = rawPathPrefix `BS.isPrefixOf` rawPath+ hasProxyHeader = any (isProxyHeader.fst) (requestHeaders req)+ scheme = lookup "X-Scheme" (requestHeaders req)+ isHTTP2Proxy = HT.httpMajor (httpVersion req) >= 2 && scheme == Just "http" && isSecure req++ isGetProxy = notCONNECT && (isRawPathProxy || isHTTP2Proxy || isJust hostHeader && hasProxyHeader)++ nreq = req+ { rawPathInfo = newRawPath+ , requestHeaders = filter (not.isToStripHeader.fst) $ requestHeaders req+ }++ ((host, port), newRawPath)+ | isRawPathProxy = (parseHostPortWithDefault defaultPort hostPortP, newRawPathP)+ | otherwise = (fromJust hostHeader, rawPath)+ where+ (hostPortP, newRawPathP) = BS8.span (/='/') $+ BS.drop (BS.length rawPathPrefix) rawPath++httpConnectProxy :: ProxySettings -> Middleware+httpConnectProxy pset fallback req respond+ | not isConnectProxy = fallback req respond+ | checkAuth pset req = respond response+ | otherwise = 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 = responseLBS HT.status500 [("Content-Type", "text/plain")]+ "HTTP CONNECT tunneling detected, but server does not support responseRaw"++ tryAndCatchAll :: IO a -> IO (Either SomeException a)+ tryAndCatchAll = try++ response+ | HT.httpMajor (httpVersion req) < 2 = responseRaw (handleConnect True) backup+ | otherwise = responseStream HT.status200 [] streaming+ where+ streaming write flush = do+ flush+ handleConnect False (getRequestBodyChunk req) (\bs -> write (BB.fromByteString bs) >> flush)++ handleConnect :: Bool -> IO BS.ByteString -> (BS.ByteString -> IO ()) -> IO ()+ handleConnect http1 fromClient' toClient' = CN.runTCPClient settings $ \server ->+ let toServer = CN.appSink server+ fromServer = CN.appSource server+ fromClient = do+ bs <- liftIO fromClient'+ unless (BS.null bs) (yield bs >> fromClient)+ toClient = awaitForever (liftIO . toClient')+ in do+ when http1 $ runConduit $ yield "HTTP/1.1 200 OK\r\n\r\n" .| toClient+ void $ tryAndCatchAll $ concurrently+ (runConduit (fromClient .| toServer))+ (runConduit (fromServer .| toClient))
+ LICENSE view
@@ -0,0 +1,202 @@++ Apache License+ Version 2.0, January 2004+ http://www.apache.org/licenses/++ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION++ 1. Definitions.++ "License" shall mean the terms and conditions for use, reproduction,+ and distribution as defined by Sections 1 through 9 of this document.++ "Licensor" shall mean the copyright owner or entity authorized by+ the copyright owner that is granting the License.++ "Legal Entity" shall mean the union of the acting entity and all+ other entities that control, are controlled by, or are under common+ control with that entity. For the purposes of this definition,+ "control" means (i) the power, direct or indirect, to cause the+ direction or management of such entity, whether by contract or+ otherwise, or (ii) ownership of fifty percent (50%) or more of the+ outstanding shares, or (iii) beneficial ownership of such entity.++ "You" (or "Your") shall mean an individual or Legal Entity+ exercising permissions granted by this License.++ "Source" form shall mean the preferred form for making modifications,+ including but not limited to software source code, documentation+ source, and configuration files.++ "Object" form shall mean any form resulting from mechanical+ transformation or translation of a Source form, including but+ not limited to compiled object code, generated documentation,+ and conversions to other media types.++ "Work" shall mean the work of authorship, whether in Source or+ Object form, made available under the License, as indicated by a+ copyright notice that is included in or attached to the work+ (an example is provided in the Appendix below).++ "Derivative Works" shall mean any work, whether in Source or Object+ form, that is based on (or derived from) the Work and for which the+ editorial revisions, annotations, elaborations, or other modifications+ represent, as a whole, an original work of authorship. For the purposes+ of this License, Derivative Works shall not include works that remain+ separable from, or merely link (or bind by name) to the interfaces of,+ the Work and Derivative Works thereof.++ "Contribution" shall mean any work of authorship, including+ the original version of the Work and any modifications or additions+ to that Work or Derivative Works thereof, that is intentionally+ submitted to Licensor for inclusion in the Work by the copyright owner+ or by an individual or Legal Entity authorized to submit on behalf of+ the copyright owner. For the purposes of this definition, "submitted"+ means any form of electronic, verbal, or written communication sent+ to the Licensor or its representatives, including but not limited to+ communication on electronic mailing lists, source code control systems,+ and issue tracking systems that are managed by, or on behalf of, the+ Licensor for the purpose of discussing and improving the Work, but+ excluding communication that is conspicuously marked or otherwise+ designated in writing by the copyright owner as "Not a Contribution."++ "Contributor" shall mean Licensor and any individual or Legal Entity+ on behalf of whom a Contribution has been received by Licensor and+ subsequently incorporated within the Work.++ 2. Grant of Copyright License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ copyright license to reproduce, prepare Derivative Works of,+ publicly display, publicly perform, sublicense, and distribute the+ Work and such Derivative Works in Source or Object form.++ 3. Grant of Patent License. Subject to the terms and conditions of+ this License, each Contributor hereby grants to You a perpetual,+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable+ (except as stated in this section) patent license to make, have made,+ use, offer to sell, sell, import, and otherwise transfer the Work,+ where such license applies only to those patent claims licensable+ by such Contributor that are necessarily infringed by their+ Contribution(s) alone or by combination of their Contribution(s)+ with the Work to which such Contribution(s) was submitted. If You+ institute patent litigation against any entity (including a+ cross-claim or counterclaim in a lawsuit) alleging that the Work+ or a Contribution incorporated within the Work constitutes direct+ or contributory patent infringement, then any patent licenses+ granted to You under this License for that Work shall terminate+ as of the date such litigation is filed.++ 4. Redistribution. You may reproduce and distribute copies of the+ Work or Derivative Works thereof in any medium, with or without+ modifications, and in Source or Object form, provided that You+ meet the following conditions:++ (a) You must give any other recipients of the Work or+ Derivative Works a copy of this License; and++ (b) You must cause any modified files to carry prominent notices+ stating that You changed the files; and++ (c) You must retain, in the Source form of any Derivative Works+ that You distribute, all copyright, patent, trademark, and+ attribution notices from the Source form of the Work,+ excluding those notices that do not pertain to any part of+ the Derivative Works; and++ (d) If the Work includes a "NOTICE" text file as part of its+ distribution, then any Derivative Works that You distribute must+ include a readable copy of the attribution notices contained+ within such NOTICE file, excluding those notices that do not+ pertain to any part of the Derivative Works, in at least one+ of the following places: within a NOTICE text file distributed+ as part of the Derivative Works; within the Source form or+ documentation, if provided along with the Derivative Works; or,+ within a display generated by the Derivative Works, if and+ wherever such third-party notices normally appear. The contents+ of the NOTICE file are for informational purposes only and+ do not modify the License. You may add Your own attribution+ notices within Derivative Works that You distribute, alongside+ or as an addendum to the NOTICE text from the Work, provided+ that such additional attribution notices cannot be construed+ as modifying the License.++ You may add Your own copyright statement to Your modifications and+ may provide additional or different license terms and conditions+ for use, reproduction, or distribution of Your modifications, or+ for any such Derivative Works as a whole, provided Your use,+ reproduction, and distribution of the Work otherwise complies with+ the conditions stated in this License.++ 5. Submission of Contributions. Unless You explicitly state otherwise,+ any Contribution intentionally submitted for inclusion in the Work+ by You to the Licensor shall be under the terms and conditions of+ this License, without any additional terms or conditions.+ Notwithstanding the above, nothing herein shall supersede or modify+ the terms of any separate license agreement you may have executed+ with Licensor regarding such Contributions.++ 6. Trademarks. This License does not grant permission to use the trade+ names, trademarks, service marks, or product names of the Licensor,+ except as required for reasonable and customary use in describing the+ origin of the Work and reproducing the content of the NOTICE file.++ 7. Disclaimer of Warranty. Unless required by applicable law or+ agreed to in writing, Licensor provides the Work (and each+ Contributor provides its Contributions) on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or+ implied, including, without limitation, any warranties or conditions+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A+ PARTICULAR PURPOSE. You are solely responsible for determining the+ appropriateness of using or redistributing the Work and assume any+ risks associated with Your exercise of permissions under this License.++ 8. Limitation of Liability. In no event and under no legal theory,+ whether in tort (including negligence), contract, or otherwise,+ unless required by applicable law (such as deliberate and grossly+ negligent acts) or agreed to in writing, shall any Contributor be+ liable to You for damages, including any direct, indirect, special,+ incidental, or consequential damages of any character arising as a+ result of this License or out of the use or inability to use the+ Work (including but not limited to damages for loss of goodwill,+ work stoppage, computer failure or malfunction, or any and all+ other commercial damages or losses), even if such Contributor+ has been advised of the possibility of such damages.++ 9. Accepting Warranty or Additional Liability. While redistributing+ the Work or Derivative Works thereof, You may choose to offer,+ and charge a fee for, acceptance of support, warranty, indemnity,+ or other liability obligations and/or rights consistent with this+ License. However, in accepting such obligations, You may act only+ on Your own behalf and on Your sole responsibility, not on behalf+ of any other Contributor, and only if You agree to indemnify,+ defend, and hold each Contributor harmless for any liability+ incurred by, or claims asserted against, such Contributor by reason+ of your accepting any such warranty or additional liability.++ END OF TERMS AND CONDITIONS++ APPENDIX: How to apply the Apache License to your work.++ To apply the Apache License to your work, attach the following+ boilerplate notice, with the fields enclosed by brackets "[]"+ replaced with your own identifying information. (Don't include+ the brackets!) The text should be enclosed in the appropriate+ comment syntax for the file format. We also recommend that a+ file or class name and description of purpose be included on the+ same "printed page" as the copyright notice for easier+ identification within third-party archives.++ Copyright 2019 Bin Jin++ Licensed under the Apache License, Version 2.0 (the "License");+ you may not use this file except in compliance with the License.+ You may obtain a copy of the License at++ http://www.apache.org/licenses/LICENSE-2.0++ Unless required by applicable law or agreed to in writing, software+ distributed under the License is distributed on an "AS IS" BASIS,+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.+ See the License for the specific language governing permissions and+ limitations under the License.
+ Main.hs view
@@ -0,0 +1,153 @@+-- 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
+ README.md view
@@ -0,0 +1,31 @@+## hprox++hprox is a lightweight HTTP/HTTPS proxy server.++### Features++* Basic HTTP proxy support, including HTTP GET/HTTP CONNECT support.+* Simple password authentication.+* HTTPS encryption support, requires a valid certificate. Supports TLS 1.3 and+ HTTP 2 out of box. This mode is also known as SPDY Proxy.+* TLS SNI validation in HTTPS mode. Blocks connections with wrong domain name.+* Provide PAC file for easy client side configuration. Supports Chrome and Firefox.+* Can run upon any Haskell Web Application with `wai` interface. Defaults to+ a dumb application which simulate the default empty page from Apache.+* websocket redirection. Compatible with v2ray-plugin for shadowsocks.+* Reverse proxy support. Redirect requests to a fallback server.++Use `hprox --help` to list the options for further details.++### Installation++Only Linux and macOS are supported. [stack](https://docs.haskellstack.org/en/stable/README/#how-to-install) is required to build `hprox`.++```sh+stack install+```++### Known Issue++* Only HTTP server are supported for websocket and reverse proxy redirection.+* Passwords are stored in plain text for now, please avoid using existing password.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hprox.cabal view
@@ -0,0 +1,54 @@+name: hprox+version: 0.1.0+synopsis: a lightweight HTTP proxy server, and more+description:+ Please see the README on GitHub at <https://github.com/bjin/hprox#readme>++license: Apache-2.0+license-file: LICENSE+author: Bin Jin+maintainer: bjin@ctrl-d.org+category: Web+build-type: Simple+cabal-version: >=1.10++extra-source-files:+ README.md++flag static+ description:+ Enable static build+ Default:+ False++executable hprox+ main-is:+ Main.hs+ ghc-options:+ -Wall -O2 -threaded -rtsopts "-with-rtsopts=-N -c"+ if flag(static)+ ghc-options:+ -optl-static+ other-modules:+ HProx+ build-depends:+ async,+ base < 5.0,+ base64-bytestring,+ binary,+ bytestring,+ case-insensitive,+ conduit,+ conduit-extra,+ http-client,+ http-reverse-proxy >= 0.4.0,+ http-types,+ optparse-applicative,+ tls >= 1.5.0,+ unix,+ wai >= 3.2.2,+ wai-extra,+ warp >= 3.2.8,+ warp-tls >= 3.2.5+ default-language:+ Haskell2010