├── Setup.hs ├── .gitignore ├── stack.yaml ├── ChangeLog.md ├── src ├── Network │ ├── N2O │ │ ├── Web.hs │ │ ├── Internal.hs │ │ └── Web │ │ │ ├── Http.hs │ │ │ └── WebSockets.hs │ └── N2O.hs └── Data │ └── BERT.hs ├── LICENSE ├── man ├── web.htm └── int.htm ├── n2o.cabal ├── README.md └── index.html /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | *~ 3 | .idea 4 | *.iml 5 | cabal.config 6 | *.aes 7 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | resolver: lts-12.12 2 | packages: 3 | - . 4 | extra-deps: 5 | - git: git@github.com:o3/nitro.git 6 | commit: master 7 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | History 2 | ======= 3 | 4 | 0.1 Andy Melnikov, Client Protocol, BERT, IxSet, Chat App 5 | 0.11 Marat Khafizov, Nitro Protocol, BERT, Simple App 6 | -------------------------------------------------------------------------------- /src/Network/N2O/Web.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Network.N2O.Web 3 | Description : Static HTTP Server and Bridge for WebSockets 4 | Copyright : (c) Marat Khafizov, 2018 5 | License : BSD-3 6 | Maintainer : xafizoff@gmail.com 7 | Stability : experimental 8 | Portability : not portable 9 | 10 | This package provides a simple static HTTP server and the WebSockets adapter 11 | to the N2O Protocol Loop. 12 | 13 | -} 14 | module Network.N2O.Web 15 | ( module Network.N2O.Web.WebSockets 16 | , module Network.N2O.Web.Http 17 | , module Web.Nitro 18 | ) where 19 | 20 | import Network.N2O.Web.WebSockets 21 | import Network.N2O.Web.Http 22 | import Web.Nitro 23 | -------------------------------------------------------------------------------- /src/Network/N2O.hs: -------------------------------------------------------------------------------- 1 | {-| 2 | Module : Network.N2O 3 | Description : Core of the N2O Framework 4 | Copyright : (c) Marat Khafizov, 2018 5 | License : BSD-3 6 | Maintainer : xafizoff@gmail.com 7 | Stability : experimental 8 | Portability : not portable 9 | 10 | This module defines basic types and functions for the N2O Framework. 11 | 12 | One of the trickiest part of the client-server applications is the communication 13 | protocol between client and server. This package aims to provide scalable application 14 | level infrastructure for protocols and services. 15 | 16 | Logically, this package consists of two parts: 17 | 18 | * the 'N2O' monad for local state management; 19 | * the 'protoRun' function, that allows to perform abstract protocol loop. 20 | 21 | For basic usage see [N2O sample app](https://github.com/xafizoff/n2o/tree/master/samples) 22 | 23 | -} 24 | module Network.N2O 25 | ( module Network.N2O.Internal 26 | , module Data.BERT 27 | , liftIO 28 | , module M 29 | ) where 30 | 31 | import Network.N2O.Internal 32 | import Data.BERT 33 | import Control.Monad.IO.Class (liftIO) 34 | import Control.Monad.Trans.Reader as M 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Marat Khafizov (c) 2018 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Marat Khafizov nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /man/web.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | WEB 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 |
23 | 24 |

WEB

25 |
26 | 27 |
28 |
29 | 30 |

INTRO

31 | 32 |

Network.N2O.Web defines adjoin interfaces to underlying 33 | transports: HTTP for static file serving and WebSocket for N2O protocols.

34 | 35 |
36 |
37 | 38 |

WebSocket

39 | 40 |

wsApp :: Cx f a b -> WS.ServerApp

41 | 42 |
43 |
44 | 45 |

HTTP

46 | 47 |

runServer :: String -> Int -> Cx f a b -> IO ()

48 |

49 | 50 |
51 |
52 | 53 |

This module may refer to: 54 | TYPES, 55 | PROTOCOLS, 56 | CORE 57 |

58 | 59 |
60 |
61 | 62 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /n2o.cabal: -------------------------------------------------------------------------------- 1 | name: n2o 2 | version: 0.11.2 3 | homepage: https://github.com/xafizoff/n2o#readme 4 | bug-reports: https://github.com/xafizoff/n2o/issues 5 | author: Marat Khafizov 6 | maintainer: xafizoff@gmail.com 7 | copyright: 2018 Marat Khafizov (c) 8 | license: BSD3 9 | license-file: LICENSE 10 | build-type: Simple 11 | cabal-version: >= 1.10 12 | extra-source-files: README.md 13 | category: Network, N2O, Web 14 | synopsis: Abstract Protocol Loop 15 | description: N2O defines a way we scale protocols, database schema, applications 16 | and services across companies, formatters, views and presentation layers. 17 | At the core N2O folds a list of protocols and their handlers providing 18 | a minimal type-level specification for general purpose application protocol tract. 19 | 20 | source-repository head 21 | type: git 22 | location: https://github.com/xafizoff/n2o 23 | 24 | library 25 | exposed-modules: 26 | Network.N2O 27 | , Network.N2O.Internal 28 | , Data.BERT 29 | , Network.N2O.Web 30 | , Network.N2O.Web.Http 31 | , Network.N2O.Web.WebSockets 32 | other-modules: 33 | Paths_n2o 34 | hs-source-dirs: 35 | src 36 | build-depends: base >= 4.7 && < 5 37 | , text >= 1.2 && < 1.3 38 | , bytestring >= 0.9 && < 1 39 | , containers >= 0.5 && < 0.6 40 | , transformers >= 0.5 && < 0.6 41 | , time >= 1.8 && < 1.9 42 | , cereal >= 0.5 && < 0.6 43 | , n2o-nitro == 0.11.2 44 | , websockets >= 0.12.5 && < 0.13 45 | , network >= 2.6 && < 2.7 46 | , text >= 1.2 && < 1.3 47 | , store >= 0.5 && < 0.6 48 | , attoparsec >= 0.13.2 && < 0.14 49 | , case-insensitive >= 1.2 && < 1.3 50 | , base64-bytestring >= 1.0 && < 1.1 51 | , async >= 2.2 && < 2.3 52 | , stm 53 | , random-bytestring 54 | default-language: Haskell2010 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # N2O for Haskell 2 | 3 | [![Build Status](https://travis-ci.org/xafizoff/n2o.svg?branch=master)](https://travis-ci.org/xafizoff/n2o) 4 | 5 | Features 6 | -------- 7 | 8 | * Endpoints: poor man's WebSocket and static HTTP server 9 | * Formatters: BERT 10 | * Protocols: NITRO 11 | * High Performance Protocol Relay 12 | * Smallest possible codebase — 500 LOC 13 | 14 | N2O defines a way we scale protocols, database schema, applications and 15 | services across companies, formatters, views and presentation layers. 16 | At the core N2O folds a list of protocols and their handlers providing 17 | a minimal type-level specification for general purpose application protocol tract. 18 | 19 | As example this Haskell version of N2O is shipped with Nitro protocol 20 | implementation, that listens the tract and push prerendered JavaScript 21 | events back to the channel. This smart and simple reactive way 22 | of client-server interaction first was used by Rusty Klophaus in 23 | his Nitrogen web framework, that was pushed forward since then in 24 | N2O by Andy Melnikov and Marat Khafizov. 25 | 26 | Setup 27 | ----- 28 | 29 | ```sh 30 | stack build 31 | stack exec n2o-sample 32 | open http://localhost:3000/samples/static/index.html 33 | ``` 34 | 35 | Nitro Protocol Demo 36 | ------------------- 37 | 38 | ```haskell 39 | {-# LANGUAGE FlexibleContexts, OverloadedStrings, DeriveGeneric, DeriveAnyClass #-} 40 | module Main (main) where 41 | 42 | import Network.N2O 43 | import Network.N2O.Web hiding (Event) 44 | import GHC.Generics (Generic) 45 | import Data.Serialize (Serialize) 46 | 47 | data Example = Greet deriving (Show, Eq, Generic, Serialize) 48 | 49 | main = runServer "localhost" 3000 cx 50 | 51 | cx :: Cx Example 52 | cx = mkCx{ cxMiddleware=[router] 53 | , cxProtos = [nitroProto] 54 | } 55 | 56 | router cx@Context{cxReq=Req{reqPath=path}} = 57 | let handle = case path of 58 | "/ws/samples/static/index.html" -> index 59 | "/ws/samples/static/about.html" -> about 60 | _ -> index 61 | in cx{cxHandler=mkHandler handle} 62 | 63 | index Init = do 64 | updateText "system" "What is your name?" 65 | wire button{id_="send", postback=Just Greet, source=["name"]} 66 | index (Message Greet) = do 67 | Just name <- get "name" -- wf:q/1 68 | updateText "system" ("Hello, " <> jsEscape name <> "!") 69 | index ev = liftIO $ putStrLn ("Unknown event" <> show ev) 70 | about Init = updateText "app" "This is the N2O Hello World App" 71 | about ev = liftIO $ putStrLn ("Unknown event " <> show ev) 72 | ``` 73 | 74 | Credits 75 | ------- 76 | 77 | * Andy Melnikov 78 | * Marat Khafizov 79 | * Maxim Sokhatsky 80 | 81 | -------------------------------------------------------------------------------- /man/int.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | INT 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 |
23 | 24 |

INT

25 |
26 | 27 |
28 |
29 |

INTRO

30 |

The Network.N2O.Internal module contains request and context definition, state and N2O monad.

31 |
32 |
33 |

REQUEST

34 |
Listing 1. HTTP Request
35 | type Header = (BS.ByteString, BS.ByteString) 36 | data Req = Req 37 | { reqPath :: BS.ByteString 38 | , reqMeth :: BS.ByteString 39 | , reqVers :: BS.ByteString 40 | , reqHead :: [Header] 41 | } 42 |
43 |
Listing 2. HTTP Response
44 | data Resp = Resp 45 | { respCode :: Int 46 | , respHead :: [Header] 47 | , respBody :: BS.ByteString 48 | } deriving (Show) 49 |
50 |
51 |
52 |

CONTEXT

53 |
Listing 3. N2O Protocol Closure
54 | data Event a 55 | = Init 56 | | Message a 57 | | Terminate 58 | deriving Show 59 |
60 |
Listing 4. N2O Protocol Result
61 | data Result a 62 | = Reply a 63 | | Ok 64 | | Unknown 65 | | Empty 66 | deriving (Show, Eq) 67 |
68 |
Listing 5. N2O Context Record
69 | data Context (f :: * -> *) a where Context :: 70 | { cxHandler :: Event a -> N2O f a (Result a) 71 | , cxReq :: Req 72 | , cxMiddleware :: [Context f a -> Context f a] 73 | , cxProtos :: [Proto f a] 74 | , cxActions :: BS.ByteString 75 | , cxDict :: M.Map BS.ByteString BS.ByteString 76 | } -> Context f a 77 |
78 |
Listing 6. N2O Context Constructor
79 | mkCx = Context 80 | { cxReq = undefined 81 | , cxHandler = undefined 82 | , cxMiddleware = [] 83 | , cxProtos = [] 84 | , cxActions = "" 85 | , cxDict = M.empty 86 | } 87 |
88 |
89 |
90 |

MONAD

91 | 92 |
Listing 7. N2O Monad
93 | type Proto (f :: * -> *) a 94 | = (f a) -> N2O f a (Result (f a)) 95 | 96 | type N2O f a 97 | = ReaderT (IORef (Context f a)) IO 98 |
99 | 100 |

put :: (B.Serialize bin) => BS.ByteString -> bin -> N2O f a ()

101 |

get :: (B.Serialize bin) => BS.ByteString -> N2O f a (Maybe bin)

102 |

getContext :: N2O f a (Context f a)

103 |

nop :: Result a

104 |

protoRun :: f a -> [Proto f a] -> N2O f a (Result (f a))

105 |
106 |
107 |

This module may refer to: 108 | INT, 109 | CORE 110 |

111 |
112 |
113 | 114 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | N2O 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 |
23 | 24 |

N2O

25 |
26 | 27 | 59 |
60 |
61 | 62 |

N2O Haskell

63 | 64 |

N2O defines a way we scale protocols, database schema, applications and 65 | services across companies, formatters, views and presentation layers. 66 | At the core N2O folds a list of protocols and their handlers providing 67 | a minimal type-level specification for general purpose application protocol tract.

68 | 69 |

As example this Haskell version of N2O is shipped with Nitro protocol 70 | implementation, that listens the tract and push prerendered JavaScript 71 | events back to the channel. This smart and simple reactive way 72 | of client-server interaction first was used by Rusty Klophaus in 73 | his Nitrogen web framework, that was pushed forward since then in 74 | N2O by Andy Melnikov and Marat Khafizov.

75 | 76 |

The philosophy behind N2O is a simplicity, cleanness, and purity. 77 | The core of N2O should be no more than 500 LOC.

78 | 79 |
80 |
Listing 2. N2O Context Setup
81 | 82 | data Example = Greet deriving (Show, Eq, Read) 83 | 84 | main = runServer "localhost" 3000 cx 85 | cx = createCx router 86 | router cx@Cx{cxReq=Req{reqPath=path}} = 87 | let handler = case path of 88 | "/ws/samples/static/index.html" -> index 89 | "/ws/samples/static/about.html" -> about 90 | _ -> index 91 | in traceShow path cx{cxHandler=handler} 92 |
93 | 94 |

The idea to send prerendered JavaScript events over the 95 | wire belongs to Rusty Klophaus who made Nitrogen Erlang web framework. 96 | Later this was refined by N2O team and now is available 97 | in its purity of Haskell.

98 | 99 |
100 |
Listing 3. NITRO Web Protocol
101 | 102 | index Init = do 103 | updateText "system" "What is your name?" 104 | wireEl button{id="send", postback=Just Greet, source=["name"]} 105 | 106 | index (Message Greet) = do 107 | Just name <- get "name" -- wf:q/1 108 | updateText "system" ("Hello, " <> jsEscape name <> "!") 109 | 110 | about Init = 111 | updateText "app" "This is the N2O Hello World App" 112 |
113 | 114 |

The N2O does not limit in developing only web applications. 115 | N2O stack of protocols covers bus, storage, process interfaces. 116 | Subscribe to follow Haskell implementations.

117 |
118 | 119 |
120 |

You may want to read: 121 | WEB, 122 | INT. 123 |

124 |
125 |
126 | 127 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /src/Network/N2O/Internal.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE RankNTypes #-} 2 | {-# LANGUAGE OverloadedStrings, KindSignatures, GADTs #-} 3 | 4 | {-| 5 | Module : Network.N2O.Internal 6 | Description : Basic types and core functions 7 | Copyright : (c) Marat Khafizov, 2018 8 | License : BSD-3 9 | Maintainer : xafizoff@gmail.com 10 | Stability : experimental 11 | Portability : not portable 12 | 13 | Basic types and core functions 14 | 15 | -} 16 | module Network.N2O.Internal 17 | ( module Network.N2O.Internal 18 | , module Control.Monad.Trans.Reader 19 | ) where 20 | import qualified Data.Serialize as B 21 | import qualified Data.ByteString as BS 22 | import qualified Data.Text as T 23 | import Data.IORef 24 | import Data.Map.Strict (Map, (!?), insert) 25 | import qualified Data.Map.Strict as M 26 | import Control.Monad.Trans.Reader 27 | import Control.Monad.IO.Class (liftIO) 28 | import Control.Exception (SomeException) 29 | import Control.Concurrent.STM 30 | import Control.Concurrent.STM.TChan 31 | import Control.Monad (forM_) 32 | import Control.Concurrent (ThreadId) 33 | 34 | -- | An HTTP header 35 | type Header = (BS.ByteString, BS.ByteString) 36 | 37 | -- | An HTTP request 38 | data Req = Req 39 | { reqPath :: BS.ByteString 40 | , reqMeth :: BS.ByteString 41 | , reqVers :: BS.ByteString 42 | , reqHead :: [Header] 43 | } 44 | 45 | -- | The N2O context data type 46 | -- This is the key data type of the N2O. @(f :: * -> *)@ - type constructor 47 | -- for the protocol handler's input type. @(a :: *)@ - base type for the 48 | -- event handler's input type. I.e. @(f a)@ gives input type for the 49 | -- protocol handler. @(Event a)@ gives input type for the event handler. 50 | data Context (f :: * -> *) a where 51 | Context :: 52 | { cxHandler :: Event a -> N2O f a (Result a) 53 | , cxReq :: Req 54 | , cxMiddleware :: [Context f a -> Context f a] 55 | , cxProtos :: [Proto f a] 56 | , cxActions :: TVar BS.ByteString 57 | , cxDict :: TVar (M.Map BS.ByteString BS.ByteString) 58 | , cxPubSub :: TVar (M.Map BS.ByteString [TChan (f a)]) 59 | , cxInBox :: TChan (f a) 60 | , cxOutBox :: TChan (f a) 61 | , cxSessions :: TVar (M.Map BS.ByteString BS.ByteString) 62 | , cxTid :: ThreadId 63 | } -> Context f a 64 | 65 | -- | Result of the message processing 66 | data Result a 67 | = Reply a 68 | | Ok 69 | | Unknown 70 | | Empty 71 | deriving (Show, Eq) 72 | 73 | -- | N2O protocol handler 74 | type Proto (f :: * -> *) a = (f a) -> N2O f a (Result (f a)) 75 | 76 | -- | Event data type 77 | data Event a 78 | = Init 79 | | Message a 80 | | Terminate 81 | deriving Show 82 | 83 | type N2O f a = ReaderT (Context f a) IO 84 | 85 | sub :: BS.ByteString -> N2O f a () 86 | sub topic = do 87 | cx <- ask 88 | liftIO $ atomically $ do 89 | Context{cxPubSub = pubsub,cxOutBox = chan} <- pure cx 90 | modifyTVar pubsub $ \m -> M.alter (\mbs -> let s = case mbs of {Just s -> s; _ -> []} in Just $ ins chan s) topic m 91 | 92 | unsub topic = do 93 | cx <- ask 94 | liftIO $ atomically $ do 95 | Context{cxPubSub = pubsub,cxOutBox = chan} <- pure cx 96 | modifyTVar pubsub $ \m -> M.alter (\mbs -> let s = case mbs of {Just s -> s; _ -> []} in Just $ del [] chan s) topic m 97 | 98 | pub topic a = do 99 | cx@Context{cxPubSub = pubsub} <- ask 100 | l <- liftIO $ atomically $ do 101 | m <- readTVar pubsub 102 | l <- pure $ case M.lookup topic m of {Just s -> s; _ -> []} 103 | forM_ l (\chan -> do {rChan <- dupTChan chan; writeTChan chan a}) 104 | pure l 105 | return () 106 | 107 | fnd x [] = False 108 | fnd x (y:ys) = if x == y then True else fnd x ys 109 | 110 | ins x ys = if fnd x ys then ys else x:ys 111 | 112 | del acc _ [] = acc 113 | del acc x (y:ys) = if x == y then del acc x ys else del (y:acc) x ys 114 | 115 | -- | Put data to the local state 116 | put :: (B.Serialize bin) => BS.ByteString -> bin -> N2O f a () 117 | put k v = do 118 | cx <- ask 119 | liftIO $ atomically $ modifyTVar (cxDict cx) (\dict -> M.insert k (B.encode v) dict) 120 | 121 | -- | Get data from the local state 122 | get :: (B.Serialize bin) => BS.ByteString -> N2O f a (Maybe bin) 123 | get k = do 124 | cx <- ReaderT return 125 | m <- liftIO $ atomically $ readTVar $ cxDict cx 126 | case m !? k of 127 | Just v -> case (B.decode v) of 128 | Right x -> return $ Just x 129 | _ -> return Nothing 130 | _ -> return Nothing 131 | 132 | getContext :: N2O f a (Context f a) 133 | getContext = do 134 | cx <- ask 135 | pure cx 136 | 137 | -- | 'Context' constructor 138 | mkCx = Context 139 | { cxReq = undefined 140 | , cxHandler = undefined 141 | , cxMiddleware = [] 142 | , cxProtos = [] 143 | , cxActions = undefined 144 | , cxDict = undefined 145 | , cxPubSub = undefined 146 | , cxOutBox = undefined 147 | , cxInBox = undefined 148 | , cxTid = undefined 149 | , cxSessions = undefined 150 | } 151 | 152 | -- | 'Req' constructor 153 | mkReq = Req { reqPath = "/", reqMeth = "GET", reqVers = "HTTP/1.1", reqHead = [] } 154 | 155 | -- | NO-OP result 156 | nop :: Result a 157 | nop = Empty 158 | 159 | -- | N2O protocol loop 160 | protoRun :: f a -> [Proto f a] -> N2O f a (Result (f a)) 161 | protoRun = loop 162 | where 163 | loop :: f a -> [Proto f a] -> N2O f a (Result (f a)) 164 | loop _ [] = return nop 165 | loop msg (proto:protos) = do 166 | res <- proto msg 167 | case res of 168 | Unknown -> loop msg protos 169 | Empty -> return Empty 170 | Reply msg1 -> return $ Reply msg1 171 | a -> loop msg protos 172 | 173 | -------------------------------------------------------------------------------- /src/Network/N2O/Web/Http.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE LambdaCase, RecordWildCards #-} 2 | {- | Naive implementation of HTTP server -} 3 | module Network.N2O.Web.Http ( runServer ) where 4 | 5 | import Control.Concurrent 6 | import Control.Exception (catch, finally, SomeException(..), bracket, try) 7 | import qualified Data.ByteString as BS 8 | import qualified Data.ByteString.Char8 as C 9 | import Network.Socket hiding (recv, send) 10 | import Network.Socket.ByteString 11 | import Network.N2O.Internal 12 | import Network.N2O.Web.WebSockets (wsApp, N2OProto, mkPending) 13 | import Prelude hiding (takeWhile) 14 | import Data.Attoparsec.ByteString hiding (try) 15 | import Data.CaseInsensitive (mk) 16 | import qualified Network.WebSockets as WS 17 | import Control.Concurrent.Async 18 | import System.IO 19 | import Text.Printf 20 | import qualified Data.Map.Strict as M 21 | import Control.Concurrent.STM.TVar (newTVarIO) 22 | 23 | data Resp = Resp 24 | { respCode :: Int 25 | , respHead :: [Header] 26 | , respBody :: BS.ByteString 27 | } deriving (Show) 28 | 29 | mkResp = Resp { respCode = 200, respHead = [], respBody = BS.empty } 30 | 31 | runServer :: String -> Int -> Context N2OProto a -> IO () 32 | runServer host port cx = do 33 | hSetBuffering stdout NoBuffering 34 | printf "Started server at %s:%d\n" host port 35 | pubsub <- newTVarIO M.empty 36 | sessions <- newTVarIO M.empty 37 | acceptor <- async $ withSocketsDo $ do 38 | addr <- resolve host (show port) 39 | bracket (open addr) close (acceptConnections cx{cxPubSub = pubsub,cxSessions = sessions}) 40 | wait acceptor 41 | where 42 | resolve host port = do 43 | let hints = defaultHints {addrSocketType = Stream, addrFlags = [AI_PASSIVE]} 44 | addr:_ <- getAddrInfo (Just hints) (Just host) (Just port) 45 | return addr 46 | open addr = do 47 | sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr) 48 | setSocketOption sock ReuseAddr 1 49 | bind sock (addrAddress addr) 50 | listen sock 10 51 | return sock 52 | 53 | acceptConnections :: Context N2OProto a -> Socket -> IO () 54 | acceptConnections cx sock = do 55 | (handle, host_addr) <- accept sock 56 | forkIO (catch 57 | (talk cx handle host_addr `finally` close handle) 58 | (\e@(SomeException _) -> print e)) 59 | acceptConnections cx sock 60 | 61 | talk :: Context N2OProto a -> Socket -> SockAddr -> IO () 62 | talk cx sock addr = do 63 | bs <- recv sock 4096 64 | let either = parseReq bs 65 | case either of 66 | Left resp -> sendResp sock resp 67 | Right req -> 68 | if needUpgrade req 69 | then do 70 | pending <- mkPending WS.defaultConnectionOptions sock req 71 | wsApp cx {cxReq = req} pending 72 | else fileResp (preparePath $ C.unpack $ reqPath req) (sendResp sock) 73 | where 74 | preparePath ('.':path) = preparePath path 75 | preparePath ('/':path) = preparePath path 76 | preparePath path = path 77 | 78 | status = \case 79 | 200 -> "OK" 80 | 400 -> "Bad Request" 81 | 404 -> "Not Found" 82 | 500 -> "Internal Server Eror" 83 | _ -> "" 84 | 85 | calcLen resp@Resp{..} = (C.pack "Content-Length", C.pack $ show $ BS.length respBody) : respHead 86 | 87 | sendResp :: Socket -> Resp -> IO () 88 | sendResp sock resp@Resp {..} = do 89 | let headers = fmap (\(k, v) -> k <> C.pack ": " <> v <> C.pack "\r\n") (calcLen resp) 90 | cmd = C.pack "HTTP/1.1 " <> C.pack (show respCode) <> C.pack " " <> C.pack (status respCode) <> C.pack "\r\n" 91 | x = cmd : headers 92 | y = x ++ [C.pack "\r\n", respBody] 93 | send sock (mconcat y) 94 | return () 95 | 96 | fileResp :: FilePath -> (Resp -> IO ()) -> IO () 97 | fileResp path respond = do 98 | res <- try (BS.readFile path) 99 | let (status, content) = case res of 100 | Left e@(SomeException _) -> (404, C.pack "File Not Found") 101 | Right content -> (200, content) 102 | respond $ mkResp {respCode = status, respBody = content} 103 | 104 | parseReq :: BS.ByteString -> Either Resp Req 105 | parseReq bs = case parseOnly reqParser bs of 106 | Left _ -> Left $ mkResp {respCode = 400} 107 | Right req -> Right req 108 | 109 | needUpgrade :: Req -> Bool 110 | needUpgrade req = 111 | case getHeader (C.pack "upgrade") (reqHead req) of 112 | Nothing -> False 113 | Just h -> mk (snd h) == mk (C.pack "websocket") 114 | 115 | isKeepAlive :: Req -> Bool 116 | isKeepAlive req = 117 | case getHeader (C.pack "connection") (reqHead req) of 118 | Nothing -> False 119 | Just h -> mk (snd h) == mk (C.pack "keep-alive") 120 | 121 | getHeader :: BS.ByteString -> [Header] -> Maybe Header 122 | getHeader _ [] = Nothing 123 | getHeader k (h:hs) 124 | | mk k == mk (fst h) = Just h 125 | | otherwise = getHeader k hs 126 | 127 | crlf = (||) <$> (==10) <*> (==13) 128 | isSpace = (== 32) 129 | 130 | reqParser :: Parser Req 131 | reqParser = do 132 | cmd <- takeWhile1 $ not.isSpace 133 | skipWhile isSpace 134 | path <- takeWhile1 $ not.isSpace 135 | skipWhile isSpace 136 | ver <- takeWhile1 $ not.crlf 137 | skipWhile crlf 138 | headers <- many' headerParser 139 | skipWhile crlf 140 | takeByteString 141 | endOfInput 142 | return $ mkReq {reqMeth = cmd, reqPath = path, reqVers = ver, reqHead = headers } 143 | 144 | headerParser :: Parser (BS.ByteString, BS.ByteString) 145 | headerParser = do 146 | name <- takeWhile1 (\b -> b /= 58 && b /= 10 && b /= 13) 147 | skip (== 58) 148 | skipWhile isSpace 149 | val <- takeWhile1 $ not.crlf 150 | skipWhile crlf 151 | return (name, val) 152 | -------------------------------------------------------------------------------- /src/Network/N2O/Web/WebSockets.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances, TypeSynonymInstances, OverloadedStrings, FlexibleContexts #-} 2 | 3 | module Network.N2O.Web.WebSockets (wsApp, mkPending, N2OProto(N2OClient), nitroProto, mkHandler, Cx) where 4 | 5 | import Control.Exception (catch, finally) 6 | import Control.Monad (forM_, forever, mapM_) 7 | import Control.Monad.IO.Class (MonadIO, liftIO) 8 | import Data.BERT 9 | import qualified Data.Serialize as B 10 | import qualified Data.ByteString.Lazy as BL 11 | import qualified Data.ByteString as BS 12 | import qualified Data.ByteString.Char8 as C8 13 | import Data.CaseInsensitive (mk) 14 | import Data.IORef 15 | import Data.Maybe (fromJust) 16 | import qualified Data.Map.Strict as M 17 | import Data.Text as T 18 | import Data.Text.Encoding as T 19 | import Network.N2O.Internal 20 | import Network.Socket (Socket) 21 | import Web.Nitro 22 | import qualified Network.WebSockets as WS 23 | import qualified Network.WebSockets.Connection as WSConn 24 | import qualified Network.WebSockets.Stream as WSStream 25 | import Control.Concurrent (forkIO, myThreadId) 26 | import Control.Concurrent.STM.TVar (TVar, readTVar, newTVarIO) 27 | import Control.Concurrent.STM (atomically, modifyTVar, writeTVar) 28 | import Control.Concurrent.STM.TChan (newBroadcastTChan, dupTChan, readTChan, writeTChan) 29 | import Data.ByteString.Random (random) 30 | import Text.Printf (printf) 31 | 32 | -- | Top level sum of protocols 33 | data N2OProto a 34 | = N2ONitro (Nitro a) 35 | | N2OClient a 36 | | Io BS.ByteString 37 | BS.ByteString 38 | | Nop 39 | --deriving (Show) 40 | 41 | type Cx a = Context N2OProto a 42 | type CxHandler a = Cx a -> Cx a 43 | 44 | instance NITRO (N2O N2OProto a) where 45 | putActions a = do 46 | cx <- ask 47 | liftIO $ atomically $ writeTVar (cxActions cx) a 48 | getActions = do 49 | Context{cxActions = tvar} <- ask 50 | acts <- liftIO $ atomically $ readTVar tvar 51 | return acts 52 | 53 | mkHandler h = \m -> do 54 | x <- h m 55 | return Empty 56 | 57 | nitroProto :: (Show a, B.Serialize a) => Proto N2OProto a 58 | nitroProto message = do 59 | cx@Context {cxHandler = handle,cxSessions = sess} <- getContext 60 | case message of 61 | msg@(N2ONitro (NitroInit pid)) -> do 62 | pid1 <- case pid of {"" -> do {rnd <- liftIO $ random(16); return $ hex rnd}; _ -> return pid} 63 | liftIO $ atomically $ modifyTVar sess $ \m -> M.alter (\mb -> let s = case mb of {Just s -> s; _ -> ""} in Just s) pid1 m 64 | handle Init 65 | acts <- getActions 66 | putActions "" 67 | return $ Reply $ reply acts pid1 68 | msg@(N2ONitro (NitroPickle _source pickled linked)) -> do 69 | forM_ (M.toList linked) (uncurry put) 70 | depickled <- depickle pickled 71 | case depickled of 72 | Just x -> do 73 | handle (Message x) 74 | acts <- getActions 75 | putActions "" 76 | return $ Reply (reply acts "") 77 | _ -> return Unknown 78 | msg@(N2OClient a) -> do 79 | handle $ Message a 80 | acts <- getActions 81 | putActions "" 82 | return $ Reply (reply acts "") 83 | msg@(N2ONitro NitroDone) -> do 84 | handle Terminate 85 | return Empty 86 | where 87 | reply eval dat = Io eval dat 88 | hex :: C8.ByteString -> C8.ByteString 89 | hex = C8.pack . Prelude.concatMap (printf "%02x") . C8.unpack 90 | 91 | wsApp :: Context N2OProto a -> WS.ServerApp 92 | wsApp cx pending = do 93 | tid <- myThreadId 94 | (chanIn,chanOut) <- atomically $ do 95 | chanOut <- newBroadcastTChan 96 | chanIn <- dupTChan chanOut 97 | pure (chanIn,chanOut) 98 | acts <- newTVarIO "" 99 | dict <- newTVarIO M.empty 100 | let path = WS.requestPath $ WS.pendingRequest pending 101 | cx1 = cx {cxReq = mkReq {reqPath = path}, cxInBox = chanIn, cxOutBox = chanOut, cxTid = tid, cxActions = acts, cxDict = dict} 102 | handlers = cxMiddleware cx1 103 | applyHandlers hs ctx = 104 | case hs of 105 | [] -> ctx 106 | (h:hs') -> applyHandlers hs' (h ctx) 107 | cx2 = applyHandlers handlers cx1 108 | conn <- WS.acceptRequest pending 109 | WS.forkPingThread conn 30 110 | -- rChan <- atomically $ dupTChan chan 111 | forkIO (pump conn cx2) 112 | listen conn cx2 113 | 114 | -- | Make pending WS request from N2O request 115 | mkPending :: WS.ConnectionOptions -> Socket -> Req -> IO WS.PendingConnection 116 | mkPending opts sock req = do 117 | stream <- WSStream.makeSocketStream sock 118 | let requestHead = 119 | WS.RequestHead 120 | { WS.requestPath = reqPath req 121 | , WS.requestSecure = False 122 | , WS.requestHeaders = fmap (\(k, v) -> (mk k, v)) (reqHead req) 123 | } 124 | return 125 | WSConn.PendingConnection 126 | { WSConn.pendingOptions = opts 127 | , WSConn.pendingRequest = requestHead 128 | , WSConn.pendingOnAccept = \_ -> return () 129 | , WSConn.pendingStream = stream 130 | } 131 | 132 | pump conn cx@Context{cxInBox = chan} = do 133 | forever $ do 134 | msg <- atomically $ do 135 | readTChan chan 136 | reply <- runReaderT (protoRun msg $ cxProtos cx) cx 137 | process conn reply 138 | 139 | listen :: WS.Connection -> Context N2OProto a -> IO () 140 | listen conn cx = 141 | do pid <- receiveN2O conn cx 142 | Context {cxProtos = protos, cxOutBox = chan, cxTid = tid} <- pure cx 143 | forever $ do 144 | message <- WS.receiveDataMessage conn 145 | case message of 146 | WS.Text "PING" _ -> WS.sendTextData conn ("PONG" :: T.Text) 147 | WS.Binary bin -> 148 | case B.decode (BL.toStrict bin) of 149 | Right term -> 150 | case fromBert term of 151 | Just msg -> do 152 | atomically $ writeTChan chan msg 153 | _ -> return () 154 | _ -> return () 155 | _ -> error "Unknown message" 156 | `finally` do 157 | Context {cxProtos = protos} <- pure cx 158 | runReaderT (protoRun (N2ONitro NitroDone) protos) cx 159 | return () 160 | 161 | process conn reply = 162 | case reply of 163 | Reply a -> WS.sendBinaryData conn (B.encode $ toBert a) 164 | x -> error $ "Unknown response type" 165 | 166 | receiveN2O conn cx = do 167 | message <- WS.receiveDataMessage conn 168 | Context {cxProtos = protos} <- pure cx 169 | case message of 170 | WS.Binary _ -> error "Protocol violation: expected text message" 171 | WS.Text "" _ -> error "Protocol violation: got empty text" 172 | WS.Text bs _ -> 173 | case C8.stripPrefix "N2O," (BL.toStrict bs) of 174 | Just pid -> do 175 | reply <- runReaderT (protoRun (N2ONitro $ NitroInit pid) protos) cx 176 | process conn reply 177 | return pid 178 | _ -> error "Protocol violation" 179 | 180 | -- | Convert Binary Erlang Terms (BERT) to the 'N2OProto' specification 181 | fromBert :: Term -> Maybe (N2OProto a) 182 | fromBert (TupleTerm [AtomTerm "init", BytelistTerm pid]) = 183 | Just $ N2ONitro (NitroInit pid) 184 | fromBert (TupleTerm [AtomTerm "pickle", BinaryTerm source, BinaryTerm pickled, ListTerm linked]) = 185 | Just $ N2ONitro (NitroPickle source pickled (convert linked)) 186 | where 187 | convert [] = M.empty 188 | convert (TupleTerm [AtomTerm k, BinaryTerm v]:vs) = 189 | M.insert (C8.pack k) v (convert vs) 190 | fromBert _ = Nothing 191 | 192 | toBert :: N2OProto a -> Term 193 | toBert (Io eval dat) = 194 | TupleTerm [AtomTerm "io", BinaryTerm eval, BinaryTerm dat] 195 | -------------------------------------------------------------------------------- /src/Data/BERT.hs: -------------------------------------------------------------------------------- 1 | {- 2 | Copyright (c) 2009 marius a. eriksen (marius@monkey.org) 3 | (c) 2013 Roman Cheplyaka 4 | All rights reserved. 5 | -} 6 | {-# LANGUAGE TypeSynonymInstances, FlexibleInstances, LambdaCase #-} 7 | module Data.BERT (Term(..)) where 8 | 9 | import Control.Monad 10 | import Control.Applicative 11 | import Data.Bits 12 | import Data.Char 13 | import Data.Int 14 | import Data.Word 15 | import Data.Serialize 16 | import Data.Serialize.Put 17 | import Data.Serialize.Get 18 | import Data.List 19 | import Data.Time 20 | import Data.ByteString (ByteString) 21 | import qualified Data.ByteString as B 22 | import qualified Data.ByteString.Char8 as C 23 | import Data.Map (Map) 24 | import qualified Data.Map as Map 25 | import Text.Printf 26 | 27 | -- | A single BERT term. 28 | data Term 29 | -- Simple (erlang) terms: 30 | = IntTerm Int 31 | | FloatTerm Float 32 | | AtomTerm String 33 | | TupleTerm [Term] 34 | | BytelistTerm ByteString 35 | | ListTerm [Term] 36 | | BinaryTerm ByteString 37 | | BigintTerm Integer 38 | | BigbigintTerm Integer 39 | -- Composite (BERT specific) terms: 40 | | NilTerm 41 | | BoolTerm Bool 42 | | DictionaryTerm [(Term, Term)] 43 | | TimeTerm UTCTime 44 | | RegexTerm String [String] 45 | deriving (Eq, Ord, Show, Read) 46 | 47 | -- The 0th-hour as per the BERT spec. 48 | zeroHour = UTCTime (read "1970-01-01") 0 49 | 50 | decomposeTime :: UTCTime -> (Int, Int, Int) 51 | decomposeTime t = (mS, s, uS) 52 | where 53 | d = diffUTCTime t zeroHour 54 | (mS, s) = floor d `divMod` 1000000 55 | uS = floor $ 1000000 * snd (properFraction d) 56 | 57 | composeTime :: (Int, Int, Int) -> UTCTime 58 | composeTime (mS, s, uS) = addUTCTime seconds zeroHour 59 | where 60 | mS' = fromIntegral mS 61 | s' = fromIntegral s 62 | uS' = fromIntegral uS 63 | seconds = (mS' * 1000000) + s' + (uS' / 1000000) 64 | 65 | -- Another design would be to split the Term type into 66 | -- SimpleTerm|CompositeTerm, and then do everything in one go, but 67 | -- that complicates syntax and semantics for end users. Let's do this 68 | -- one ugly thing instead, eh? 69 | ct b rest = TupleTerm $ [AtomTerm "bert", AtomTerm b] ++ rest 70 | compose NilTerm = ListTerm [] 71 | compose (BoolTerm True) = ct "true" [] 72 | compose (BoolTerm False) = ct "false" [] 73 | compose (DictionaryTerm kvs) = 74 | ct "dict" [ListTerm $ map (\(k, v) -> TupleTerm [k, v]) kvs] 75 | compose (TimeTerm t) = 76 | ct "time" [IntTerm mS, IntTerm s, IntTerm uS] 77 | where 78 | (mS, s, uS) = decomposeTime t 79 | compose (RegexTerm s os) = 80 | ct "regex" [BytelistTerm (C.pack s), 81 | TupleTerm [ListTerm $ map AtomTerm os]] 82 | compose _ = error "invalid composite term" 83 | 84 | showTerm (IntTerm x) = show x 85 | showTerm (FloatTerm x) = printf "%15.15e" x 86 | showTerm (AtomTerm "") = "" 87 | showTerm (AtomTerm a@(x:xs)) 88 | | isAsciiLower x = a 89 | | otherwise = "'" ++ a ++ "'" 90 | showTerm (TupleTerm ts) = 91 | "{" ++ intercalate ", " (map showTerm ts) ++ "}" 92 | showTerm (BytelistTerm bs) = show $ C.unpack bs 93 | showTerm (ListTerm ts) = 94 | "[" ++ intercalate ", " (map showTerm ts) ++ "]" 95 | showTerm (BinaryTerm b) 96 | | all (isAscii . chr . fromIntegral) (B.unpack b) = 97 | wrap $ "\"" ++ C.unpack b ++ "\"" 98 | | otherwise = 99 | wrap $ intercalate ", " $ map show $ B.unpack b 100 | where 101 | wrap x = "<<" ++ x ++ ">>" 102 | showTerm (BigintTerm x) = show x 103 | showTerm (BigbigintTerm x) = show x 104 | -- All other terms are composite: 105 | showTerm t = showTerm . compose $ t 106 | 107 | class BERT a where 108 | -- | Introduce a 'Term' from a Haskell value. 109 | showBERT :: a -> Term 110 | -- | Attempt to read a haskell value from a 'Term'. 111 | readBERT :: Term -> Either String a 112 | 113 | -- Herein are some instances for common Haskell data types. To do 114 | -- anything more complicated, you should make your own instance. 115 | 116 | instance BERT Term where 117 | showBERT = id 118 | readBERT = return 119 | 120 | instance BERT Int where 121 | showBERT = IntTerm 122 | readBERT (IntTerm value) = return value 123 | readBERT _ = fail "Invalid integer type" 124 | 125 | instance BERT Bool where 126 | showBERT = BoolTerm 127 | readBERT (BoolTerm x) = return x 128 | readBERT _ = fail "Invalid bool type" 129 | 130 | instance BERT Integer where 131 | showBERT = BigbigintTerm 132 | readBERT (BigintTerm x) = return x 133 | readBERT (BigbigintTerm x) = return x 134 | readBERT _ = fail "Invalid integer type" 135 | 136 | instance BERT Float where 137 | showBERT = FloatTerm 138 | readBERT (FloatTerm value) = return value 139 | readBERT _ = fail "Invalid floating point type" 140 | 141 | instance BERT String where 142 | showBERT = BytelistTerm . C.pack 143 | readBERT (BytelistTerm x) = return $ C.unpack x 144 | readBERT (BinaryTerm x) = return $ C.unpack x 145 | readBERT (AtomTerm x) = return x 146 | readBERT (ListTerm xs) = map chr <$> mapM readBERT xs 147 | readBERT _ = fail "Invalid string type" 148 | 149 | instance BERT ByteString where 150 | showBERT = BytelistTerm 151 | readBERT (BytelistTerm value) = return value 152 | readBERT _ = fail "Invalid bytestring type" 153 | 154 | instance (BERT a) => BERT [a] where 155 | showBERT xs = ListTerm $ map showBERT xs 156 | readBERT (ListTerm xs) = mapM readBERT xs 157 | readBERT _ = fail "Invalid list type" 158 | 159 | instance (BERT a, BERT b) => BERT (a, b) where 160 | showBERT (a, b) = TupleTerm [showBERT a, showBERT b] 161 | readBERT (TupleTerm [a, b]) = liftM2 (,) (readBERT a) (readBERT b) 162 | readBERT _ = fail "Invalid tuple(2) type" 163 | 164 | instance (BERT a, BERT b, BERT c) => BERT (a, b, c) where 165 | showBERT (a, b, c) = TupleTerm [showBERT a, showBERT b, showBERT c] 166 | readBERT (TupleTerm [a, b, c]) = 167 | liftM3 (,,) (readBERT a) (readBERT b) (readBERT c) 168 | readBERT _ = fail "Invalid tuple(3) type" 169 | 170 | instance (BERT a, BERT b, BERT c, BERT d) => BERT (a, b, c, d) where 171 | showBERT (a, b, c, d) = 172 | TupleTerm [showBERT a, showBERT b, showBERT c, showBERT d] 173 | readBERT (TupleTerm [a, b, c, d]) = 174 | liftM4 (,,,) (readBERT a) (readBERT b) (readBERT c) (readBERT d) 175 | readBERT _ = fail "Invalid tuple(4) type" 176 | 177 | instance (Ord k, BERT k, BERT v) => BERT (Map k v) where 178 | showBERT m = DictionaryTerm 179 | $ map (\(k, v) -> (showBERT k, showBERT v)) (Map.toList m) 180 | readBERT (DictionaryTerm kvs) = 181 | Map.fromList <$> 182 | mapM (\ (k, v) -> liftM2 (,) (readBERT k) (readBERT v)) kvs 183 | readBERT _ = fail "Invalid map type" 184 | 185 | -- Binary encoding & decoding. 186 | instance Serialize Term where 187 | put term = putWord8 131 >> putTerm term 188 | get = getWord8 >>= \case 189 | 131 -> getTerm 190 | _ -> fail "bad magic" 191 | 192 | -- | Binary encoding of a single term (without header) 193 | putTerm :: Term -> Put 194 | putTerm (IntTerm value) 195 | | 0 <= value && value < 256 = tag 97 >> put8u value 196 | | otherwise = tag 98 >> put32s value 197 | putTerm (FloatTerm value) = tag 99 >> (putL . C.pack . pad $ printf "%15.15e" value) 198 | where 199 | pad s = s ++ replicate (31 - length s) '\0' 200 | putTerm (AtomTerm value) 201 | | len < 256 = tag 100 >> put16u len >> putL (C.pack value) 202 | | otherwise = fail "BERT atom too long (>= 256)" 203 | where 204 | len = length value 205 | putTerm (TupleTerm value) 206 | | len < 256 = tag 104 >> put8u len >> forM_ value putTerm 207 | | otherwise = tag 105 >> put32u len >> forM_ value putTerm 208 | where 209 | len = length value 210 | putTerm (BytelistTerm value) 211 | | len < 65536 = tag 107 >> put16u len >> putL value 212 | | otherwise = do -- too big: encode as a list. 213 | tag 108 214 | put32u len 215 | forM_ (B.unpack value) $ \v -> do 216 | tag 97 217 | putWord8 v 218 | where 219 | len = B.length value 220 | putTerm (ListTerm value) 221 | | len == 0 = putNil -- this is mentioned in the BERT spec. 222 | | otherwise= do 223 | tag 108 224 | put32u $ length value 225 | forM_ value putTerm 226 | putNil 227 | where 228 | len = length value 229 | putNil = putWord8 106 230 | putTerm (BinaryTerm value) = tag 109 >> put32u (B.length value) >> putL value 231 | putTerm (BigintTerm value) = tag 110 >> putBigint put8u value 232 | putTerm (BigbigintTerm value) = tag 111 >> putBigint put32u value 233 | -- All other terms are composite: 234 | putTerm t = putTerm . compose $ t 235 | 236 | -- | Binary decoding of a single term (without header) 237 | getTerm :: Get Term 238 | getTerm = do 239 | tag <- get8u 240 | case tag of 241 | 97 -> IntTerm <$> get8u 242 | 98 -> IntTerm <$> get32s 243 | 99 -> FloatTerm . read . C.unpack <$> getL 31 244 | 100 -> AtomTerm . C.unpack <$> (get16u >>= getL) 245 | 104 -> get8u >>= getN >>= tupleTerm 246 | 105 -> get32u >>= getN >>= tupleTerm 247 | 106 -> return $ ListTerm [] 248 | 107 -> BytelistTerm <$> (get16u >>= getL) 249 | 108 -> get32u >>= \n -> ListTerm <$> (getN n <* expectNil) 250 | 109 -> BinaryTerm <$> (get32u >>= getL) 251 | 110 -> BigintTerm . fromIntegral <$> getBigint get8u 252 | 111 -> (BigintTerm . fromIntegral) <$> getBigint get32u 253 | where 254 | getN :: Int -> Get [Term] 255 | getN n = replicateM n getTerm 256 | expectNil :: Get () 257 | expectNil = do 258 | tag <- get8u 259 | case tag of 260 | 106 -> return () 261 | _ -> fail $ "invalid list - expected list ending with Nil" 262 | -- First try & decode composite terms. 263 | tupleTerm [AtomTerm "bert", AtomTerm "true"] = return $ BoolTerm True 264 | tupleTerm [AtomTerm "bert", AtomTerm "false"] = return $ BoolTerm False 265 | tupleTerm [AtomTerm "bert", AtomTerm "dict", ListTerm kvs] = mapM toTuple kvs >>= return . DictionaryTerm 266 | where 267 | toTuple (TupleTerm [k, v]) = return $ (k, v) 268 | toTuple _ = fail "invalid dictionary" 269 | tupleTerm [AtomTerm "bert", AtomTerm "time", IntTerm mS, IntTerm s, IntTerm uS] = 270 | return $ TimeTerm $ composeTime (mS, s, uS) 271 | tupleTerm [AtomTerm "bert", AtomTerm "regex", BytelistTerm s, ListTerm os] = 272 | options os >>= return . RegexTerm (C.unpack s) 273 | -- TODO: type-check the options values as well 274 | where 275 | options [] = return [] 276 | options ((AtomTerm o):os) = options os >>= return . (o :) 277 | options _ = fail "regex options must be atoms" 278 | -- All other tuples are just .. tuples 279 | tupleTerm xs = return $ TupleTerm xs 280 | 281 | putBigint putter value = do 282 | putter len -- TODO: verify size? 283 | if value < 0 284 | then put8u 1 285 | else put8u 0 286 | putL $ B.pack $ map (fromIntegral . digit) [0..len-1] 287 | where 288 | value' = abs value 289 | len = ceiling $ logBase 256 (fromIntegral $ value' + 1) 290 | digit pos = (value' `shiftR` (8 * pos)) .&. 0xFF 291 | 292 | getBigint getter = do 293 | len <- fromIntegral <$> getter 294 | sign <- get8u 295 | bytes <- getL len 296 | multiplier <- 297 | case sign of 298 | 0 -> return 1 299 | 1 -> return (-1) 300 | _ -> fail "Invalid sign byte" 301 | return $ (*) multiplier 302 | $ foldl (\s (n, d) -> s + d*(256^n)) 0 303 | $ zip [0..len-1] (map fromIntegral $ B.unpack bytes) 304 | 305 | -- Note about put32s/get32s: 306 | -- 307 | -- When dealing with 32-bit signed ints, we first convert between Int and 308 | -- Int32, and only then cast to Word32. This is to ensure put and get are 309 | -- as close to inverse as possible. Coercing word types to and from 310 | -- integer types using 'fromIntegral' is guaranteed to preserve 311 | -- representation (see Notes in "Data.Int"). 312 | -- 313 | -- For an example of what can go wrong, see 314 | -- https://github.com/feuerbach/bert/issues/6 315 | 316 | put8u :: (Integral a) => a -> Put 317 | put8u = putWord8 . fromIntegral 318 | put16u :: (Integral a) => a -> Put 319 | put16u = putWord16be . fromIntegral 320 | put32u :: (Integral a) => a -> Put 321 | put32u = putWord32be . fromIntegral 322 | put32s :: (Integral a) => a -> Put 323 | put32s = putWord32be . (fromIntegral :: Int32 -> Word32) . fromIntegral 324 | putL = putByteString 325 | 326 | get8u :: (Integral a) => Get a 327 | get8u = fromIntegral <$> getWord8 328 | get16u :: (Integral a) => Get a 329 | get16u = fromIntegral <$> getWord16be 330 | get32u :: (Integral a) => Get a 331 | get32u = fromIntegral <$> getWord32be 332 | get32s :: (Integral a) => Get a 333 | get32s = fromIntegral . (fromIntegral :: Word32 -> Int32) <$> getWord32be 334 | getL :: (Integral a) => a -> Get ByteString 335 | getL = getByteString . fromIntegral 336 | 337 | tag :: Word8 -> Put 338 | tag = putWord8 339 | --------------------------------------------------------------------------------