diff --git a/README.md b/README.md index 1dd08cd8..2072e1e5 100644 --- a/README.md +++ b/README.md @@ -33,40 +33,58 @@ wsTunnelClient <---> wsTunnelServer <---> RemoteHost Use secure connection (wss://) to bypass proxies wstunnel [OPTIONS] ws[s]://wstunnelServer[:port] + Client options: - -L --localToRemote=[BIND:]PORT:HOST:PORT Listen on local and forwards - traffic from remote - -D --dynamicToRemote=[BIND:]PORT Listen on local and dynamically - (with socks5 proxy) forwards - traffic from remote - -u --udp forward UDP traffic instead of - TCP - --udpTimeoutSec=INT When using udp forwarding, - timeout in seconds after when the - tunnel connection is closed. - Default 30sec, -1 means no timeout - -p --httpProxy=USER:PASS@HOST:PORT If set, will use this proxy to - connect to the server - --soMark=int (linux only) Mark network packet - with SO_MARK sockoption with the - specified value. You need to use - {root, sudo, capabilities} to run - wstunnel when using this option - --upgradePathPrefix=String Use a specific prefix that will - show up in the http path in the - upgrade request. Useful if you need - to route requests server side but - don't have vhosts + -L --localToRemote=[BIND:]PORT:HOST:PORT Listen on local and forwards + traffic from remote. Can be + used multiple time + -D --dynamicToRemote=[BIND:]PORT Listen on local and + dynamically (with socks5 proxy) + forwards traffic from remote + -u --udp forward UDP traffic instead + of TCP + --udpTimeoutSec=INT When using udp forwarding, + timeout in seconds after when + the tunnel connection is + closed. Default 30sec, -1 means + no timeout + -p --httpProxy=USER:PASS@HOST:PORT If set, will use this proxy + to connect to the server + --soMark=int (linux only) Mark network + packet with SO_MARK sockoption + with the specified value. You + need to use {root, sudo, + capabilities} to run wstunnel + when using this option + --upgradePathPrefix=String Use a specific prefix that + will show up in the http path + in the upgrade request. Useful + if you need to route requests + server side but don't have + vhosts + --hostHeader=String If set, add the custom string + as host http header + --tlsSNI=String If set, use custom string in + the SNI during TLS handshake + --websocketPingFrequencySec=int do a hearthbeat ping every x + seconds to maintain websocket + connection + --upgradeCredentials=USER[:PASS] Credentials for the Basic + HTTP authorization type sent + with the upgrade request. + -H --customHeaders="HeaderName: HeaderValue" Send custom headers in the + upgrade request. Can be used + multiple time + -h --help Display help message + -V --version Print version information Server options: - --server Start a server that will forward - traffic for you - -r --restrictTo=HOST:PORT Accept traffic to be forwarded - only to this service + --server Start a server that will + forward traffic for you + -r --restrictTo=HOST:PORT Accept traffic to be + forwarded only to this service Common options: - -v --verbose Print debug information - -q --quiet Print only errors - -h --help Display help message - -V --version Print version information + -v --verbose Print debug information + -q --quiet Print only errors ``` ## Examples diff --git a/app/Main.hs b/app/Main.hs index e687da5a..790ef48b 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -6,6 +6,8 @@ module Main where import ClassyPrelude hiding (getArgs, head) +import Data.CaseInsensitive ( CI ) +import qualified Data.CaseInsensitive as CI import qualified Data.ByteString.Char8 as BC import Data.List (head, (!!)) import Data.Maybe (fromMaybe) @@ -35,6 +37,7 @@ data WsTunnel = WsTunnel , tlsSNI :: String , websocketPingFrequencySec :: Int , wsTunnelCredentials :: String + , customHeaders :: [String] } deriving (Show, Data, Typeable) data WsServerInfo = WsServerInfo @@ -62,6 +65,8 @@ cmdLine = WsTunnel , udpMode = def &= explicit &= name "u" &= name "udp" &= help "forward UDP traffic instead of TCP" &= groupname "Client options" , udpTimeout = def &= explicit &= name "udpTimeoutSec" &= help "When using udp forwarding, timeout in seconds after when the tunnel connection is closed. Default 30sec, -1 means no timeout" &= groupname "Client options" + , customHeaders = def &= explicit &= name "H" &= name "customHeaders" &= help "Send custom headers in the upgrade request. Can be used multiple time" + &= typ "\"HeaderName: HeaderValue\"" &= groupname "Client options" , pathPrefix = def &= explicit &= name "upgradePathPrefix" &= help "Use a specific prefix that will show up in the http path in the upgrade request. Useful if you need to route requests server side but don't have vhosts" &= typ "String" &= groupname "Client options" @@ -173,6 +178,9 @@ parseProxyInfo str = do return $ ProxySettings (BC.unpack $ head ret) (fromIntegral portNumber) Nothing else Nothing +parseCustomHeader :: String -> (CI ByteString, ByteString) +parseCustomHeader header = (CI.mk . BC.pack $ takeWhile (/= ':') header, BC.pack . dropWhile (\c -> c == ' ' || c == ':') $ (dropWhile (/= ':') header)) + main :: IO () main = do @@ -242,6 +250,7 @@ runApp cfg serverInfo , tlsSNI = BC.pack $ Main.tlsSNI cfg , hostHeader = BC.pack $ Main.hostHeader cfg , websocketPingFrequencySec = Main.websocketPingFrequencySec cfg + , customHeaders = parseCustomHeader <$> Main.customHeaders cfg } toTcpLocalToRemoteTunnelSetting cfg serverInfo (TunnelInfo lHost lPort rHost rPort) = @@ -262,6 +271,7 @@ runApp cfg serverInfo , tlsSNI = BC.pack $ Main.tlsSNI cfg , hostHeader = BC.pack $ Main.hostHeader cfg , websocketPingFrequencySec = Main.websocketPingFrequencySec cfg + , customHeaders = parseCustomHeader <$> Main.customHeaders cfg } toUdpLocalToRemoteTunnelSetting cfg serverInfo (TunnelInfo lHost lPort rHost rPort) = @@ -282,6 +292,7 @@ runApp cfg serverInfo , tlsSNI = BC.pack $ Main.tlsSNI cfg , hostHeader = BC.pack $ Main.hostHeader cfg , websocketPingFrequencySec = Main.websocketPingFrequencySec cfg + , customHeaders = parseCustomHeader <$> Main.customHeaders cfg } toDynamicTunnelSetting cfg serverInfo (TunnelInfo lHost lPort _ _) = @@ -302,4 +313,5 @@ runApp cfg serverInfo , tlsSNI = BC.pack $ Main.tlsSNI cfg , hostHeader = BC.pack $ Main.hostHeader cfg , websocketPingFrequencySec = Main.websocketPingFrequencySec cfg + , customHeaders = parseCustomHeader <$> Main.customHeaders cfg } diff --git a/src/Tunnel.hs b/src/Tunnel.hs index 93da3b73..edc8c88c 100644 --- a/src/Tunnel.hs +++ b/src/Tunnel.hs @@ -63,7 +63,8 @@ tunnelingClientP cfg@TunnelSettings{..} app conn = onError $ do debug "Opening Websocket stream" stream <- connectionToStream conn - let headers = if not (null upgradeCredentials) then [("Authorization", "Basic " <> B64.encode upgradeCredentials)] else [] + let authorization = if not (null upgradeCredentials) then [("Authorization", "Basic " <> B64.encode upgradeCredentials)] else [] + let headers = authorization <> customHeaders let hostname = if not (null hostHeader) then (BC.unpack hostHeader) else serverHost ret <- WS.runClientWithStream stream hostname (toPath cfg) WS.defaultConnectionOptions headers run diff --git a/src/Types.hs b/src/Types.hs index 3537fdbe..3e2c1106 100644 --- a/src/Types.hs +++ b/src/Types.hs @@ -11,6 +11,7 @@ import Data.Maybe import System.IO (stdin, stdout) import Data.ByteString (hGetSome, hPutStr) +import Data.CaseInsensitive ( CI ) import qualified Data.Streaming.Network as N import qualified Network.Connection as NC import Network.Socket (HostName, PortNumber) @@ -80,6 +81,7 @@ data TunnelSettings = TunnelSettings , hostHeader :: ByteString , udpTimeout :: Int , websocketPingFrequencySec :: Int + , customHeaders :: [(CI ByteString, ByteString)] } instance Show TunnelSettings where diff --git a/test/Spec.hs b/test/Spec.hs index 268c0125..4f5a3e78 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -9,6 +9,8 @@ import qualified Network.Socket.ByteString as N import qualified Data.Conduit.Network.TLS as N import qualified Data.Streaming.Network as N +import Data.CaseInsensitive ( CI ) +import qualified Data.CaseInsensitive as CI import Control.Concurrent.Async as Async import Data.ByteString (hPutStr) import Control.Concurrent (threadDelay) @@ -51,6 +53,7 @@ testTCPLocalToRemote useTLS = do , hostHeader = "toto.com" , tlsSNI = "toto.com" , websocketPingFrequencySec = 30 + , customHeaders = [(CI.mk "toto", "tata"), (CI.mk "titi", "tutu")] } let client = runClient tunnelSetting @@ -112,6 +115,7 @@ testUDPLocalToRemote useTLS = do , hostHeader = "toto.com" , tlsSNI = "toto.com" , websocketPingFrequencySec = 30 + , customHeaders = [(CI.mk "toto", "tata"), (CI.mk "titi", "tutu")] } let client = runClient tunnelSetting @@ -172,6 +176,7 @@ testSocks5Tunneling useTLS = do , hostHeader = "toto.com" , tlsSNI = "toto.com" , websocketPingFrequencySec = 30 + , customHeaders = [(CI.mk "toto", "tata"), (CI.mk "titi", "tutu")] } let client = runClient tunnelSetting diff --git a/wstunnel.cabal b/wstunnel.cabal index c4c05ea2..bc05af5a 100644 --- a/wstunnel.cabal +++ b/wstunnel.cabal @@ -33,6 +33,7 @@ library , unordered-containers , websockets >= 0.12.4.0 , iproute + , case-insensitive default-language: Haskell2010 @@ -42,7 +43,7 @@ test-suite wstunnel-test main-is: Spec.hs default-extensions: NoImplicitPrelude, ScopedTypeVariables, BangPatterns, RecordWildCards build-depends: base >= 4.5 && < 5 - , async + , async , text >= 1.2.2.1 , classy-prelude , bytestring @@ -52,6 +53,7 @@ test-suite wstunnel-test , wstunnel , hspec , binary + , case-insensitive ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 @@ -74,5 +76,6 @@ executable wstunnel , text >= 1.2.2.1 , async , wstunnel + , case-insensitive default-language: Haskell2010