{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StrictData #-} {-# LANGUAGE ViewPatterns #-} module HttpProxy () where import ClassyPrelude import qualified Data.ByteString.Char8 as BC import Control.Monad.Except import qualified Data.Conduit.Network.TLS as N import qualified Data.Streaming.Network as N import qualified Data.ByteString.Base64 as B64 import Network.Socket (HostName, PortNumber) import qualified Network.Socket as N hiding (recv, recvFrom, send, sendTo) import qualified Network.Socket.ByteString as N import Logger import Types data HttpProxySettings = HttpProxySettings { proxyHost :: HostName , proxyPort :: PortNumber , credentials :: Maybe (ByteString, ByteString) } deriving (Show) httpProxyConnection :: MonadError Error m => HttpProxySettings -> (HostName, PortNumber) -> (Connection -> IO (m a)) -> IO (m a) httpProxyConnection HttpProxySettings{..} (host, port) app = onError $ do debug $ "Opening tcp connection to proxy " <> show proxyHost <> ":" <> show proxyPort ret <- N.runTCPClient (N.clientSettingsTCP (fromIntegral proxyPort) (fromString proxyHost)) $ \conn' -> do let conn = toConnection conn' _ <- sendConnectRequest conn -- wait 10sec for a reply before giving up let _10sec = 1000000 * 10 responseM <- timeout _10sec $ readConnectResponse mempty conn case responseM of Just (isAuthorized -> True) -> app conn Just response -> return . throwError $ ProxyForwardError (BC.unpack response) Nothing -> return . throwError $ ProxyForwardError ("No response from the proxy after " <> show (_10sec `div` 1000000) <> "sec" ) debug $ "Closing tcp connection to proxy " <> show proxyHost <> ":" <> show proxyPort return ret where credentialsToHeader :: (ByteString, ByteString) -> ByteString credentialsToHeader (user, password) = "Proxy-Authorization: Basic " <> B64.encode (user <> ":" <> password) <> "\r\n" sendConnectRequest :: Connection -> IO () sendConnectRequest h = write h $ "CONNECT " <> fromString host <> ":" <> fromString (show port) <> " HTTP/1.0\r\n" <> "Host: " <> fromString host <> ":" <> (fromString $ show port) <> "\r\n" <> maybe mempty credentialsToHeader credentials <> "\r\n" readConnectResponse :: ByteString -> Connection -> IO ByteString readConnectResponse buff conn = do responseM <- read conn case responseM of Nothing -> return buff Just response -> if "\r\n\r\n" `isInfixOf` response then return $ buff <> response else readConnectResponse (buff <> response) conn isAuthorized :: ByteString -> Bool isAuthorized response = " 200 " `isInfixOf` response onError f = catch f $ \(e :: SomeException) -> return $ if (take 10 (show e) == "user error") then throwError $ ProxyConnectionError (show e) else throwError $ ProxyConnectionError ("Unknown Error :: " <> show e)