packages feed

second-transfer 0.6.0.0 → 0.6.1.0

raw patch · 13 files changed

+251/−91 lines, 13 files

Files

cbits/tlsinc.c view
@@ -63,7 +63,10 @@ void dispose_wired_session(wired_session_t* ws); static int thread_setup(void); +// Preprocessor flag: when 1, we will trace ALPN info+#define TRACE_ALPN_NEGOTIATION 0 + ////////////////////////////////////////////////////////////////////////  @@ -319,24 +322,32 @@     connection_t* conn = (connection_t*) arg;     static char output[64]; -    char* incursor = (char*) in; -    while (incursor < (char*)in + inlen )-    {-        // Got a protocol.... can I satisfy it?-        char sublen = *incursor;-        //printf("offered prot %.*s \n", sublen, incursor+1);+#if TRACE_ALPN_NEGOTIATION+    printf("protocol check starts, offered %d bytes for protocols ^^^^^^^^^^^^^ \n", inlen);+    int first_run = 1;+#endif+    char* stored_cursor = conn->protocol_list; -        char* stored_cursor = conn->protocol_list;+    while( stored_cursor < conn->protocol_list + conn->protocol_list_length)+    {+        char* incursor = (char*) in;         int sto_protocol = 0;+        char sublen2 = *stored_cursor; -        while( stored_cursor < conn->protocol_list + conn->protocol_list_length)+        while (incursor < (char*)in + inlen )         {-            char sublen2 = *stored_cursor;+            // Got a protocol.... can I satisfy it?+            char sublen = *incursor; +#if TRACE_ALPN_NEGOTIATION+            if (first_run)+                printf("offered prot %.*s \n", sublen, incursor+1);+#endif+             if (sublen != sublen2)             {-+                // No match, just continue             } else {                 int cmpresult = strncmp( incursor + 1, stored_cursor + 1, sublen);                 if (cmpresult == 0)@@ -344,19 +355,29 @@                     // They are equal, choose this one...                     strncpy( output, stored_cursor+1, sublen);                     *outlen = sublen;-                    *out = output;--+                    *out = (unsigned char*)output;+#if TRACE_ALPN_NEGOTIATION+                    printf("protocol check ends by match vvvvvvvvvvv \n");+#endif                     return SSL_TLSEXT_ERR_OK;                 }             }-            sto_protocol += 1;-            stored_cursor += (1+sublen2);++            incursor += (1+sublen);         } -        incursor += (1+sublen);+#if TRACE_ALPN_NEGOTIATION+        first_run = 0;+#endif++        sto_protocol += 1;+        stored_cursor += (1+sublen2);+     } +#if TRACE_ALPN_NEGOTIATION+    printf("protocol check ends without match vvvvvvvvvvv \n");+#endif     // I think this is what should be returned     return -1; }@@ -487,7 +508,10 @@         // Give the impression that we are using SNI         SSL_CTX_set_tlsext_servername_callback(c->sslContext, ssl_servername_cb); +        // Disable the session cache+        SSL_CTX_set_session_cache_mode(c->sslContext, SSL_SESS_CACHE_OFF); +         // The only cipher supported by HTTP/2 ... sort of.         result = SSL_CTX_set_cipher_list(c->sslContext,             "ECDHE-RSA-AES128-GCM-SHA256" //  --  ibidem@@ -811,16 +835,31 @@ {     if (ws == 0)         return ;+     if ( ws-> sslHandle )     {-        SSL_shutdown( ws->sslHandle );-        ws -> sslHandle = 0;+        int rr = SSL_shutdown( ws->sslHandle );+        // printf("rr=%d\n", rr);+        int err = SSL_get_error(ws->sslHandle, rr);+        if ( err < 0)+        {+            if ( err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE )+            {+                printf("error when closing socket / wait-for-data\n");+            } else if (err != 0 ){+                printf("cause openssl error code %d \n",err);+            }+        }     }++    SSL_free(ws->sslHandle);+    ws -> sslHandle = 0;     if (ws->socket)     {         close(ws->socket);         ws -> socket = 0;     }+     free(ws); } 
changelog.md view
@@ -1,9 +1,19 @@+- 0.6.1.0 :+    * Improved documentation.+    * Improved ALPN negotiation handling.+    * Messages with a body are now handled by the HTTP/1.1 speaking part of this+      library.+ - 0.6.0.0 :     * Changed the interface to provide more information to and from workers     * Implemented HTTP/2 push     * Upgraded dependencies to 1.0. for http2 package     * Extended channel interface in such a way that latency is reduced, as a result,       the test suite is completely broken. Please wait for 0.6.0.1 release.++- 0.5.5.1 :+    * Restricted HTTP2 package version.+    * Fixes #2  - 0.5.5.0 :     * Made HeaderEditor a Monoid instance
hs-src/SecondTransfer.hs view
@@ -6,6 +6,9 @@ Stability   : experimental Portability : POSIX +SecondTransfer is a HTTP/1.1 and HTTP/2 server session library, with an emphasis towards+experimentation (so far).+ This library implements enough of the HTTP/2  to build compliant HTTP/2 servers. It also implements enough of HTTP/1.1 so you can actually use it to build polyglot web-servers.@@ -15,9 +18,6 @@ This library just takes care of making sense of sent and received frames. -You can find more detailed information about this library at the page-<https://www.httptwo.com/second-transfer/>.- The library    * Is concurrent, meaning that you can use amazing Haskell lightweight threads to@@ -63,9 +63,11 @@     -- if you wish.     -- If you do multiple yields, no data will be left buffered between them,     -- so that you can for example implement a chat client in a single HTTP/2 stream.-    -- Pity browsers hardly support that.+    -- Not that browsers support that.      yield "Hello world!"-    -- No footers+    -- The HTTP/2 protocol supports sending headers *after* stream data. So, you usually+    -- return an empty list to signal that you don't want any of those headers. +    -- In these docs, the post headers are often called "footers".     return []  @@ -107,7 +109,10 @@  `AwareWorker` is the type of the basic callback function that you need to implement, but most times you can do with a simplified version called `CoherentWorker`. The function-`coherentToAwareWorker` does the conversion.+`coherentToAwareWorker` does the conversion. The difference between the two callbacks is +the level of information that you manage. With AwareWorker, you get a record on the request+with all sort of details, things like the session id and the + The callback is used to handle all requests to the server on a given negotiated ALPN protocol. If you need routing functionality (and you most certainly will need it), you need to build that functionality inside the callback.
hs-src/SecondTransfer/Http1/Parse.hs view
@@ -48,8 +48,6 @@ import           SecondTransfer.MainLoop.CoherentWorker (Headers) import           SecondTransfer.Utils                   (subByteString) -import           Prelude-  data IncrementalHttp1Parser = IncrementalHttp1Parser {     _fullText :: Bu.Builder
hs-src/SecondTransfer/Http1/Session.hs view
@@ -8,6 +8,7 @@ import           Control.Lens import           Control.Exception                       (catch) import           Control.Concurrent                      (forkIO)+import           Control.Monad.IO.Class                  (liftIO)  import qualified Data.ByteString                         as B -- import qualified Data.ByteString.Lazy                   as LB@@ -15,10 +16,12 @@ -- import qualified Data.ByteString.Builder                as Bu import           Data.Conduit import           Data.Conduit.List                       (consume)+import           Data.IORef -- import           Data.Monoid                            (mconcat, mappend)  import           SecondTransfer.MainLoop.CoherentWorker import           SecondTransfer.MainLoop.PushPullType+import           SecondTransfer.MainLoop.Protocol import           SecondTransfer.Sessions.Internal        (SessionsContext, acquireNewSessionTag, sessionsConfig)  -- Logging utilities@@ -41,7 +44,7 @@     =     do         new_session_tag <- acquireNewSessionTag sessions_context-        infoM "Session.Session_HTTP11" $ "Starting new session with tag: " ++(show new_session_tag)+        -- infoM "Session.Session_HTTP11" $ "Starting new session with tag: " ++(show new_session_tag)         forkIO $ go new_session_tag (Just "") 1         return ()   where@@ -52,13 +55,15 @@      go :: Int -> Maybe B.ByteString -> Int -> IO ()     go session_tag (Just leftovers) reuse_no = do-        infoM "Session.Session_HTTP11" $ "(Re)Using session with tag: " ++ (show session_tag)+        -- infoM "Session.Session_HTTP11" $ "(Re)Using session with tag: " ++ (show session_tag)         maybe_leftovers <- add_data newIncrementalHttp1Parser leftovers session_tag reuse_no         go session_tag maybe_leftovers (reuse_no + 1)      go _ Nothing _  =         return () +    -- This function will invoke itself as long as data is coming for the currently-being-parsed+    -- request/response.     add_data :: IncrementalHttp1Parser  -> B.ByteString -> Int -> Int -> IO (Maybe B.ByteString)     add_data parser bytes session_tag reuse_no = do         let@@ -78,7 +83,7 @@                     ( (\ _e -> do                         -- This is a pretty harmless condition that happens                         -- often when the remote peer closes the connection-                        debugM "Session.HTTP1" "Could not receive data"+                        -- debugM "Session.HTTP1" "Could not receive data"                         close_action                         return Nothing                     ) :: IOProblem -> IO (Maybe B.ByteString) )@@ -98,9 +103,11 @@                         _headers_RQ = modified_headers,                         _inputData_RQ = Nothing,                         _perception_RQ = Perception {-                          _startedTime_Pr = started_time,-                          _streamId_Pr    = reuse_no,-                          _sessionId_Pr   = session_tag+                          _startedTime_Pr       = started_time,+                          _streamId_Pr          = reuse_no,+                          _sessionId_Pr         = session_tag,+                          _protocol_Pr          = Http11_HPV,+                          _anouncedProtocols_Pr = Nothing                         }                     }                 let@@ -117,18 +124,74 @@                         return $ Just leftovers                     )                     ((\ _e -> do-                        debugM "Session.HTTP1" "Session abandoned"+                        -- debugM "Session.HTTP1" "Session abandoned"                         close_action                         return Nothing                     ) :: IOProblem -> IO (Maybe B.ByteString) ) -            HeadersAndBody_H1PC _headers _stopcondition _recv_leftovers -> do-                -- print "HeadersAndBody_H1PC"-                -- Let's see if I can go through the basic movements first, then through-                -- more complicated things.-                -- TODO: Implement posts and other requests with bodies....-                close_action-                error "NotImplemented requests with bodies"+            HeadersAndBody_H1PC headers stopcondition recv_leftovers -> do+                let+                    modified_headers = addExtraHeaders sessions_context headers+                started_time <- getTime Monotonic+                set_leftovers <- newIORef ""++                principal_stream <- coherent_worker Request {+                        _headers_RQ = modified_headers,+                        _inputData_RQ = Just $ counting_read recv_leftovers stopcondition set_leftovers,+                        _perception_RQ = Perception {+                          _startedTime_Pr       = started_time,+                          _streamId_Pr          = reuse_no,+                          _sessionId_Pr         = session_tag,+                          _protocol_Pr          = Http11_HPV,+                          _anouncedProtocols_Pr = Nothing+                        }+                    }+                let+                    data_and_conclusion = principal_stream ^. dataAndConclusion_PS+                    response_headers    = principal_stream ^. headers_PS+                (_, fragments) <- runConduit $ fuseBoth data_and_conclusion consume+                channel_leftovers <- readIORef set_leftovers+                let+                    response_text =+                        serializeHTTPResponse response_headers fragments++                catch+                    (do+                        push_action response_text+                        return $ Just channel_leftovers+                    )+                    ((\ _e -> do+                        -- debugM "Session.HTTP1" "Session abandoned"+                        close_action+                        return Nothing+                    ) :: IOProblem -> IO (Maybe B.ByteString) )++    counting_read :: B.ByteString -> BodyStopCondition -> IORef B.ByteString -> Source IO B.ByteString+    counting_read leftovers un@(UseBodyLength_BSC n) set_leftovers = do+        -- Can I continue?+        if n == 0 then+          do+            liftIO $ writeIORef set_leftovers leftovers+            return ()+        else+          do+            let+                lngh_leftovers = B.length leftovers+            if lngh_leftovers > 0 then+                if lngh_leftovers <= n then+                  do+                    yield leftovers+                    counting_read "" (UseBodyLength_BSC (n - lngh_leftovers )) set_leftovers+                else+                  do+                    let+                      (pass, new_leftovers) = B.splitAt n leftovers+                    yield pass+                    counting_read new_leftovers (UseBodyLength_BSC 0) set_leftovers+            else+              do+                more_text <- liftIO $ best_effort_pull_action True+                counting_read more_text un set_leftovers   addExtraHeaders :: SessionsContext -> Headers -> Headers
hs-src/SecondTransfer/Http2/Framer.hs view
@@ -135,7 +135,7 @@     , _closeAction           :: CloseAction      -- Global id of the session, used for e.g. error reporting.-    , _sessionIdAtFramer     :: Int+    , _sessionIdAtFramer     :: !Int      -- Sessions context, used for thing like e.g. error reporting     , _sessionsContext       :: SessionsContext@@ -166,7 +166,7 @@      new_session_id <- modifyMVarMasked         session_id_mvar $-        \ session_id -> return (session_id+1, session_id)+        \ session_id -> return $ session_id `seq` (session_id + 1, session_id)      (session_input, session_output) <- http2Session                                         aware_worker
hs-src/SecondTransfer/Http2/Session.hs view
@@ -66,6 +66,7 @@ -- Imports from other parts of the program import           SecondTransfer.MainLoop.CoherentWorker import           SecondTransfer.MainLoop.Tokens+import           SecondTransfer.MainLoop.Protocol import           SecondTransfer.Sessions.Config import           SecondTransfer.Sessions.Internal       (sessionExceptionHandler,                                                          SessionsContext,@@ -356,8 +357,6 @@ --- more robust. sessionInputThread :: ReaderT SessionData IO () sessionInputThread  = do-    INSTRUMENTATION( debugM "HTTP2.Session" "Entering sessionInputThread" )-     -- This is an introductory and declarative block... all of this is tail-executed     -- every time that  a packet needs to be processed. It may be a good idea to abstract     -- these values in a closure...@@ -518,7 +517,9 @@                   perception = Perception {                       _startedTime_Pr = headers_arrived_time,                       _streamId_Pr = stream_id,-                      _sessionId_Pr = current_session_id+                      _sessionId_Pr = current_session_id,+                      _protocol_Pr = Http2_HPV,+                      _anouncedProtocols_Pr = Nothing                       }                   request = Request {                       _headers_RQ = header_list_after,@@ -571,9 +572,10 @@                 maybe_thread_id <- H.lookup stream2workerthread stream_id                 case maybe_thread_id  of                     Nothing ->-                        -- This is actually more like an internal error, when this-                        -- happens, cancel the session-                        error "InterruptingUnexistentStream"+                        -- This is can actually happen in some implementations: we are asked to+                        -- cancel an stream we know nothing about. Log which stream it is+                        logit $  "InterruptingUnexistentStream " `mappend` (pack . show $ stream_id)+                        -- and don't get too crazy about it.                      Just thread_id -> do                         -- INSTRUMENTATION( infoM "HTTP2.Session" $ "Stream successfully interrupted" )
hs-src/SecondTransfer/MainLoop/CoherentWorker.hs view
@@ -27,6 +27,7 @@     , CoherentWorker     , InputDataStream     , TupledPrincipalStream+    , TupledRequest     , FragmentDeliveryCallback      , headers_RQ@@ -42,6 +43,8 @@     , startedTime_Pr     , streamId_Pr     , sessionId_Pr+    , anouncedProtocols_Pr+    , protocol_Pr     , fragmentDeliveryCallback_Ef     , priorityEffect_Ef @@ -54,12 +57,14 @@   import           Control.Lens-import qualified Data.ByteString   as B+import qualified Data.ByteString                       as B import           Data.Conduit-import           Data.Foldable     (find)-import           System.Clock      (TimeSpec)+import           Data.Foldable                         (find)+import           System.Clock                          (TimeSpec) +import           SecondTransfer.MainLoop.Protocol      (HttpProtocolVersion) + -- | The name part of a header type HeaderName = B.ByteString @@ -84,15 +89,19 @@  -- | Data related to the request data Perception = Perception {-  -- Monotonic time close to when the request was first seen in-  -- the processing pipeline.-  _startedTime_Pr :: TimeSpec,-  -- The HTTP/2 stream id. Or the serial number of the request in an-  -- HTTP/1.1 session.-  _streamId_Pr :: Int,-  -- You know better than to use this for normal web request-  -- processing. But otherwise a number uniquely identifying the session. -  _sessionId_Pr :: Int+    -- | The HTTP/2 stream id. Or the serial number of the request in an+    -- HTTP/1.1 session.+    _streamId_Pr :: Int,+    -- | A number uniquely identifying the session. This number is unique and+    --   the same for each TPC connection that a client opens using a given protocol.+    _sessionId_Pr :: Int,+    -- | Monotonic time close to when the request was first seen in+    -- the processing pipeline.+    _startedTime_Pr       :: TimeSpec,+    -- | Which protocol is serving the request+    _protocol_Pr          :: HttpProtocolVersion,+    -- | For new connections, probably a list of anounced protocols+    _anouncedProtocols_Pr :: Maybe [B.ByteString]   }  makeLenses ''Perception@@ -197,15 +206,22 @@ --   cancels the stream type AwareWorker = Request -> IO PrincipalStream --- | A CoherentWorker is a less fuzzy worker, but less aware.-type CoherentWorker =  (Headers, Maybe InputDataStream) -> IO (Headers, PushedStreams, DataAndConclusion)+-- | A CoherentWorker is a simplified callback that you can implement to handle requests.+--  Then you can convert it to an AwareWorker with `tupledPrincipalStreamToPrincipalStream`.+type CoherentWorker =  TupledRequest -> IO TupledPrincipalStream --- | Not exactly equivalent of the prinicipal stream+-- | A tuple representing the data alone that you usually need to give as a response, that+--   is, the headers in the response (including the HTTP/2 :status), any pushed streams,+--   a stream with the response data and the footers. type TupledPrincipalStream = (Headers, PushedStreams, DataAndConclusion) +-- | A tuple representing the data alone usually needed to create a response. That is,+--   the headers (including HTTP/2 :path, :authority, etc) and maybe an input data stream+--   for requests that include it, that is, POST and PUT. type TupledRequest = (Headers, Maybe InputDataStream)  +-- | Convert between the two types of callback. tupledPrincipalStreamToPrincipalStream :: TupledPrincipalStream -> PrincipalStream tupledPrincipalStreamToPrincipalStream (headers, pushed_streams, data_and_conclusion) = PrincipalStream       {
hs-src/SecondTransfer/MainLoop/OpenSSL_TLS.hs view
@@ -303,15 +303,13 @@ -- provideActions :: Wired_Ptr -> IO (LB.ByteString -> IO (), Int -> IO B.ByteString, IO ()) provideActions :: Wired_Ptr -> IO AttendantCallbacks provideActions wired_ptr = do-    already_closed_mvar <- newMVar False+    can_write_mvar <- newMVar True+    can_read_mvar  <- newMVar True     let         pushAction :: LB.ByteString -> IO ()-        pushAction datum = do-            already_closed <- readMVar already_closed_mvar-            if already_closed+        pushAction datum = withMVar can_write_mvar  $ \ can_write ->+            if can_write               then-                throwIO $ TLSLayerGenericProblem "Tried to send data on closed handle"-              else                 BU.unsafeUseAsCStringLen (LB.toStrict datum) $ \ (pchar, len) -> do                     result <- sendData wired_ptr pchar (fromIntegral len)                     case result of@@ -319,14 +317,13 @@                                 return ()                           | r == badHappened     ->                                 throwIO $ TLSLayerGenericProblem "Could not send data"+            else+              throwIO $ TLSLayerGenericProblem "CouldNotWriteData--SocketAlreadyClosed"          pullAction :: Int -> IO B.ByteString-        pullAction bytes_to_get =  do-            already_closed <- readMVar already_closed_mvar-            if already_closed+        pullAction bytes_to_get =  withMVar can_read_mvar  $ \ can_read ->+            if can_read               then-                throwIO $ TLSLayerGenericProblem "Tried to receive on closed handle"-              else                 allocaBytes bytes_to_get $ \ pcharbuffer ->                     alloca $ \ data_recvd_ptr -> do                         result <- recvData wired_ptr pcharbuffer (fromIntegral bytes_to_get) data_recvd_ptr@@ -336,14 +333,13 @@                                     throwIO $ TLSLayerGenericProblem "Could not receive data"                          B.packCStringLen (pcharbuffer, fromIntegral recvd_bytes)+              else+                throwIO $ TLSLayerGenericProblem "CouldNotReadData--SocketAlreadyClosed"          bestEffortPullAction :: Bool -> IO B.ByteString-        bestEffortPullAction can_wait =  do-            already_closed <- readMVar already_closed_mvar-            if already_closed+        bestEffortPullAction can_wait = withMVar can_read_mvar  $ \ can_read ->+            if can_read               then-                throwIO $ TLSLayerGenericProblem "Tried to receive on closed handle"-              else                 allocaBytes useBufferSize $ \ pcharbuffer ->                     alloca $ \ data_recvd_ptr -> do                         result <- recvDataBestEffort@@ -358,14 +354,22 @@                                     throwIO $ TLSLayerGenericProblem "Could not receive data"                          B.packCStringLen (pcharbuffer, fromIntegral recvd_bytes)+              else+                throwIO $ TLSLayerGenericProblem "CouldNotReadData--SocketAlreadyClosed" +         closeAction :: IO ()         -- Ensure that the socket and the struct are only closed once-        closeAction = do-            b <- readMVar already_closed_mvar-            unless b $  do-                modifyMVar_ already_closed_mvar (\ _ -> return True)-                disposeWiredSession wired_ptr+        closeAction =+            modifyMVar_ can_write_mvar $ \ can_write ->+                modifyMVar can_read_mvar $ \ can_read ->+                    if can_write && can_read+                      then do+                          disposeWiredSession wired_ptr+                          return (False,False)+                      else+                          return (False,False)+     return  AttendantCallbacks {         _pushAction_AtC = pushAction,         _pullAction_AtC = pullAction,
+ hs-src/SecondTransfer/MainLoop/Protocol.hs view
@@ -0,0 +1,11 @@+module SecondTransfer.MainLoop.Protocol (+    HttpProtocolVersion(..)+                                        ) where+++-- | The protocol version used. Here we distinguish only between+--   HTTP/1.1 and  HTTP/2+data HttpProtocolVersion =+     Http11_HPV+    |Http2_HPV+
hs-src/SecondTransfer/MainLoop/PushPullType.hs view
@@ -47,20 +47,29 @@ --   is severed suddenly. type CloseAction = IO () -+-- | A set of functions describing how to do I/O in a session.+--   As usual, we provide lenses accessors. data AttendantCallbacks = AttendantCallbacks {+    -- | put some data in the channel     _pushAction_AtC               :: PushAction,+    -- | get exactly this much data from the channel. This function can+    --   be used by HTTP/2 since lengths are pretty well built inside the+    --   protocoll itself.     _pullAction_AtC               :: PullAction,+    -- | pull data from the channel, as much as the TCP stack wants to provide.+    --   we have no option but use this one when talking HTTP/1.1, where the best+    --   way to know the length is to scan until a Content-Length is found.     _bestEffortPullAction_AtC     :: BestEffortPullAction,+    -- | this is called when we wish to close the channel.     _closeAction_AtC              :: CloseAction     }  makeLenses ''AttendantCallbacks --- | A function which takes three arguments: the first one says---   how to send data (on a socket or similar transport), and the second one how---   to receive data on the transport. The third argument encapsulates---   the sequence of steps needed for a clean shutdown.+-- | This is an intermediate type. It represents what you obtain+--   by combining something that speaks the protocol and an AwareWorker.+--   In turn, you need to feed a bundle of callbacks implementing I/O+--   to finally start a server. -- --   You can implement one of these to let somebody else  supply the --   push, pull and close callbacks. For example, 'tlsServeWithALPN' will
hs-src/SecondTransfer/Types.hs view
@@ -1,7 +1,9 @@ module SecondTransfer.Types(     module SecondTransfer.MainLoop.PushPullType     ,module SecondTransfer.MainLoop.CoherentWorker+    ,module SecondTransfer.MainLoop.Protocol     ) where  import SecondTransfer.MainLoop.PushPullType import SecondTransfer.MainLoop.CoherentWorker+import SecondTransfer.MainLoop.Protocol
second-transfer.cabal view
@@ -7,7 +7,7 @@ -- PVP       summary:      +-+------- breaking API changes --                         | | +----- non-breaking API additions --                         | | | +--- code changes with no API change-version     :              0.6.0.0+version     :              0.6.1.0  synopsis    :              Second Transfer HTTP/2 web server @@ -53,7 +53,7 @@ source-repository this   type:     git   location: git@github.com:alcidesv/second-transfer.git-  tag:      0.6.0.0+  tag:      0.6.1.0  library @@ -75,6 +75,7 @@    other-modules:  SecondTransfer.MainLoop.CoherentWorker                 , SecondTransfer.MainLoop.PushPullType+                , SecondTransfer.MainLoop.Protocol                 , SecondTransfer.MainLoop.Tokens                 , SecondTransfer.MainLoop.Framer                 , SecondTransfer.MainLoop.Logging