diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 sen.cenan@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,33 @@
+module Main where
+
+import Data.Semigroup ((<>))
+import qualified Options.Applicative as Opt
+
+import Network.Socketed (SocketedOptions(..), runSocketedServer)
+
+params :: Opt.Parser SocketedOptions
+params = SocketedOptions
+   <$> Opt.strOption
+      ( Opt.long "bind"
+      <> Opt.short 'b'
+      <> Opt.help "host ip the server will be bind to"
+      <> Opt.showDefault
+      <> Opt.value "0.0.0.0"
+      <> Opt.metavar "STRING" )
+   <*> Opt.option Opt.auto
+      ( Opt.long "port"
+      <> Opt.short 'p'
+      <> Opt.help "port number the server will be running on"
+      <> Opt.showDefault
+      <> Opt.value 3000
+      <> Opt.metavar "INT" )
+
+main :: IO ()
+main = Opt.execParser opts >>= runSocketedServer
+   where
+      opts = Opt.info (Opt.helper <*> params)
+         (
+            Opt.fullDesc
+            <> Opt.progDesc "socketed"
+            <> Opt.header "socketed"
+         )
diff --git a/Network/Socketed.hs b/Network/Socketed.hs
new file mode 100644
--- /dev/null
+++ b/Network/Socketed.hs
@@ -0,0 +1,72 @@
+module Network.Socketed (
+      SocketedOptions(..), runSocketedServer
+   ) where
+
+import Conduit (
+      ConduitM, MonadIO,
+      (.|), mapM_C, runConduit, sinkList
+   )
+import Data.Conduit.TMChan (TMChan, sinkTMChan, dupTMChan, sourceTMChan)
+
+import Control.Concurrent.Async (async)
+import Control.Concurrent.STM (atomically)
+import Control.Concurrent.STM.TMChan (newBroadcastTMChanIO)
+
+import Data.ByteString (ByteString)
+import Data.ByteString.Char8 (pack, unpack)
+import Data.ByteString.Lazy (fromStrict)
+import Data.Void (Void)
+
+import Network.HTTP.Types (status200)
+import Network.Wai (Application, responseLBS)
+import Network.Wai.Handler.Warp (
+      Settings,
+      defaultSettings, setPort, setHost, runSettings
+   )
+import Network.Wai.Handler.WebSockets (websocketsOr)
+
+import Network.WebSockets (
+      ServerApp,
+      acceptRequest, defaultConnectionOptions, sendTextData
+   )
+
+import Network.Socketed.Internal (
+      SocketedOptions(..),
+      stdinLines, showWSHost, takeUntil2Empty
+   )
+import Network.Socketed.Template (evalHtml)
+
+sinkStdinToChan :: MonadIO m => TMChan ByteString -> ConduitM a Void m ()
+sinkStdinToChan = (stdinLines .|) . flip sinkTMChan False
+
+serverSettings :: SocketedOptions -> Settings
+serverSettings (SocketedOptions h p) = setHost (read hoststr)
+   $ setPort p defaultSettings where hoststr = "Host \"" ++ h ++ "\""
+
+socketedApp :: [ByteString] -> ByteString -> TMChan ByteString -> Application
+socketedApp rls html broadcastChan
+   = websocketsOr defaultConnectionOptions wsApp backupApp where
+      wsApp :: ServerApp
+      wsApp pendingConn = do
+         conn <- acceptRequest pendingConn
+         chan <- atomically $ dupTMChan broadcastChan
+         _ <- mapM (sendTextData conn) rls -- send replayed lines
+         runConduit $ sourceTMChan chan .| mapM_C (sendTextData conn)
+
+      backupApp :: Application
+      backupApp _ respond = respond
+         $ responseLBS status200 [] (fromStrict html)
+
+runSocketedServer :: SocketedOptions -> IO ()
+runSocketedServer opts@(SocketedOptions h p) = do
+   putStrLn "Accepting replayed data: "
+   rls <- runConduit $ stdinLines .| takeUntil2Empty .| sinkList
+   putStrLn "Replayed data: "
+   mapM_ print rls
+   putStrLn $ "\nStart streaming @ " ++ showWSHost h p
+   chan <- newBroadcastTMChanIO
+   mirror <- atomically $ dupTMChan chan
+   _ <- async . runConduit $ sinkStdinToChan chan
+   _ <- async . runConduit $ sourceTMChan mirror .| mapM_C (putStrLn . unpack)
+   let app = socketedApp rls (pack $ evalHtml (length rls) opts) chan
+   runSettings (serverSettings opts) app
diff --git a/Network/Socketed/Internal.hs b/Network/Socketed/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Network/Socketed/Internal.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Network.Socketed.Internal where
+
+import Conduit (
+      ConduitM, MonadIO,
+      (.|), runConduit, sourceHandle, stdoutC, await, yield, leftover
+   )
+
+import Control.Concurrent (threadDelay)
+import Control.Concurrent.Async (Async, async, cancel, race, wait)
+import Control.Exception (AsyncException(..), catch)
+
+import Data.ByteString (ByteString, empty)
+import Data.Either.Utils (fromRight)
+import qualified Data.Conduit.Binary as CB (lines)
+
+import Language.Haskell.TH (stringE)
+import Language.Haskell.TH.Quote (QuasiQuoter(..))
+
+import System.IO (stdin)
+
+data SocketedOptions = SocketedOptions {
+      host :: String,
+      port :: Int
+   }
+
+stdinLines :: MonadIO m => ConduitM a ByteString m ()
+stdinLines = sourceHandle stdin .| CB.lines
+
+takeUntil2Empty :: MonadIO m => ConduitM ByteString ByteString m ()
+takeUntil2Empty =
+      loop
+   where
+      loop = do
+         a <- await
+         b <- await
+         case (a, b) of
+            (Nothing, Nothing) -> loop
+            (Just x, Nothing) -> leftover x >> loop
+            (Nothing, Just x) -> leftover x >> loop
+            (Just x, Just y)
+               | x == empty && y == empty -> return ()
+               | otherwise -> leftover y >> yield x >> loop
+
+stdinPassthrough :: IO ()
+stdinPassthrough = runConduit $ sourceHandle stdin .| stdoutC
+
+withStdinPassthrough :: IO a -> IO a
+withStdinPassthrough work = do
+   a <- async work
+   stdinPassthrough
+   wait a
+
+showWSHost :: String -> Int -> String
+showWSHost h p = "ws://" ++ h ++ ":" ++ show p
+
+stringQuote :: QuasiQuoter
+stringQuote = QuasiQuoter {
+   quoteExp = stringE,
+   quotePat = undefined,
+   quoteType = undefined,
+   quoteDec = undefined
+}
+
+waitTimeout :: forall a . Async a -> a -> Int -> IO a
+waitTimeout task def time =
+   let
+      left = threadDelay time >> cancel task
+      right = wait task
+      hdl :: AsyncException -> IO a
+      hdl _ = return def
+   in
+      fmap fromRight (race left right) `catch` hdl
diff --git a/Network/Socketed/Template.hs b/Network/Socketed/Template.hs
new file mode 100644
--- /dev/null
+++ b/Network/Socketed/Template.hs
@@ -0,0 +1,83 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Network.Socketed.Template (socketedScript, evalHtml) where
+
+import Data.List (intercalate)
+
+import Network.Socketed.Internal (SocketedOptions(..), showWSHost, stringQuote)
+
+socketedScript :: String -> Int -> String -> String  -> String
+socketedScript h ldrop hf ef = [stringQuote|
+   <script>
+   (function(host, linesToDrop, handleMessage, handleMessageError) {
+      window.handleMessage = handleMessage;
+      window.handleMessageError = handleMessageError;
+
+      var retryCount;
+
+      var connect = function(l) {
+         var
+            lineDropped = 0,
+            socket = new WebSocket(host);
+
+         socket.onopen = function(e) {
+            console.log('connected');
+            retryCount = 0;
+         };
+
+         socket.onclose = function(e) {
+            console.log('socket closed: ', e);
+
+            if (++retryCount > 20) {
+               window.document.body.textContent = 'disconnected...';
+            } else {
+               setTimeout(function() {
+                  console.log('reconnecting...');
+                  connect(linesToDrop);
+               }, 2000);
+            }
+         };
+
+         socket.onmessage = function(event) {
+            if (++lineDropped > l) {
+               try {
+                  window.handleMessage(event);
+               } catch(error) {
+                  window.handleMessageError(event, error);
+               }
+            } else {
+               console.log('drops: ', event.data)
+            }
+         };
+      };
+
+      connect(0);
+   })(|]
+   ++ intercalate "," ["'" ++ h ++ "'", show ldrop, hf, ef]
+   ++ [stringQuote|)</script>|]
+
+wrapHtml :: String -> String
+wrapHtml inner = [stringQuote|
+      <!DOCTYPE html><html><body>
+   |]
+   ++ inner
+   ++ [stringQuote|
+      </body></html>
+   |]
+
+evalHtml :: Int -> SocketedOptions -> String
+evalHtml r (SocketedOptions h p) = wrapHtml $ socketedScript
+   (showWSHost h p)
+   r
+   [stringQuote|
+      function(event) {
+         eval(event.data);
+      }
+   |]
+   [stringQuote|
+      function(event, error) {
+         var c = document.createElement('div');
+         c.textContent = 'failed to eval: '+ event.data;
+         window.document.body.appendChild(c);
+      }
+   |]
diff --git a/socketed.cabal b/socketed.cabal
new file mode 100644
--- /dev/null
+++ b/socketed.cabal
@@ -0,0 +1,66 @@
+name: socketed
+version: 0.1.0.0
+
+synopsis: simpe tool to serve piped data over http and websocket
+description: simpe tool to serve piped data over http and websocket
+
+license: MIT
+license-file: LICENSE
+author: sen.cenan@gmail.com
+maintainer: sen.cenan@gmail.com
+
+category: Web
+build-type: Simple
+cabal-version: >=1.10
+
+library
+  build-depends:
+    base >=4.9 && <4.10,
+    bytestring >= 0.10.8.1,
+    websockets >= 0.10.0.0,
+    conduit-combinators >= 1.1.1,
+    conduit-extra >= 1.1.15,
+    http-types >= 0.9.1,
+    warp >= 3.2.11.1,
+    text >= 1.2.2.1,
+    async >= 2.1.1,
+    wai >= 3.2.1.1,
+    wai-websockets >= 3.0.1.1,
+    stm >= 2.4.4.1,
+    stm-chans >= 3.0.0.4,
+    stm-conduit >= 3.0.0,
+    template-haskell >= 2.11.1.0,
+    optparse-applicative >= 0.13.2.0,
+    MissingH >= 1.4.0.1
+  default-language: Haskell2010
+  exposed-modules:
+    Network.Socketed,
+    Network.Socketed.Template,
+    Network.Socketed.Internal
+
+executable socketed
+  main-is: Main.hs
+  build-depends:
+    base >=4.9 && <4.10,
+    bytestring >= 0.10.8.1,
+    websockets >= 0.10.0.0,
+    conduit-combinators >= 1.1.1,
+    conduit-extra >= 1.1.15,
+    http-types >= 0.9.1,
+    warp >= 3.2.11.1,
+    text >= 1.2.2.1,
+    async >= 2.1.1,
+    wai >= 3.2.1.1,
+    wai-websockets >= 3.0.1.1,
+    stm >= 2.4.4.1,
+    stm-chans >= 3.0.0.4,
+    stm-conduit >= 3.0.0,
+    template-haskell >= 2.11.1.0,
+    optparse-applicative >= 0.13.2.0,
+    MissingH >= 1.4.0.1
+  default-language: Haskell2010
+  ghc-options: -threaded
+  other-modules:
+    Network.Socketed,
+    Network.Socketed.Template,
+    Network.Socketed.Internal
