packages feed

network-minihttp 0.1 → 0.2

raw patch · 17 files changed

+2227/−363 lines, 17 filesdep +HsOpenSSLdep +network-connectiondep +network-dnsPVP ok

version bump matches the API change (PVP)

Dependencies added: HsOpenSSL, network-connection, network-dns, tagsoup

API changes (from Hackage documentation)

- Network.MiniHTTP.MimeTypesParse: parseMimeTypes :: String -> IO (Map ByteString MediaType)
- Network.MiniHTTP.MimeTypesParse: parseMimeTypesTotal :: String -> IO (Maybe (Map ByteString MediaType))
- Network.MiniHTTP.Server: SourceData :: ByteString -> SourceResult
- Network.MiniHTTP.Server: SourceEOF :: SourceResult
- Network.MiniHTTP.Server: SourceError :: SourceResult
- Network.MiniHTTP.Server: bsSource :: ByteString -> IO Source
- Network.MiniHTTP.Server: data SourceResult
- Network.MiniHTTP.Server: hSource :: (Int64, Int64) -> Handle -> IO Source
- Network.MiniHTTP.Server: instance Show SourceResult
- Network.MiniHTTP.Server: nullSource :: Source
- Network.MiniHTTP.Server: serve :: Int -> WebMonad () -> IO ()
- Network.MiniHTTP.Server: type Source = IO SourceResult
+ Network.MiniHTTP.Client: connection :: URL -> IO Socket
+ Network.MiniHTTP.Client: fetchBasic :: Headers -> URL -> IO (Connection, Reply, Maybe Source)
+ Network.MiniHTTP.Client: request :: Connection -> Request -> Maybe Source -> IO (Maybe (Reply, Maybe Source))
+ Network.MiniHTTP.Client: transport :: URL -> Socket -> IO Connection
+ Network.MiniHTTP.HTTPConnection: SourceData :: ByteString -> SourceResult
+ Network.MiniHTTP.HTTPConnection: SourceEOF :: SourceResult
+ Network.MiniHTTP.HTTPConnection: SourceError :: SourceResult
+ Network.MiniHTTP.HTTPConnection: bsSource :: ByteString -> IO Source
+ Network.MiniHTTP.HTTPConnection: connChunkedSource :: Connection -> IO Source
+ Network.MiniHTTP.HTTPConnection: connEOFSource :: Connection -> IO Source
+ Network.MiniHTTP.HTTPConnection: connSource :: Int64 -> ByteString -> Connection -> IO Source
+ Network.MiniHTTP.HTTPConnection: data SourceResult
+ Network.MiniHTTP.HTTPConnection: hSource :: (Int64, Int64) -> Handle -> IO Source
+ Network.MiniHTTP.HTTPConnection: instance Show SourceResult
+ Network.MiniHTTP.HTTPConnection: maybeRead :: (Read a) => ByteString -> Maybe a
+ Network.MiniHTTP.HTTPConnection: nullSource :: Source
+ Network.MiniHTTP.HTTPConnection: readIG :: Connection -> Int -> Int -> Get a a -> IO (Maybe a)
+ Network.MiniHTTP.HTTPConnection: sourceDrain :: Source -> IO ()
+ Network.MiniHTTP.HTTPConnection: sourceIG :: Source -> Int -> Get a a -> IO (Maybe a)
+ Network.MiniHTTP.HTTPConnection: sourceToBS :: Int -> Source -> IO (Maybe ByteString)
+ Network.MiniHTTP.HTTPConnection: sourceToLBS :: Source -> IO ByteString
+ Network.MiniHTTP.HTTPConnection: sslToBaseConnection :: SSL -> BaseConnection
+ Network.MiniHTTP.HTTPConnection: streamSource :: Int -> Connection -> Source -> IO Bool
+ Network.MiniHTTP.HTTPConnection: streamSourceChunked :: Int -> Connection -> Source -> IO Bool
+ Network.MiniHTTP.HTTPConnection: type Source = IO SourceResult
+ Network.MiniHTTP.Marshal: Cookie :: ByteString -> ByteString -> Maybe String -> Maybe String -> Maybe UTCTime -> Bool -> Cookie
+ Network.MiniHTTP.Marshal: cookieDomain :: Cookie -> Maybe String
+ Network.MiniHTTP.Marshal: cookieExpires :: Cookie -> Maybe UTCTime
+ Network.MiniHTTP.Marshal: cookieName :: Cookie -> ByteString
+ Network.MiniHTTP.Marshal: cookiePath :: Cookie -> Maybe String
+ Network.MiniHTTP.Marshal: cookieSecure :: Cookie -> Bool
+ Network.MiniHTTP.Marshal: cookieValue :: Cookie -> ByteString
+ Network.MiniHTTP.Marshal: data Cookie
+ Network.MiniHTTP.Marshal: emptyCookie :: Cookie
+ Network.MiniHTTP.Marshal: httpCookie :: Headers -> [Cookie]
+ Network.MiniHTTP.Marshal: httpSetCookie :: Headers -> [Cookie]
+ Network.MiniHTTP.Marshal: instance Eq Cookie
+ Network.MiniHTTP.Marshal: instance Ord Cookie
+ Network.MiniHTTP.Marshal: instance Show Cookie
+ Network.MiniHTTP.Marshal: parseChunkHeader :: (BinaryParser m) => m Int64
+ Network.MiniHTTP.OpenID: CheckIDImmediate :: CheckIDType
+ Network.MiniHTTP.OpenID: CheckIDSetup :: CheckIDType
+ Network.MiniHTTP.OpenID: OpenIDDiscovery :: URL -> Maybe String -> OpenIDDiscovery
+ Network.MiniHTTP.OpenID: associate :: OpenIDDiscovery -> IO (Handle, Key)
+ Network.MiniHTTP.OpenID: checkID :: CheckIDType -> URL -> OpenIDDiscovery -> Handle -> ByteString -> Maybe ByteString -> URL
+ Network.MiniHTTP.OpenID: data CheckIDType
+ Network.MiniHTTP.OpenID: data Handle
+ Network.MiniHTTP.OpenID: data Key
+ Network.MiniHTTP.OpenID: data OpenIDDiscovery
+ Network.MiniHTTP.OpenID: discover :: URL -> IO OpenIDDiscovery
+ Network.MiniHTTP.OpenID: discoveryLocalID :: OpenIDDiscovery -> Maybe String
+ Network.MiniHTTP.OpenID: discoveryProvider :: OpenIDDiscovery -> URL
+ Network.MiniHTTP.OpenID: findKey :: ByteString -> Handle -> STM (Maybe Key)
+ Network.MiniHTTP.OpenID: instance Eq CheckIDType
+ Network.MiniHTTP.OpenID: instance Eq Handle
+ Network.MiniHTTP.OpenID: instance Eq Key
+ Network.MiniHTTP.OpenID: instance Eq OpenIDDiscovery
+ Network.MiniHTTP.OpenID: instance Show CheckIDType
+ Network.MiniHTTP.OpenID: instance Show Handle
+ Network.MiniHTTP.OpenID: instance Show Key
+ Network.MiniHTTP.OpenID: instance Show OpenIDDiscovery
+ Network.MiniHTTP.OpenID: processCheckIDReply :: Map ByteString ByteString -> IO (Either String ByteString)
+ Network.MiniHTTP.Server: Exact :: ByteString -> DispatchMatch
+ Network.MiniHTTP.Server: Prefix :: ByteString -> DispatchMatch
+ Network.MiniHTTP.Server: WebState :: Request -> Maybe Source -> Map ByteString MediaType -> Reply -> Maybe Source -> Maybe Handle -> Maybe (IO ()) -> WebState
+ Network.MiniHTTP.Server: data DispatchMatch
+ Network.MiniHTTP.Server: dispatchOnURL :: [(DispatchMatch, WebMonad ())] -> WebMonad ()
+ Network.MiniHTTP.Server: errorPage :: String -> WebMonad ()
+ Network.MiniHTTP.Server: getGET :: WebMonad (Map ByteString ByteString)
+ Network.MiniHTTP.Server: getPOST :: Int -> WebMonad (Map ByteString ByteString)
+ Network.MiniHTTP.Server: getPayload :: WebMonad (Maybe Source)
+ Network.MiniHTTP.Server: instance Eq DispatchMatch
+ Network.MiniHTTP.Server: instance Show DispatchMatch
+ Network.MiniHTTP.Server: serveHTTP :: Int -> WebMonad () -> IO ()
+ Network.MiniHTTP.Server: serveHTTPS :: Int -> FilePath -> FilePath -> WebMonad () -> IO ()
+ Network.MiniHTTP.Server: setCookie :: Cookie -> WebMonad ()
+ Network.MiniHTTP.Server: wsAction :: WebState -> Maybe (IO ())
+ Network.MiniHTTP.Server: wsBody :: WebState -> Maybe Source
+ Network.MiniHTTP.Server: wsHandle :: WebState -> Maybe Handle
+ Network.MiniHTTP.Server: wsMimeTypes :: WebState -> Map ByteString MediaType
+ Network.MiniHTTP.Server: wsReply :: WebState -> Reply
+ Network.MiniHTTP.Server: wsRequest :: WebState -> Request
+ Network.MiniHTTP.Server: wsSource :: WebState -> Maybe Source
+ Network.MiniHTTP.Session: addSession :: ByteString -> ByteString -> WebMonad ()
+ Network.MiniHTTP.Session: getSession :: WebMonad (Map ByteString ByteString)
+ Network.MiniHTTP.Session: putSession :: Map ByteString ByteString -> WebMonad ()
+ Network.MiniHTTP.Session: setSessionSecretKey :: ByteString -> STM ()
+ Network.MiniHTTP.URL: HTTP :: Scheme
+ Network.MiniHTTP.URL: HTTPS :: Scheme
+ Network.MiniHTTP.URL: Hostname :: ByteString -> Host
+ Network.MiniHTTP.URL: IPv4Literal :: HostAddress -> Host
+ Network.MiniHTTP.URL: IPv6Literal :: HostAddress6 -> Host
+ Network.MiniHTTP.URL: RelativeURL :: ByteString -> Map ByteString ByteString -> Maybe ByteString -> RelativeURL
+ Network.MiniHTTP.URL: URL :: Scheme -> Maybe ByteString -> Maybe ByteString -> Host -> Int -> ByteString -> Map ByteString ByteString -> Maybe ByteString -> URL
+ Network.MiniHTTP.URL: data Host
+ Network.MiniHTTP.URL: data RelativeURL
+ Network.MiniHTTP.URL: data Scheme
+ Network.MiniHTTP.URL: data URL
+ Network.MiniHTTP.URL: instance Eq Host
+ Network.MiniHTTP.URL: instance Eq RelativeURL
+ Network.MiniHTTP.URL: instance Eq Scheme
+ Network.MiniHTTP.URL: instance Eq URL
+ Network.MiniHTTP.URL: instance Show Host
+ Network.MiniHTTP.URL: instance Show RelativeURL
+ Network.MiniHTTP.URL: instance Show Scheme
+ Network.MiniHTTP.URL: instance Show URL
+ Network.MiniHTTP.URL: parse :: ByteString -> Maybe URL
+ Network.MiniHTTP.URL: parseArguments :: ByteString -> Maybe (Map ByteString ByteString)
+ Network.MiniHTTP.URL: parseRelative :: ByteString -> Maybe RelativeURL
+ Network.MiniHTTP.URL: rurlArguments :: RelativeURL -> Map ByteString ByteString
+ Network.MiniHTTP.URL: rurlFragment :: RelativeURL -> Maybe ByteString
+ Network.MiniHTTP.URL: rurlPath :: RelativeURL -> ByteString
+ Network.MiniHTTP.URL: serialise :: URL -> ByteString
+ Network.MiniHTTP.URL: serialiseArguments :: Map ByteString ByteString -> ByteString
+ Network.MiniHTTP.URL: serialiseRelative :: RelativeURL -> ByteString
+ Network.MiniHTTP.URL: toRelative :: URL -> RelativeURL
+ Network.MiniHTTP.URL: urlArguments :: URL -> Map ByteString ByteString
+ Network.MiniHTTP.URL: urlFragment :: URL -> Maybe ByteString
+ Network.MiniHTTP.URL: urlHost :: URL -> Host
+ Network.MiniHTTP.URL: urlPassword :: URL -> Maybe ByteString
+ Network.MiniHTTP.URL: urlPath :: URL -> ByteString
+ Network.MiniHTTP.URL: urlPort :: URL -> Int
+ Network.MiniHTTP.URL: urlScheme :: URL -> Scheme
+ Network.MiniHTTP.URL: urlUser :: URL -> Maybe ByteString
- Network.MiniHTTP.Marshal: Headers :: Maybe [(MediaType, Int)] -> Maybe [(String, Int)] -> Maybe [(String, Int)] -> Maybe [(String, Int)] -> Bool -> Maybe Int64 -> Maybe [Method] -> Maybe ByteString -> Bool -> [String] -> [String] -> Maybe [String] -> Maybe Int64 -> Maybe ByteString -> Maybe (Maybe (Int64, Int64), Maybe Int64) -> Maybe MediaType -> Maybe UTCTime -> Maybe (Bool, ByteString) -> Maybe UTCTime -> Maybe ByteString -> Maybe (Either () [ByteString]) -> Maybe UTCTime -> Maybe (Either () [(Bool, ByteString)]) -> Maybe (Either ByteString UTCTime) -> Maybe UTCTime -> Maybe Int -> Maybe UTCTime -> Maybe ByteString -> Maybe [(String, Maybe String)] -> Maybe ByteString -> Maybe ByteString -> Maybe [Range] -> Maybe ByteString -> Maybe Int64 -> Maybe ByteString -> Maybe [String] -> [String] -> Maybe ByteString -> Maybe ByteString -> Map ByteString ByteString -> Headers
+ Network.MiniHTTP.Marshal: Headers :: Maybe [(MediaType, Int)] -> Maybe [(String, Int)] -> Maybe [(String, Int)] -> Maybe [(String, Int)] -> Bool -> Maybe Int64 -> Maybe [Method] -> Maybe ByteString -> [Cookie] -> Bool -> [String] -> [String] -> Maybe [String] -> Maybe Int64 -> Maybe ByteString -> Maybe (Maybe (Int64, Int64), Maybe Int64) -> Maybe MediaType -> Maybe UTCTime -> Maybe (Bool, ByteString) -> Maybe UTCTime -> Maybe ByteString -> Maybe (Either () [ByteString]) -> Maybe UTCTime -> Maybe (Either () [(Bool, ByteString)]) -> Maybe (Either ByteString UTCTime) -> Maybe UTCTime -> Maybe Int -> Maybe UTCTime -> Maybe ByteString -> Maybe [(String, Maybe String)] -> Maybe ByteString -> Maybe ByteString -> Maybe [Range] -> Maybe ByteString -> Maybe Int64 -> Maybe ByteString -> [Cookie] -> Maybe [String] -> [String] -> Maybe ByteString -> Maybe ByteString -> Map ByteString ByteString -> Headers
- Network.MiniHTTP.Marshal: Request :: Method -> ByteString -> Int -> Int -> Headers -> Request
+ Network.MiniHTTP.Marshal: Request :: Method -> RelativeURL -> Int -> Int -> Headers -> Request
- Network.MiniHTTP.Marshal: reqUrl :: Request -> ByteString
+ Network.MiniHTTP.Marshal: reqUrl :: Request -> RelativeURL

Files

+ Network/MiniHTTP/Client.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module contains a very basic HTTP client. At the moment, it doesn't+--   even handle redirects for you.+--+--   Note that, in order to use SSL, you need to have the root CA certificates+--   in a PEM file in @/etc/ssh/certs.pem@ and you need to have wrapped your+--   main function in 'OpenSSL.withOpenSSL'+module Network.MiniHTTP.Client+  ( fetchBasic+  , connection+  , transport+  , request+  ) where++import           Control.Monad (when)+import           Control.Concurrent.STM+import           Control.Exception (handle, throwIO)++import qualified Data.Binary.Put as P+import qualified Data.ByteString as B+import           Data.ByteString.Internal (w2c)+import qualified Data.ByteString.Lazy as BL+import           Data.Maybe (isNothing)+import           Data.String++import qualified Network.Connection as C+import qualified Network.DNS.Client as DNS+import qualified Network.DNS.Types as DNS+import           Network.MiniHTTP.Marshal+import           Network.MiniHTTP.HTTPConnection+import qualified Network.MiniHTTP.URL as URL+import           Network.Socket++import           System.IO.Unsafe (unsafePerformIO)++import qualified OpenSSL.Session as SSL+import qualified OpenSSL.X509 as X509+++readReply :: C.Connection -> IO (Maybe (Reply, Maybe Source))+readReply conn = do+  r <- readIG conn 256 4096 parseReply+  case r of+       Nothing -> return Nothing+       Just reply ->+         case httpContentLength $ replyHeaders reply of+              Nothing ->+                if "chunked" `elem` (httpTransferEncoding $ replyHeaders reply)+                   then do+                     source <- connChunkedSource conn+                     return $ Just (reply, Just source)+                   else do+                     source <- connEOFSource conn+                     return $ Just (reply, Just source)+              Just n -> do+                source <- connSource n B.empty conn+                return $ Just (reply, Just source)++-- | A lower level HTTP client, but it allows you to perform POSTs etc+request :: C.Connection  -- ^ the connection to use+        -> Request  -- ^ a request to serialise+        -> Maybe Source  -- ^ a possible payload (for POSTs etc)+        -> IO (Maybe (Reply, Maybe Source))+request conn req msource = do+  let requestBytes = B.concat $ BL.toChunks $ P.runPut $ putRequest req+  atomically $ C.write conn requestBytes+  let lowWater = 32 * 1024++  case msource of+       (Just source) -> do+         success <- if isNothing $ httpContentLength $ reqHeaders req+                       then streamSourceChunked lowWater conn source+                       else streamSource lowWater conn source+         if not success+            then return Nothing+            else readReply conn+       Nothing -> readReply conn++globalOpenSSLClientContext :: SSL.SSLContext+globalOpenSSLClientContext = unsafePerformIO $ do+  ctx <- SSL.context+  SSL.contextSetDefaultCiphers ctx+  SSL.contextSetVerificationMode ctx $ SSL.VerifyPeer False False+  SSL.contextSetCAFile ctx "/etc/ssl/cert.pem"+  return ctx++-- | Construct a connection to the correct host for the given URL. (i.e.+--   resolve the hostname and create a TCP connection to the correct port).+--+--   Note that the DNS resolution (if any) doesn't block the whole process.+connection :: URL.URL+           -> IO Socket+connection (URL.URL { URL.urlHost = URL.IPv4Literal host, URL.urlPort = port }) = do+  sock <- socket AF_INET Stream 0+  connect sock $ SockAddrInet (fromIntegral port) host+  return sock++connection (URL.URL { URL.urlHost = URL.IPv6Literal host, URL.urlPort = port }) = do+  sock <- socket AF_INET6 Stream 0+  connect sock $ SockAddrInet6 (fromIntegral port) 0 host 0+  return sock++connection (URL.URL { URL.urlHost = URL.Hostname hostname, URL.urlPort = port }) = do+  r <- DNS.resolve DNS.A $ map w2c $ B.unpack hostname+  case r of+       Left error -> fail $ show error+       Right [] -> fail "DNS returned no A records"+       Right (((_, DNS.RRA (haddr:_))):_) -> do+         sock <- socket AF_INET Stream 0+         connect sock $ SockAddrInet (fromIntegral port) haddr+         return sock++-- | Setup the transport (i.e. SSL) for the given URL. In the case of a HTTP+--   scheme, this just wraps the socket in a Connection.+transport :: URL.URL -> Socket -> IO C.Connection+transport (URL.URL { URL.urlScheme = URL.HTTP }) sock =+  C.new (return ()) $ C.baseConnectionFromSocket sock++transport (URL.URL { URL.urlScheme = URL.HTTPS, URL.urlHost = URL.Hostname hostname }) sock = do+  ssl <- SSL.connection globalOpenSSLClientContext sock+  SSL.connect ssl+  verified <- SSL.getVerifyResult ssl+  when (not verified) $ fail "Failed to verify SSL server certificate"+  mcert <- SSL.getPeerCertificate ssl+  case mcert of+       Nothing -> fail "No server certificate"+       Just cert -> do+         subjects <- X509.getSubjectName cert True+         case "commonName" `lookup` subjects of+              Nothing -> fail "No hostname in certificate"+              Just h -> do+                when (fromString h /= hostname) $ fail $+                  "Hostname doesn't match certificate (" +++                   h ++ " vs " ++ show hostname ++ ")"+                conn <- C.new (return ()) $ sslToBaseConnection ssl+                return conn++transport _ _ = fail "Cannot create HTTPS connection to an IP address (cannot check certificate)"++-- | Fetch an HTTP(S) entity.+fetchBasic :: Headers  -- ^ the headers to use. This function will set the Host+                       --   header for you in the case that the URL has a+                       --   hostname in it. If in doubt, use 'emptyHeaders'+           -> URL.URL  -- ^ the resource to fetch+           -> IO (C.Connection, Reply, Maybe Source)+              -- ^ the connection (which you have to close once you are done+              --   reading the 'Source', if any), the Reply and a possible+              --   payload+fetchBasic headers url = do+  sock <- connection url+  handle (\e -> sClose sock >> throwIO e) $ do+    conn <- transport url sock+    let headers' = case URL.urlHost url of+                        URL.Hostname h -> headers { httpHost = Just h }+                        _ -> headers+    r <- request conn (Request GET (URL.toRelative url) 1 1 headers') Nothing+    case r of+         Nothing -> C.close conn >> fail "HTTP parse error"+         (Just (reply, msource)) -> return (conn, reply, msource)
− Network/MiniHTTP/Connection.hs
@@ -1,120 +0,0 @@-module Network.MiniHTTP.Connection-  ( Connection-  , new-  , forkThreads-  , close-  , connoutq-  , connsocket-  ) where--import Control.Concurrent-import Control.Concurrent.STM-import Control.Exception-import Control.Monad--import Data.Maybe (fromJust)-import qualified Data.ByteString as BS-import qualified Data.Sequence as Seq--import Network.Socket hiding (send, sendTo, recv, recvFrom)-import Network.Socket.ByteString--data Connection = Connection { connsocket :: Socket-                             , connoutq :: TVar (Seq.Seq BS.ByteString)-                             , connreaderthread :: TVar (Maybe ThreadId)-                             , connwriterthread :: TVar (Maybe ThreadId)-                             , conndeath :: IO ()-                             , conndead :: TVar Bool }---- Threading: each connection has two threads: one pumping data out and one--- reading it in. The failure (by throwing an exception) of either is enough--- to close the socket and kill the connection. This could happen to both at--- the same time, in which case they race to set conndead to True.--- However, we also need to record their ThreadIds in the Connection record--- so that they can kill each other. It's possible that they could try to--- kill the connection right away - before the creating thread has recorded--- the correct ThreadIds in the Connection. Thus, at startup, they both wait--- for the controlling thread to set conndead to False before doing anything--- else--new :: Socket  -- ^ the socket to make a connection from-    -> IO ()  -- ^ the action run when the connection fails-    -> STM Connection-new socket deathaction = do-  dead <- newTVar True-  outq <- newTVar Seq.empty-  p1 <- newTVar Nothing-  p2 <- newTVar Nothing--  let conn = Connection socket outq p1 p2 deathaction dead-  return conn--forkThreads :: Connection  -- ^ the connection to fork the threads for-            -> IO ()  -- ^ the action which reads from the socket-            -> IO ()-forkThreads conn readeraction = do-  reader <- forkIO $ waitForReadySignal conn $-                     connectionThreadWrapper conn connwriterthread $-                     readeraction-  writer <- forkIO $ waitForReadySignal conn $-                     connectionThreadWrapper conn connreaderthread $-                     seqToSocket (connoutq conn) (connsocket conn)-  -- update the thread ids in the Connection and set the ready flag-  atomically (writeTVar (connreaderthread conn) (Just reader) >>-              writeTVar (connwriterthread conn) (Just writer) >>-              writeTVar (conndead conn) False)-  return ()---- | Wait for conndead to be set to False on the given connection, then run---   the given action-waitForReadySignal :: Connection -> IO a -> IO a-waitForReadySignal conn action = do-  atomically (do dead <- readTVar (conndead conn)-                 if dead == True then retry else return ())-  action---- | Wrap a connection thread so that, when the thread dies, it races to set---   the dead flag. If it does so, it closes the socket and kills the other---   thread-connectionThreadWrapper :: Connection -> (Connection -> TVar (Maybe ThreadId)) -> IO a -> IO a-connectionThreadWrapper conn otherthread action = do-  finally action-          (do isDead <- atomically (do dead <- readTVar (conndead conn)-                                       when (not dead) $ writeTVar (conndead conn) True-                                       return dead)-              when (not isDead) (do t <- atomically (readTVar $ otherthread conn)-                                    killThread $ fromJust t-                                    sClose (connsocket conn)-                                    conndeath conn))---- | Close a connection-close :: Connection -> IO ()-close = sClose . connsocket---- | Atomically take elements from the end of the given sequence and write them---   to the given socket. Throw an exception when the write fails-seqToSocket :: TVar (Seq.Seq BS.ByteString)  -- ^ data is removed from the end-            -> Socket  -- ^ the socket to write to-            -> IO ()-seqToSocket q sock = do-  -- Atomically remove an element from the end of the sequence-  bs <- atomically (do q' <- readTVar q-                       (bs, rest) <--                         case Seq.viewr q' of-                              Seq.EmptyR -> retry-                              rest Seq.:> head -> return (head, rest)-                       writeTVar q rest-                       return bs)-  -- Write the data to the socket-  writea sock bs-  seqToSocket q sock---- | Write a given number of bytes to a socket.-writea :: Socket -> BS.ByteString -> IO ()-writea sock bytes-  | BS.null bytes = return ()-  | otherwise = do-    n <- send sock bytes-    if n == BS.length bytes-       then return ()-       else writea sock $ BS.drop n bytes
+ Network/MiniHTTP/HTTPConnection.hs view
@@ -0,0 +1,276 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module contains many helper functions, as well the code for 'Source',+--   which is a pretty important structure+module Network.MiniHTTP.HTTPConnection+  ( -- * Sources, and related functions+    Source+  , SourceResult(..)+  , bsSource+  , hSource+  , nullSource+  , sourceToLBS+  , sourceToBS+  , connSource+  , connChunkedSource+  , connEOFSource+  , sourceDrain+  , streamSource+  , streamSourceChunked++  -- * Misc functions+  , readIG+  , sourceIG+  , maybeRead+  , sslToBaseConnection+  ) where++import           Control.Concurrent.STM+import qualified Control.Exception as Exception+import           Control.Monad (liftM)++import qualified Data.ByteString as B+import           Data.ByteString.Char8 ()+import           Data.ByteString.Internal (c2w, w2c)+import qualified Data.ByteString.Lazy.Internal as BL+import qualified Data.Binary.Strict.IncrementalGet as IG+import           Data.IORef+import           Data.Int (Int64)++import           System.IO++import           Text.Printf (printf)++import           System.IO.Unsafe (unsafeInterleaveIO)++import qualified Network.Connection as C+import           Network.Socket as Socket+import           Network.MiniHTTP.Marshal (parseChunkHeader)++import qualified OpenSSL.Session as SSL++-- | A source is a stream of data, like a lazy data structure, but without+--   some of the dangers that such entail. A source returns a 'SourceResult'+--   each time you evaluate it.+type Source = IO SourceResult++data SourceResult = SourceError  -- ^ error - please don't read this source again+                  | SourceEOF  -- ^ end of data+                  | SourceData B.ByteString  -- ^ some data+                  deriving (Show)++-- | Construct a source from a ByteString+bsSource :: B.ByteString -> IO Source+bsSource bs = do+  ref <- newIORef $ SourceData bs+  return $ do+    v <- readIORef ref+    writeIORef ref SourceEOF+    return v++-- | Construct a source from a Handle+hSource :: (Int64, Int64)  -- ^ the first and last byte to include+        -> Handle  -- ^ the handle to read from+        -> IO Source+hSource (from, to) handle = do+  bytesSoFar <- newIORef (from :: Int64)+  hSeek handle AbsoluteSeek (fromIntegral from)+  return $ do+    Exception.catch+      (do done <- readIORef bytesSoFar+          bytes <- B.hGet handle $ min (128 * 1024) (fromIntegral $ (to + 1) - done)+          if B.length bytes == 0+             then do+               if to + 1 == done+                  then return SourceEOF+                  else return SourceError+             else do modifyIORef bytesSoFar ((+) (fromIntegral $ B.length bytes))+                     return $ SourceData bytes)+      (const $ return SourceError)++-- | A source with no data (e.g. @/dev/null@)+nullSource :: Source+nullSource = return SourceEOF++-- | A source which reads from the given 'Connection' until the connection+--   signals end-of-file.+connEOFSource :: C.Connection -> IO Source+connEOFSource conn = do+  return $+    catch (liftM SourceData $ C.read conn 1024) (const $ return SourceEOF)++-- | A source which reads from a 'C.Connection'+connSource :: Int64  -- ^ the number of bytes to read+           -> B.ByteString  -- ^ a string which is prepended to the output+           -> C.Connection  -- ^ the connection to read from+           -> IO Source+connSource n bs conn =+  if fromIntegral (B.length bs) == n+     then bsSource bs+     else do+       ref <- newIORef (False, 0 :: Int64)+       return $ do+         (doneBS, n') <- readIORef ref+         if not doneBS+            then do writeIORef ref (True, fromIntegral $ B.length bs)+                    return $ SourceData bs+            else if n' == n+                    then return SourceEOF+                    else do bytes <- C.read conn $ min (32 * 1024) $ fromIntegral (n - n')+                            if B.length bytes == 0+                               then return SourceError+                               else do writeIORef ref (doneBS, n' + (fromIntegral $ B.length bytes))+                                       return $ SourceData bytes++-- | A source which reads an HTTP chunked reply from a 'C.Connection'+connChunkedSource :: C.Connection -> IO Source+connChunkedSource conn = do+  -- the contents of this reference are the number of bytes remaining in the+  -- current chunk. If zero, a chunk headers needs to be read. If < 0, we have+  -- hit EOF. If we read the end of a chunk, we always read the trailing \r\n+  -- before returning (so one need never consider that case on entry)+  ref <- newIORef (0 :: Int64)+  let f = do+        remainingInThisChunk <- readIORef ref+        case remainingInThisChunk of+             0 -> do+               m <- readIG conn 16 256 parseChunkHeader+               case m of+                    Nothing -> return SourceError+                    Just n ->+                      if n == 0+                         then C.reada conn 2 >> writeIORef ref (-1) >> return SourceEOF+                         else writeIORef ref n >> f+             (-1) -> return SourceEOF+             remainingInThisChunk -> do+               bytes <- C.read conn $ fromIntegral $ min remainingInThisChunk $ 32*1024+               if B.null bytes+                  then return SourceError+                  else do+                    let stillRemaining = remainingInThisChunk - (fromIntegral $ B.length bytes)+                    if stillRemaining == 0+                       then do+                         C.reada conn 2  -- read \r\n+                         writeIORef ref 0+                       else do+                         writeIORef ref stillRemaining+                    return $ SourceData bytes+  return f++-- | Read a source until it returns 'SourceEOF'+sourceDrain :: Source -> IO ()+sourceDrain s = do+  v <- s+  case v of+       SourceEOF -> return ()+       SourceError -> return ()+       SourceData _ -> sourceDrain s++-- | Convert a source to a lazy ByteString+sourceToLBS :: Source -> IO BL.ByteString+sourceToLBS s = do+  bytes <- s+  case bytes of+       SourceEOF -> return $ BL.Empty+       SourceError -> fail "Error in reading from client"+       SourceData bs -> do+         rest <- unsafeInterleaveIO $ sourceToLBS s+         return $ BL.Chunk bs rest++-- | Take, at most, the first n bytes from a Source and return a strict+--   ByteString. Returns Nothing on error. (A short read is not an error)+sourceToBS :: Int -> Source -> IO (Maybe B.ByteString)+sourceToBS n source = f 0 where+  f soFar = do+    s <- source+    case s of+         SourceEOF -> return $ Just B.empty+         SourceError -> return Nothing+         SourceData bs -> do+           if B.length bs + soFar >= n+              then return $ Just $ B.take (n - soFar) bs+              else do+                rest <- f (soFar + B.length bs)+                return $ (rest >>= return . B.append bs)++-- | Stream a source to a connection while not enqueuing more than lowWater+--   bytes in the outbound queue (not inc the kernel buffer)+streamSource :: Int -> C.Connection -> Source -> IO Bool+streamSource lowWater conn source = do+  next <- source+  case next of+       SourceEOF -> return True+       SourceError -> return False+       SourceData bs -> do+         atomically $ C.writeAtLowWater lowWater conn bs+         streamSource lowWater conn source++-- | Stream a source to a connection, with chunked encoding, while not+--   enqueuing more than lowWater bytes in the outbound queue (not inc the+--   kernel buffer)+streamSourceChunked :: Int -> C.Connection -> Source -> IO Bool+streamSourceChunked lowWater conn source = do+  next <- source+  case next of+       SourceEOF -> do+         atomically $ C.writeAtLowWater lowWater conn "0\r\n\r\n"+         return True+       SourceError -> return False+       SourceData bs -> do+         atomically $ C.writeAtLowWater lowWater conn $ B.pack $ map c2w $+           printf "%d\r\n\r\n" $ B.length bs+         atomically $ C.writeAtLowWater lowWater conn bs+         atomically $ C.writeAtLowWater lowWater conn "\r\n"+         streamSourceChunked lowWater conn source++-- | Run an incremental parser from the network+readIG :: C.Connection  -- ^ the connection to read from+       -> Int  -- ^ the block size to use+       -> Int  -- ^ maximum number of bytes to parse+       -> IG.Get a a -- ^ the parser+       -> IO (Maybe a)+readIG conn blockSize maxBytes parser = do+  let f sofar result+        | sofar >= maxBytes = return Nothing+        | otherwise = do+            case result of+                 IG.Failed _ -> return Nothing+                 IG.Partial cont -> C.read conn blockSize >>= \bs -> f (sofar + B.length bs) $ cont bs+                 IG.Finished rest result -> do+                   atomically $ C.pushBack conn rest+                   return $ Just result+  C.read conn blockSize >>= f 0 . IG.runGet parser++-- | Run an incremental parser from a 'Source'+sourceIG :: Source  -- ^ the source to read from+         -> Int  -- ^ the maximum number of bytes to parse+         -> IG.Get a a  -- ^ the parser+         -> IO (Maybe a)+sourceIG source maxBytes parser = do+  let f sofar result+        | sofar >= maxBytes = return Nothing+        | otherwise =+            case result of+                 IG.Failed _ -> return Nothing+                 IG.Partial cont -> do+                   s <- source+                   case s of+                        SourceError -> return Nothing+                        SourceEOF -> return Nothing+                        SourceData bytes -> f (B.length bytes + sofar) $ cont bytes+                 IG.Finished _ result -> do+                   return $ Just result+  f 0 $ IG.runGet parser B.empty++maybeRead :: Read a => B.ByteString -> Maybe a+maybeRead s = case reads $ map w2c $ B.unpack s of+                   [(x, "")] -> Just x+                   _         -> Nothing++-- | Convert an SSL connection to a BaseConnection for Network.Connection+sslToBaseConnection :: SSL.SSL -> C.BaseConnection+sslToBaseConnection ssl = C.BaseConnection r w c where+  r n = do+    bytes <- SSL.read ssl n+    return bytes+  w bs = SSL.write ssl bs >> return (B.length bs)+  c = SSL.shutdown ssl SSL.Unidirectional >> sClose (SSL.sslSocket ssl)
Network/MiniHTTP/Marshal.hs view
@@ -1,5 +1,6 @@ {-# OPTIONS_GHC -fno-monomorphism-restriction                 -fno-warn-missing-signatures #-}+{-# LANGUAGE OverloadedStrings #-} -- | This module serialises and deserialises HTTP headers. It contains Haskell --   representations of request and replies and can transform them to, and from, --   the HTTP wire format.@@ -8,7 +9,9 @@   , Reply(..)   , Range(..)   , Headers(..)+  , Cookie(..)   , emptyHeaders+  , emptyCookie   , statusToMessage   , Method(..)   , MediaType@@ -16,37 +19,41 @@   , putReply   , parseRequest   , parseReply+  , parseChunkHeader   ) where -import Prelude hiding (putChar)-import Control.Monad (when)-import GHC.Exts()-import Data.Time (UTCTime(..))-import Data.Time.Format (formatTime)-import Data.Time.Calendar (fromGregorian)-import Data.Time.LocalTime (TimeOfDay(..), timeOfDayToTime)-import Data.Int (Int64)-import Data.Maybe (isJust, maybe)-import Data.List (foldl')-import System.Locale (TimeLocale(..))-import qualified Data.Map as Map-import Control.Applicative ((<|>), liftA, liftA2, (*>))+import           Prelude hiding (putChar)++import           Control.Applicative ((<|>), liftA, liftA2, (*>))+import           Control.Monad (when, forM_)+ import qualified Data.ByteString as B-import Data.ByteString.Char8 ()-import Data.ByteString.Internal (c2w, w2c)+import           Data.ByteString.Char8 ()+import           Data.ByteString.Internal (c2w, w2c) import qualified Data.Binary.Put as P-import qualified Data.Binary.Strict.Get as G-import qualified Data.Binary.Strict.Class as C import qualified Data.Binary.Strict.ByteSet as BSet+import qualified Data.Binary.Strict.Class as C+import qualified Data.Binary.Strict.Get as G+import           Data.Int (Int64)+import           Data.List (intersperse, foldl')+import qualified Data.Map as Map+import           Data.Maybe (isJust, maybe)+import           Data.String (fromString)+import           Data.Time (UTCTime(..))+import           Data.Time.Calendar (fromGregorian)+import           Data.Time.Format (formatTime)+import           Data.Time.LocalTime (TimeOfDay(..), timeOfDayToTime) -import Debug.Trace (trace)+import           GHC.Exts() -debug x = trace (show x) x+import           System.Locale (TimeLocale(..)) +import qualified Network.MiniHTTP.URL as URL+ -- | A HTTP request data Request =   Request { reqMethod :: Method-          , reqUrl :: B.ByteString+          , reqUrl :: URL.RelativeURL           , reqMajor :: Int           , reqMinor :: Int           , reqHeaders :: Headers@@ -77,6 +84,7 @@           , httpAge :: Maybe Int64           , httpAllow :: Maybe [Method]           , httpAuthorization :: Maybe B.ByteString+          , httpCookie :: [Cookie]           , httpConnectionClose :: Bool           , httpConnection :: [String]           , httpContentEncodings :: [String]@@ -104,6 +112,7 @@           , httpReferer :: Maybe B.ByteString           , httpRetryAfter :: Maybe Int64           , httpServer :: Maybe B.ByteString+          , httpSetCookie :: [Cookie]           , httpTrailer :: Maybe [String]           , httpTransferEncoding :: [String]           , httpUserAgent :: Maybe B.ByteString@@ -111,13 +120,22 @@           , httpOtherHeaders :: Map.Map B.ByteString B.ByteString           } deriving (Show) +-- | A HTTP Cookie. See <http://en.wikipedia.org/wiki/HTTP_cookie>+data Cookie = Cookie { cookieName :: B.ByteString+                     , cookieValue :: B.ByteString+                     , cookiePath :: Maybe String+                     , cookieDomain :: Maybe String+                     , cookieExpires :: Maybe UTCTime+                     , cookieSecure :: Bool+                     } deriving (Show, Eq, Ord)+ emptyHeaders :: Headers emptyHeaders =   Headers Nothing Nothing Nothing Nothing-  False Nothing Nothing Nothing False [] [] Nothing Nothing+  False Nothing Nothing Nothing [] False [] [] Nothing Nothing   Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing   Nothing Nothing Nothing Nothing Nothing Nothing Nothing-  Nothing Nothing Nothing Nothing Nothing Nothing Nothing+  Nothing Nothing Nothing Nothing Nothing Nothing Nothing []   Nothing [] Nothing Nothing Map.empty  -- | The list of valid methods, see RFC 2616 section 5.1@@ -200,7 +218,7 @@         BSet.range (c2w 'A') (c2w 'F') `BSet.union`         digits separators = BSet.fromList $ map c2w "()<>@,;:\\\"/[]?={} \t"-ctexts = texts `BSet.difference` (BSet.fromList $ map c2w "()\\")+--ctexts = texts `BSet.difference` (BSet.fromList $ map c2w "()\\") qdtexts = texts `BSet.difference` (BSet.fromList $ map c2w "\"\\")  --------------------------------------------------------------------------------@@ -226,11 +244,12 @@      then return 0      else return $ read $ toString r ++ replicate (3 - B.length r) '0' -comment = do+{-comment = do   char '('   comment <- C.many ((C.spanOf $ BSet.member ctexts) <|> quotedPair <|> comment) >>= return . B.concat   char ')'   return comment+-}  quotedPair = (char '\\') >> (C.getWord8 >>= return . B.singleton) @@ -346,6 +365,9 @@ int64 :: (C.BinaryParser c) => c Int64 int64 = C.spanOf1 (BSet.member digits) >>= return . readOrZero . toString +hexInt64 :: (C.BinaryParser c) => c Int64+hexInt64 = C.spanOf1 (BSet.member hexes) >>= return . readOrZero . ((++) "0x") . toString+ readOrZero "" = 0 readOrZero x = read x @@ -466,7 +488,7 @@   invalid (RangeOf a b) = a > b   invalid _ = False -headerRange req = (C.string "bytes=" *> list f) >>= \a -> return $ req { httpRange = checkRanges $ debug a } where+headerRange req = (C.string "bytes=" *> list f) >>= \a -> return $ req { httpRange = checkRanges a } where   f = a <|> b <|> c where     a = char '-' *> liftA RangeSuffix int64     b = int64 >>= (\start -> char '-' *> liftA (RangeOf start) int64)@@ -480,6 +502,42 @@ headerUserAgent req = C.remaining >>= \rem -> C.getByteString rem >>= \x -> return $ req { httpUserAgent = Just x } headerWWWAuthenticate req = C.remaining >>= \rem -> C.getByteString rem >>= \x -> return $ req { httpWWWAuthenticate = Just x } +emptyCookie = Cookie B.empty B.empty Nothing Nothing Nothing False++mergeCookie a b =+  Cookie { cookieName = bs cookieName+         , cookieValue = bs cookieValue+         , cookiePath = m cookiePath+         , cookieDomain = m cookieDomain+         , cookieExpires = m cookieExpires+         , cookieSecure = cookieSecure a || cookieSecure b } where+  bs f = if B.null $ f a then f b else f a+  m f = case f a of+             Nothing -> f b+             x -> x++cookie = do+  name <- C.spanOf1 (/= 0x3d {- '=' -} )+  C.word8 0x3d+  value <- C.spanOf1 (/= 0x3b {- ';' -} )++  options <- C.many $ do+    C.word8 0x3b+    lws+    key <- C.spanOf1 (BSet.member alphas)+    let value = C.spanOf1 (/= 0x3b)+    case key of+         "secure" -> return $ emptyCookie { cookieSecure = True }+         "path" -> value >>= \v -> (return $ emptyCookie { cookiePath = Just $ toString v })+         "domain" -> value >>= \v ->(return $ emptyCookie { cookieDomain = Just $ toString v })+         "expires" -> date >>= \v -> (return $ emptyCookie { cookieExpires = Just v})+         _ -> C.spanOf1 (/= 0x3d) >> return emptyCookie++  return $ foldl mergeCookie (emptyCookie { cookieName = name, cookieValue = value }) options++headerSetCookie req = cookie >>= \c -> return $ req { httpSetCookie = c : httpSetCookie req }+headerCookie req = cookie >>= \c -> return $ req { httpCookie = c : httpCookie req }+ -------------------------------------------------------------------------------- -- Top level parsing functions @@ -560,6 +618,8 @@   , ("Trailer", headerTrailer)   , ("User-Agent", headerUserAgent)   , ("WWW-Authenticate", headerWWWAuthenticate)+  , ("Cookie", headerCookie)+  , ("Set-Cookie", headerSetCookie)   ]  parseMethod :: (Monad m) => B.ByteString -> m Method@@ -578,10 +638,13 @@ parseRequest :: (C.BinaryParser m) => m Request parseRequest = do   (strmethod, url, major, minor) <- requestLine+  uri <- case URL.parseRelative url of+              Just uri -> return uri+              Nothing -> fail "Failed to parse URL"   method <- parseMethod strmethod   headers <- parseHeaders -  return $ Request method url major minor headers+  return $ Request method uri major minor headers  parseReply :: (C.BinaryParser m) => m Reply parseReply = do@@ -605,10 +668,18 @@    return req' +parseChunkHeader :: (C.BinaryParser m) => m Int64+parseChunkHeader = do+  length <- hexInt64+  C.optional lws  -- not in RFC, due to Yahoo being broken+  C.many $ char ';' >> token >> char '=' >> (token <|> quotedString)+  crlf+  return length+ -------------------------------------------------------------------------------- -- Serialisation functions -putString = P.putByteString . B.pack . map c2w+putString = P.putByteString . fromString putChar = P.putWord8 . c2w  putShow = putString . show@@ -632,12 +703,43 @@ putHeaderM (Just x) h f = P.putByteString h >> P.putByteString ": " >> f x >> P.putByteString "\r\n"  putHeaderML :: Maybe [a] -> B.ByteString -> (a -> P.Put) -> P.Put-putHeaderML a b c = putHeaderM a b (mapM_ c)+putHeaderML a b c = putHeaderM a b (sequence_ . intersperse (P.putByteString ",") . map c) +putHeaderMLE :: Maybe [a] -> B.ByteString -> (a -> P.Put) -> B.ByteString -> P.Put+putHeaderMLE a b c extra = putHeaderM a b (sequence_ . ((:) (P.putByteString extra)) . intersperse (P.putByteString ", ") . map c)+ putHeaderL :: [a] -> B.ByteString -> (a -> P.Put) -> P.Put putHeaderL [] _ _ = return () putHeaderL xs h f = P.putByteString h >> P.putByteString ": " >> mapM_ f xs >> P.putByteString "\r\n" +putHeaderMulti :: [a] -> B.ByteString -> (a -> P.Put) -> P.Put+putHeaderMulti vs name f = forM_ vs $ \v -> do+  P.putByteString name+  P.putByteString ": "+  f v+  P.putByteString "\r\n"++whenMaybe :: Maybe a -> (a -> P.Put) -> P.Put+whenMaybe Nothing _ = return ()+whenMaybe (Just x) f = f x++putCookie :: Cookie -> P.Put+putCookie cookie = do+  P.putByteString $ cookieName cookie+  P.putWord8 0x3d  {- '=' -}+  P.putByteString $ cookieValue cookie++  whenMaybe (cookiePath cookie) $ \s -> do+    P.putByteString "; path="+    P.putByteString $ fromString s+  whenMaybe (cookieDomain cookie) $ \s -> do+    P.putByteString "; domain="+    P.putByteString $ fromString s+  whenMaybe (cookieExpires cookie) $ \date -> do+    P.putByteString "; expires="+    putDate date+  when (cookieSecure cookie) $ P.putByteString "; secure"+ putContentRange (Just (a, b), Just c) = putShow a >> putChar '-' >> putShow b >> putChar '/' >> putShow c putContentRange (Just (a, b), Nothing) = putShow a >> putChar '-' >> putShow b >> P.putByteString "/*" putContentRange (Nothing, Just c) = P.putByteString "*/" >> putShow c@@ -707,7 +809,7 @@   putHeaderML (httpPragma headers) "Pragma" putPragma   putHeaderM (httpProxyAuthenticate headers) "Proxy-Authenticate" P.putByteString   putHeaderM (httpProxyAuthorization headers) "Proxy-Authorization" P.putByteString-  putHeaderML (httpRange headers) "Range" putRange+  putHeaderMLE (httpRange headers) "Range" putRange "bytes="   putHeaderM (httpReferer headers) "Referer" P.putByteString   putHeaderM (httpRetryAfter headers) "Retry-After" putShow   putHeaderM (httpServer headers) "Server" P.putByteString@@ -715,12 +817,14 @@   putHeaderML (httpTrailer headers) "Trailer" putString   putHeaderM (httpUserAgent headers) "User-Agent" P.putByteString   putHeaderM (httpWWWAuthenticate headers) "WWW-Authenticate" P.putByteString-  mapM_ (\(k, v) -> P.putByteString k >> putString ": " >> P.putByteString v) $+  putHeaderMulti (httpSetCookie headers) "Set-Cookie" putCookie+  putHeaderMulti (httpSetCookie headers) "Cookie" putCookie+  mapM_ (\(k, v) -> P.putByteString k >> putString ": " >> P.putByteString v >> P.putByteString "\r\n") $          Map.toList $ httpOtherHeaders headers  putRequest :: Request -> P.Put putRequest (Request method url major minor headers) = do-  putShow method >> putChar ' ' >> P.putByteString url >> putChar ' '+  putShow method >> putChar ' ' >> P.putByteString (URL.serialiseRelative url) >> putChar ' '   P.putByteString "HTTP/"   putShow major >> putChar '.' >> putShow minor >> P.putByteString "\r\n"   putHeaders headers
Network/MiniHTTP/MimeTypesParse.hs view
@@ -7,18 +7,20 @@   , parseMimeTypesTotal   ) where -import Prelude hiding (catch)-import Control.Applicative-import Control.Exception (catch)-import qualified Data.Map as Map-import Data.Maybe (catMaybes)+import           Prelude hiding (catch)++import           Control.Applicative+import           Control.Exception (catch)+ import qualified Data.ByteString as B-import Data.ByteString.Internal (w2c)-import qualified Data.Binary.Strict.Get as G+import           Data.ByteString.Internal (w2c)+import qualified Data.Binary.Strict.ByteSet           as BSet import qualified Data.Binary.Strict.Class as C-import qualified Data.Binary.Strict.ByteSet as BSet+import qualified Data.Binary.Strict.Get           as G+import qualified Data.Map as Map+import           Data.Maybe (catMaybes) -import Network.MiniHTTP.Marshal (MediaType)+import           Network.MiniHTTP.Marshal (MediaType)  lws = BSet.fromList [9, 32] newline = BSet.singleton 10
+ Network/MiniHTTP/OpenID.hs view
@@ -0,0 +1,350 @@+{-# LANGUAGE OverloadedStrings #-}+-- | This module contains support for the OpenID authentication standard. See+--   <http://www.openid.net> for details of the protocol. At the moment, only the+--   basic v2 authentication is supported. Also, we only support OpenID 2.0+--   HTML discovery, not Yadis nor XRI.+--+--   Only DH-SHA1 is used for the cryptography. This used to be SHA256, but+--   Yahoo doesn't support it (boo!)+--+--   References in this module are to the OpenID v2 spec+--      <http://openid.net/specs/openid-authentication-2_0.html>+module Network.MiniHTTP.OpenID+  ( -- * Types+    OpenIDDiscovery(..)+  , CheckIDType(..)+  , Handle+  , Key++    -- * Actions+  , findKey+  , discover+  , associate+  , checkID+  , processCheckIDReply+  ) where++import           Control.Monad (liftM)+import           Control.Concurrent.STM+import           Control.Exception (handle, throwIO)++import           Data.Bits (shiftR, xor, (.&.))+import qualified Data.Binary.Strict.Class as C+import qualified Data.Binary.Strict.Get as G+import qualified Data.ByteString as B+import           Data.ByteString.Char8 ()+import           Data.ByteString.Internal (w2c)+import qualified Data.Map as Map+import           Data.Maybe (maybe, fromJust)+import           Data.String (IsString(..))+import           Data.Time.Clock (UTCTime, getCurrentTime, addUTCTime)+import           Data.Word (Word8, Word32)++import qualified Network.Connection as C+import           Network.Socket (sClose)+import           Network.MiniHTTP.Client+import           Network.MiniHTTP.HTTPConnection+import           Network.MiniHTTP.Marshal+import qualified Network.MiniHTTP.URL as URL++import           System.IO.Unsafe (unsafePerformIO)++import           Text.HTML.TagSoup++import qualified OpenSSL.EVP.Base64 as Base64+import qualified OpenSSL.EVP.Digest as Digest+import qualified OpenSSL.BN as BN++-- | This is the result of Discovery: the OP local identity and the server HTTP+--   endpoint.+data OpenIDDiscovery =+  OpenIDDiscovery { discoveryProvider :: URL.URL+                  , discoveryLocalID :: Maybe String+                  } deriving (Show, Eq)++-- | This is the number of bytes of HTML that we'll try and read in order to+--   find the OpenID link elements+discoveryHTMLLimit :: Integral i => i+discoveryHTMLLimit = 4096++-- | Use HTML discovery to find the OpenID information for a given URL+discover :: URL.URL -> IO OpenIDDiscovery+discover uri = do+  r <- fetchBasic (emptyHeaders { httpRange = Just [RangeOf 0 discoveryHTMLLimit] }) uri+  case r of+       (conn, _, Nothing) -> do+         C.close conn+         fail "HTTP server returned no content"+       (conn, _, Just source) -> do+         let f d@(mprovider, mlocalid) (TagOpen "link" attrs) = maybe d id $ do+               rel <- "rel" `lookup` attrs+               href <- "href" `lookup` attrs+               case rel of+                    "openid2.provider" -> do+                      uri <- URL.parse $ fromString href+                      return (Just uri, mlocalid)+                    "openid2.local_id" -> return (mprovider, Just href)+                    _ -> return d++         payload <- sourceToBS 4096 source+         C.close conn+         case payload of+              Nothing -> fail "Error reading HTTP reply"+              Just payload -> do+                let (mprovider, mlocalid) =+                      foldl f (Nothing, Nothing) $ map head $+                        concatMap (sections (~== ("<link>" :: String))) $+                        sections (~== ("<head>" :: String)) $+                        parseTags $ map w2c $ B.unpack payload++                case mprovider of+                     Nothing -> fail "No provider discovered"+                     Just provider -> return $ OpenIDDiscovery provider mlocalid++-- | The default DH generator+dhG :: Integer+dhG = 2+-- | The default DH prime. See appendix B+dhP :: Integer+dhP = 0xDCF93A0B883972EC0E19989AC5A2CE310E1D37717E8D9571BB7623731866E61EF75A2E27898B057F9891C2E27A639C3F29B60814581CD3B2CA3986D2683705577D45C2E7E52DC81C7A171876E5CEA74B1448BFDFAF18828EFD2519F14E45E3826634AF1949E5B535CC829A483B8A76223E5D490A257F05BDFF16F2FB22C583AB++-- | Encode a URL of URL query key-value pairs into a query string (not+--   including the leading "?"). The string "openid." is prepended to all the+--   key names. See 4.1.2+postEncode :: Map.Map B.ByteString B.ByteString -> B.ByteString+postEncode = URL.serialiseArguments . Map.mapKeys (B.append "openid.")++-- | Encode a list of key-value pairs, in order, into the OpenID Key Value+--   format. See 4.1.1.+keyValueEncode :: [(B.ByteString, B.ByteString)] -> B.ByteString+keyValueEncode values = (B.intercalate (B.singleton 10) $ map f values) `B.append` B.singleton 10 where+  f (key, value) = key `B.append`+                   B.singleton 0x3a `B.append`+                   value++-- | A map of the default parameters included in all OpenID requests+defaultParams :: Map.Map B.ByteString B.ByteString+defaultParams = Map.fromList [("ns", "http://specs.openid.net/auth/2.0")]++-- | Convert an Integer to base64(btwoc) form. See 4.2+integerToBase64btwoc :: Integer -> IO B.ByteString+integerToBase64btwoc i = liftM (Base64.encodeBase64BS . B.drop 4) $ BN.integerToMPI i++-- | Convert a ByteString in btwoc form to an Integer. See 4.2+btwocToInteger :: B.ByteString -> IO Integer+btwocToInteger bs = do+  let len :: Word32+      len = fromIntegral $ B.length bs+      lengthbytes = B.pack $ map (\n -> fromIntegral $ (len `shiftR` n) .&. 0xff) [24, 16, 8, 0]+      mpi = lengthbytes `B.append` bs+  BN.mpiToInteger mpi++-- | The type of an OpenID handle. Handles are used to identify sessions+--   between the consumer and OP.+newtype Handle = Handle B.ByteString deriving (Show, Eq)+-- | The type of a key.+newtype Key = Key B.ByteString deriving (Show, Eq)++-- | This is the cache of association handles. It maps a string (hostname ++--   path of the OP) to a handle, key, expiry time triple.+associateCache :: TVar (Map.Map B.ByteString (Handle, Key, UTCTime))+associateCache = unsafePerformIO $ newTVarIO Map.empty++updateTVar :: TVar a -> (a -> a) -> STM ()+updateTVar var f = readTVar var >>= writeTVar var . f++-- | Lookup a key given the hostname of the OP and the handle. Generally used+--   after an indirect request to check a signature from an OP.+findKey :: B.ByteString -> Handle -> STM (Maybe Key)+findKey host (Handle handle) = do+  cache <- readTVar associateCache+  case Map.lookup host cache of+       Nothing -> return Nothing+       Just ((Handle handle'), key, _) ->+         if handle == handle'+            then return $ Just key+            else return Nothing++-- | Perform an association with a discovered OP and return either an error+--   message or a handle, a key and the number of seconds from now when the+--   handle will expire.+--+--   Internally this uses a cache so 'associate' may not actually involve an+--   HTTP request to the OP.+associate :: OpenIDDiscovery -> IO (Handle, Key)+associate discovery@(OpenIDDiscovery provider _) = do+  let cacheKey = fromString $ show provider+  currentTime <- getCurrentTime+  v <- atomically $ do+    cache <- readTVar associateCache+    case cacheKey `Map.lookup` cache of+         Nothing -> return Nothing+         Just v@(_, _, expiry) ->+           -- if the key expires in the next five minutes, dump it now+           if 300 `addUTCTime` expiry > currentTime+              then do writeTVar associateCache $ cacheKey `Map.delete` cache+                      return Nothing+              else return $ Just v+  case v of+       Just (h, k, _) -> return (h, k)+       Nothing -> do+         (h, k, secs) <- associateHTTP discovery+         let expiry = addUTCTime (fromIntegral secs) currentTime+         atomically $ do+           updateTVar associateCache (Map.insert cacheKey (h, k, expiry))+           return (h, k)++-- | Convert a URL to a Host header+urlToHost :: URL.URL -> Maybe B.ByteString+urlToHost (URL.URL {URL.urlHost = URL.Hostname h}) = Just h+urlToHost _ = Nothing++-- | An implementation of 'associate' which does an actual HTTP lookup+--   everytime. This is wrapped by 'associate', which handles caching of these+--   values. You can call this directly to bypass the cache.+associateHTTP :: OpenIDDiscovery -> IO (Handle, Key, Int)+associateHTTP (OpenIDDiscovery provider _) = do+  sock <- connection provider+  handle (\e -> sClose sock >> throwIO e) $ do+    conn <- transport provider sock+    (postbody, x) <- associateRequest+    postsource <- bsSource postbody+    r <- request conn (Request POST (URL.toRelative provider) 1 1 $+            emptyHeaders { httpHost = urlToHost provider+                         , httpContentType = Just (("application", "x-www-form-urlencoded"), [])+                         , httpContentLength = Just $ fromIntegral $ B.length postbody+                         }) $ Just postsource+    case r of+         (Just (Reply {replyStatus = 200}, Just source)) -> do+           mreplyBytes <- sourceToBS 4096 source+           C.close conn+           case mreplyBytes of+                Nothing -> fail "Error reading reply"+                Just replyBytes -> do+                  case G.runGet (parseKeyValue 10 0x3a) replyBytes of+                       (Right reply, _) -> processAssociateReply reply x+                       _ -> fail "Error parsing reply"+         _ -> print (fst $ fromJust r) >> fail "Bad HTTP reply code"++-- | Handle a reply from an associate call.+processAssociateReply :: Map.Map B.ByteString B.ByteString  -- ^ reply from the server+                      -> Integer  -- ^ the x value from associateRequest+                      -> IO (Handle, Key, Int)+                         -- ^ (handle, key, seconds to expiry)+processAssociateReply reply x =+  mapWrapper reply["assoc_handle", "dh_server_public", "enc_mac_key", "expires_in"] $+    \[handle, serverPublic, encKey, expiresStr] -> do+       Just sha1 <- Digest.getDigestByName "SHA1"+       gy <- btwocToInteger $ Base64.decodeBase64BS serverPublic+       let encKey' = Base64.decodeBase64BS encKey+           shared = BN.modexp gy x dhP+       sharedmpi <- BN.integerToMPI shared+       print encKey'+       print $ B.length encKey'+       let sharedbtwoc = B.drop 4 sharedmpi+           sharedkey = Digest.digestBS' sha1 sharedbtwoc+           key = B.pack $ B.zipWith (xor) sharedkey encKey'+       case maybeRead expiresStr of+            Nothing -> fail "Failed to parse expiry"+            Just expires -> return (Handle handle, Key key, expires)+++-- | This creates an associate request and returns the URL query and the value+--   of @x@+associateRequest :: IO (B.ByteString, Integer)+associateRequest = do+  x <- BN.randIntegerUptoNMinusOneSuchThat (/= 0) dhP+  let gxmodp = BN.modexp dhG x dhP+  encoded <- integerToBase64btwoc gxmodp++  let m = Map.union defaultParams $ Map.fromList extras+      extras = [ ("mode", "associate")+               , ("assoc_type", "HMAC-SHA1")+               , ("session_type", "DH-SHA1")+               , ("dh_consumer_public", encoded)+               ]+  print encoded+  return (postEncode m, x)++-- | A helper function which extracts a number of keys from a list, calling a+--   continuation with those values if all are found and returning a Left if+--   any are missing+mapWrapper :: Map.Map B.ByteString B.ByteString  -- ^ the map of values+           -> [B.ByteString]  -- ^ the required keys+           -> ([B.ByteString] -> IO a)  -- ^ the continuation+           -> IO a  -- ^ the result+mapWrapper m keys f =+  case mapM (flip Map.lookup m) keys of+       Nothing -> fail "Map missing required value"+       Just values -> f values++parseKeyValue :: (C.BinaryParser m)+              => Word8  -- ^ byte which breaks 'lines' (e.g. \n)+              -> Word8  -- ^ byte which breaks pairs (e.g. ':')+              -> m (Map.Map B.ByteString B.ByteString)+parseKeyValue lineBreak pairBreak = do+  let parseLine = do+        key <- C.spanOf1 (/= pairBreak)+        C.word8 pairBreak+        value <- C.spanOf1 (/= lineBreak)+        if B.isPrefixOf "openid." key+           then return (B.drop 7 key, value)+           else return (key, value)+  line <- parseLine+  rest <- C.many (C.word8 lineBreak >> parseLine)+  C.optional $ C.word8 lineBreak+  donep <- C.isEmpty+  if not donep+     then fail "Trailing garbage found"+     else return $ Map.fromList $ line : rest++-- | There are two types of checkid calls.+data CheckIDType = CheckIDSetup | CheckIDImmediate deriving (Show, Eq)++typeToString :: CheckIDType -> B.ByteString+typeToString CheckIDSetup = "checkid_setup"+typeToString CheckIDImmediate = "checkid_immediate"++-- | Construct a checkid call+checkID :: CheckIDType+        -> URL.URL  -- ^ claimed id+        -> OpenIDDiscovery -- ^ OP-local id+        -> Handle  -- ^ assoc handle+        -> B.ByteString  -- ^ return to URL+        -> Maybe B.ByteString  -- ^ trust realm+        -> URL.URL  -- ^ URL+checkID ty claimed (OpenIDDiscovery provider mlocalid) (Handle handle) returnTo realm = r where+  r = provider { URL.urlArguments = foldl Map.union Map.empty+                   [ URL.urlArguments provider+                   , Map.mapKeys (B.append "openid.") defaultParams+                   , Map.mapKeys (B.append "openid.") $ Map.fromList rest+                   ] }+  common = [ ("mode", typeToString ty)+           , ("claimed_id", URL.serialise claimed)+           , ("identity", maybe (URL.serialise claimed) fromString mlocalid)+           , ("assoc_handle", handle)+           , ("return_to", returnTo)+           ]+  rest = case realm of+              Nothing -> common+              Just realm -> ("realm", realm) : common++processCheckIDReply :: Map.Map B.ByteString B.ByteString  -- ^ the arguments+                    -> IO (Either String B.ByteString)+processCheckIDReply args' = do+  -- first strip openid. from the front of all the keys+  let args = Map.mapKeys (\k -> if "openid." `B.isPrefixOf` k then B.drop 7 k else k) args'+  Just sha1 <- Digest.getDigestByName "SHA1"+  mapWrapper args ["assoc_handle", "claimed_id", "op_endpoint", "signed", "sig"] $+    \[handle, claimed, endpoint, signed, sig] -> do+    mkey <- atomically $ findKey endpoint $ Handle handle+    case mkey of+         Nothing -> return $ Left "Cannot find assoc key"+         Just (Key key) -> do+           let signedFields = B.split 0x2c signed+            in mapWrapper args signedFields $ \signedValues -> do+                 let kv = keyValueEncode $ zip signedFields signedValues+                     mySig = Base64.encodeBase64BS $ Digest.hmacBS sha1 key kv+                 if mySig == sig+                    then return $ Right claimed+                    else return $ Left "OpenID signature verification failed"
Network/MiniHTTP/Server.hs view
@@ -1,22 +1,22 @@+{-# LANGUAGE OverloadedStrings #-} -- | This module contains functions for writing webservers. These servers --   process requests in a state monad pipeline and several useful actions are---   provided here in. See examples/fileserver.hs for an example of how to use---   this module.+--   provided herein.+--+--   See @examples/test.hs@ for an example of how to use this module. module Network.MiniHTTP.Server-  ( -- * Sources-    Source-  , SourceResult(..)-  , bsSource-  , hSource-  , nullSource--  -- * The processing monad-  , WebMonad-  , WebState+  ( -- * The processing monad+    WebMonad+  , WebState(..)   , getRequest+  , getPayload+  , getPOST+  , getGET   , getReply   , setReply   , setHeader+  , setCookie+  , errorPage    -- * WebMonad actions   , handleConditionalRequest@@ -26,104 +26,60 @@   , handleFromFilesystem    -- * Running the server-  , serve+  , serveHTTP+  , serveHTTPS+  , DispatchMatch(..)+  , dispatchOnURL   ) where -import Prelude hiding (foldl, elem, catch)+import           Prelude hiding (foldl, catch) -import Control.Concurrent.STM-import Control.Monad (liftM)-import Control.Monad.State.Strict-import Control.Exception (catch)+import           Control.Concurrent.STM+import           Control.Exception (catch)+import           Control.Monad.State.Strict -import Data.Foldable-import Data.Word (Word16)-import Data.Bits (shiftL, shiftR, (.|.), (.&.))-import Data.Maybe (isNothing, isJust, fromJust, catMaybes, maybe)-import Data.Time.Clock.POSIX-import Data.IORef-import Data.Int (Int64)+import qualified Data.Binary.Put as P import qualified Data.ByteString as B-import Data.ByteString.Internal (w2c, c2w)-import Data.ByteString.Char8 ()+import           Data.ByteString.Char8 () import qualified Data.ByteString.Lazy as BL+import           Data.ByteString.Internal (c2w, w2c)+import           Data.Char (chr)+import           Data.Int (Int64) import qualified Data.Map as Map-import qualified Data.Sequence as Seq-import System.IO-import System.Posix-import System.FilePath (combine, splitDirectories, joinPath, takeExtension)-import Network.Socket hiding (send, sendTo, recv, recvFrom)-import Network.Socket.ByteString-import Text.Printf (printf)--import qualified Data.Binary.Put as P-import qualified Data.Binary.Strict.IncrementalGet as IG-import Network.MiniHTTP.Marshal-import qualified Network.MiniHTTP.Connection as C-import Network.MiniHTTP.MimeTypesParse---- | This assumes a little endian system because I can't find a nice way to probe---   the endianness-htons :: Word16 -> Word16-htons x = ((x .&. 255) `shiftL` 8) .|. (x `shiftR` 8)---- | A source is a stream of data, like a lazy data structure, but without---   some of the dangers that such entail. A source returns a @SourceResult@---   each time you evaluate it.-type Source = IO SourceResult--data SourceResult = SourceError  -- ^ error - please don't read this source again-                  | SourceEOF  -- ^ end of data-                  | SourceData B.ByteString  -- ^ some data-                  deriving (Show)+import           Data.Maybe (isNothing, isJust, fromJust, catMaybes, maybe)+import           Data.String (fromString)+import           Data.Time.Clock.POSIX --- | Construct a source from a ByteString-bsSource :: B.ByteString -> IO Source-bsSource bs = do-  ref <- newIORef $ SourceData bs-  return $ do-    v <- readIORef ref-    writeIORef ref SourceEOF-    return v+import           System.FilePath (combine, splitDirectories, joinPath, takeExtension)+import           System.IO+import           System.IO.Unsafe (unsafePerformIO)+import           System.Posix+import qualified System.Posix.Signals as Signal --- | Construct a source from a Handle-hSource :: (Int64, Int64)  -- ^ the first and last byte to include-               -> Handle  -- ^ the handle to read from-               -> IO Source-hSource (from, to) handle = do-  bytesSoFar <- newIORef (from :: Int64)-  hSeek handle AbsoluteSeek (fromIntegral from)-  return $ do-    catch-      (do done <- readIORef bytesSoFar-          bytes <- B.hGet handle $ min (128 * 1024) (fromIntegral $ (to + 1) - done)-          if B.length bytes == 0-             then do-               if to + 1 == done-                  then return SourceEOF-                  else return SourceError-             else do modifyIORef bytesSoFar ((+) (fromIntegral $ B.length bytes))-                     return $ SourceData bytes)-      (const $ return SourceError)+import qualified OpenSSL.Session as SSL --- | A source with no data (e.g. /dev/null)-nullSource :: Source-nullSource = return SourceEOF+import qualified Network.Connection as C+import           Network.Socket hiding (send, sendTo, recv, recvFrom)+import           Network.MiniHTTP.Marshal+import           Network.MiniHTTP.MimeTypesParse+import           Network.MiniHTTP.HTTPConnection+import qualified Network.MiniHTTP.URL as URL --- | Processing a request involve running a number of actions in a StateT monad+-- | Processing a request involves running a number of actions in a StateT monad --   where the state for that monad is this record. This contains both a---   @Source@ and a @Handle@ element. Often something will fill in the @Handle@---   and expect later processing to convert it to a @Source@. Somehow, you have---   to end up with a @Source@, however.+--   @Source@ and a "Handle" element. Often something will fill in the "Handle"+--   and expect later processing to convert it to a "Source". Somehow, you have+--   to end up with a "Source", however. data WebState =   WebState { wsRequest :: Request  -- ^ the original request-             -- | the system mime types db, mapping file extensions+           , wsBody :: Maybe Source  -- ^ the client's payload            , wsMimeTypes :: Map.Map B.ByteString MediaType+             -- ^ the system mime types db, mapping file extensions            , wsReply :: Reply   -- ^ the current reply            , wsSource :: Maybe Source  -- ^ the current source            , wsHandle :: Maybe Handle  -- ^ the current handle-             -- | an action to be performed before sending the reply            , wsAction :: Maybe (IO ())+             -- ^ an action to be performed before sending the reply            }  -- | The processing monad@@ -137,6 +93,25 @@ getReply :: WebMonad Reply getReply = get >>= return . wsReply +-- | Return the client's request payload (if any)+getPayload :: WebMonad (Maybe Source)+getPayload = get >>= return . wsBody++-- | Get the arguments to a POST request+getPOST :: Int  -- ^ max number of bytes to read+        -> WebMonad (Map.Map B.ByteString B.ByteString)+getPOST maxBytes = do+  -- My kingdom for a MaybeT+  msource <- getPayload+  maybe (return Map.empty) (\source -> do+    mbs <- liftIO $ sourceToBS maxBytes source+    maybe (return Map.empty) (\bs -> do+      maybe (return Map.empty) return $ URL.parseArguments bs) mbs) msource++-- | Get the arguments to a GET request+getGET :: WebMonad (Map.Map B.ByteString B.ByteString)+getGET = liftM (URL.rurlArguments . reqUrl) getRequest+ -- | Set the current reply to be a reply with the given status code, the --   default message for that status code, an empty body and an empty set of --   headers.@@ -158,6 +133,16 @@   s <- get   put $ s { wsReply = reply { replyHeaders = f h } } +setCookie :: Cookie -> WebMonad ()+setCookie newcookie@(Cookie { cookieName = n }) = do+  reply <- getReply+  let h = replyHeaders reply+      sets = httpSetCookie h+      sets' = if any (\cookie -> cookieName cookie == n) sets+                 then map (\cookie -> if cookieName cookie == n then newcookie else cookie) sets+                 else newcookie : sets+  s <- get+  put $ s { wsReply = reply { replyHeaders = h { httpSetCookie = sets' } } }  -- | This handles the If-*Matches and If-*Modified conditional headers. It takes --   its information from the Last-Modified and ETag headers of the current@@ -299,131 +284,197 @@    -- stopping directory traversal needs to be done a little carefully.   -- Hopefully this is all correct-  let path = reqUrl req+  let path = map w2c $ B.unpack $ URL.rurlPath $ reqUrl req       -- First, make sure that there aren't any NULs in the path-      path' = B.takeWhile (/= 0) path-      path'' = map w2c $ B.unpack path'-      elems = splitDirectories path''+      path' = takeWhile (/= chr 0) path+      elems = splitDirectories path'       -- Remove any '..'       elems' = filter (\x -> x /= ".." && x /= "/") elems-      ext = takeExtension path''+      ext = takeExtension path'       filepath = combine docroot $ joinPath elems'   mimeTypes <- get >>= return . wsMimeTypes   s <- get-  s' <- lift $ catch+  r <- lift $ catch     (do fd <- openFd filepath ReadOnly Nothing (OpenFileFlags False False True False False)         stat <- getFdStatus fd         let size = fromIntegral $ fileSize stat             mtime = posixSecondsToUTCTime $ fromRational $ toRational $ modificationTime stat         handle <- fdToHandle fd-        return $ s { wsHandle = Just handle-                   , wsSource = Nothing-                   , wsReply = Reply 1 1 200 "Ok" $ emptyHeaders-                      { httpLastModified = Just mtime-                      , httpContentLength = Just size-                      , httpContentType = Map.lookup (B.pack $ map c2w ext) mimeTypes } } )-    (const $ return $ s { wsReply = Reply 1 1 404 "Not found" $ emptyHeaders })-  put s'+        return $ Just $+          s { wsHandle = Just handle+            , wsSource = Nothing+            , wsReply = Reply 1 1 200 "Ok" $ emptyHeaders+               { httpLastModified = Just mtime+               , httpContentLength = Just size+               , httpContentType = Map.lookup (B.pack $ map c2w ext) mimeTypes } } )+    (const $ return Nothing)+  case r of+       Just x -> put x+       Nothing -> errorPage "File not found"  pipeline :: Map.Map B.ByteString MediaType          -> WebMonad ()          -> Request+         -> Maybe Source          -> IO (Reply, Source)-pipeline mimetypes action req = do-  let initState = (WebState req mimetypes (Reply 1 1 500 "Server error" emptyHeaders)+pipeline mimetypes action req msource = do+  let initState = (WebState req msource mimetypes (Reply 1 1 500 "Server error" emptyHeaders)                    Nothing Nothing Nothing)-  (_, s) <- runStateT (do+  (_, s) <- catch (+    runStateT (do     action-    handleFinal) initState+    handleFinal) initState)+    (\e -> runStateT (do+             errorPage $ show e+             handleFinal) initState)    return (wsReply s, fromJust $ wsSource s) --- | Block until the given queue has less than the given number of bytes in it---   then enqueue a new ByteString.-waitForLowWaterAndEnqueue :: Int  -- ^ the max number of bytes in the queue before we enqueue anything-                          -> C.Connection  -- ^ the connection to write to-                          -> B.ByteString  -- ^ the data to enqueue-                          -> IO ()-waitForLowWaterAndEnqueue lw conn bs = do-  atomically $ do-    q <- readTVar $ C.connoutq conn-    let size = foldl (\sz bs -> sz + B.length bs) 0 q-    if size > lw-       then retry-       else writeTVar (C.connoutq conn) $ bs Seq.<| q- -- | Read a single request from a socket-readRequest :: Socket-            -> B.ByteString  -- ^ data which has already been read from the socket-            -> IO (B.ByteString, Request)-readRequest socket initbs = do-  let f result = do-        case result of-             IG.Failed _ -> fail "Parse failed"-             IG.Partial cont -> recv socket 256 >>= (\x -> (f $ cont x))-             IG.Finished rest result -> return (rest, result)-  f $ IG.runGet parseRequest initbs+readRequest :: C.Connection+            -> IO Request+readRequest conn = readIG conn 256 4096 parseRequest >>= return . fromJust  -- | Loop, reading and processing requests-readRequests :: (Request -> IO (Reply, IO SourceResult))+readRequests :: (Request -> Maybe Source -> IO (Reply, IO SourceResult))              -> C.Connection-             -> B.ByteString  -- ^ previously read data              -> IO ()-readRequests handler conn initbs = do-  (rest, result) <- readRequest (C.connsocket conn) initbs-  (reply, source) <- handler result+readRequests handler conn = do+  result <- readRequest conn+  body <-+    case httpContentLength $ reqHeaders result of+         Nothing -> return Nothing+         Just n -> connSource n B.empty conn >>= return . Just+  (reply, source) <- handler result body   let lowWater = 32 * 1024-      stream = do-        next <- source-        case next of-             SourceEOF -> return True-             SourceError -> return False-             SourceData bs -> waitForLowWaterAndEnqueue lowWater conn bs >> stream-      streamChunked = do-        next <- source-        case next of-             SourceEOF -> do-               waitForLowWaterAndEnqueue lowWater conn "0\r\n\r\n"-               return True-             SourceError -> return False-             SourceData bs -> do-               waitForLowWaterAndEnqueue lowWater conn $ B.pack $ map c2w $-                 printf "%d\r\n\r\n" $ B.length bs-               waitForLowWaterAndEnqueue lowWater conn bs-               waitForLowWaterAndEnqueue lowWater conn "\r\n"-               streamChunked-  waitForLowWaterAndEnqueue (32 * 1024) conn $ B.concat $ BL.toChunks $ P.runPut $ putReply reply+  atomically $ C.writeAtLowWater lowWater conn $ B.concat $ BL.toChunks $ P.runPut $ putReply reply   success <- if isNothing $ httpContentLength $ replyHeaders reply-                then streamChunked-                else stream+                then streamSourceChunked lowWater conn source+                else streamSource lowWater conn source   if not success      then C.close conn-     else readRequests handler conn rest+     else do case body of+                  Nothing -> return ()+                  Just source -> sourceDrain source+             readRequests handler conn -acceptLoop :: Socket -> (Request -> IO (Reply, Source)) -> IO ()-acceptLoop acceptingSocket handler = do+sslHandshake :: SSL.SSL -> IO () -> IO ()+sslHandshake ssl k = SSL.accept ssl >> k++acceptLoop :: (Request -> Maybe Source -> IO (Reply, Source)) -> Socket -> IO ()+acceptLoop handler acceptingSocket = do   (newsock, addr) <- accept acceptingSocket   setSocketOption newsock NoDelay 1   putStrLn $ "Connection from " ++ show addr -  c <- atomically $ C.new newsock (return ())-  C.forkThreads c $ readRequests handler c B.empty-  acceptLoop acceptingSocket handler+  c <- C.new (return ()) $ C.baseConnectionFromSocket newsock+  C.forkInConnection c $ readRequests handler c+  acceptLoop handler acceptingSocket -serve :: Int  -- ^ the port number to listen on-      -> WebMonad ()  -- ^ the processing action+acceptLoopHTTPS :: SSL.SSLContext+                -> (Request -> Maybe Source -> IO (Reply, Source))+                -> Socket+                -> IO ()+acceptLoopHTTPS ctx handler acceptingSocket = do+  (newsock, addr) <- accept acceptingSocket+  setSocketOption newsock NoDelay 1+  putStrLn $ "Connection from " ++ show addr++  ssl <- SSL.connection ctx newsock+  c <- C.new (return ()) $ sslToBaseConnection ssl+  C.forkInConnection c $ sslHandshake ssl $ readRequests handler c+  acceptLoopHTTPS ctx handler acceptingSocket++errorPage :: String -> WebMonad ()+errorPage error = (do+  s <- get+  source <- liftIO $ bsSource message+  put $ s { wsSource = Just source }+  setHeader $ \h -> h { httpContentLength = Just $ fromIntegral $ B.length message }+  handleDecoration) where+  message = head `B.append` errorbs `B.append` tail+  head = "<html> <head> <title>Network.MiniHTTP error page</title> <style language=\"text/css\"> #top { height: 1.5em; width: 100%; background-color: #BFD9FF; border-bottom: 3px solid #004FBF; margin-bottom: 2em; padding-left: 1em; font-variant: small-caps; font-size: 2em; padding-top: 0.5em; } body { margin: 0 0 0 0; } #main { margin-left: 4px; } .enbox { padding-left: 2em; background-color: \"#003786\" } h4 { color: #004FBF; } </style> </head> <body> <div id=\"top\">Network.MiniHTTP</div> <div id=\"main\"> <h4>An error occured while processing your request:</h4> <pre class=\"enbox\">"+  tail = "</pre> </div> </body> </html>"+  errorbs = fromString $ concatMap escape error+  escape '<' = "&lt;"+  escape '&' = "&amp;"+  escape '>' = "&gt;"+  escape x = [x]++data DispatchMatch = Exact B.ByteString+                   | Prefix B.ByteString+                   deriving (Show, Eq)++dispatchMatch :: B.ByteString -> DispatchMatch -> Bool+dispatchMatch b (Exact m) = b == m+dispatchMatch b (Prefix p) = p `B.isPrefixOf` b++-- | This is an, optional, helper function which you might find useful. The+--   serving fuctions both expect a "WebMonad" action which is called to+--   process each request. In general you have to write that and dispatch based+--   on the client's request.+--+--   This might save you some work: it tries each of the elements in the list+--   in turn. As soon as one matches it runs the given action to process the+--   request.+dispatchOnURL :: [(DispatchMatch, WebMonad ())]+                 -- ^ the list of URL prefixes (with '/'!) and their actions+              -> WebMonad ()+dispatchOnURL paths = do+  req <- getRequest+  let path = URL.rurlPath $ reqUrl req++  case map snd $ filter (dispatchMatch path . fst) paths of+       [] -> errorPage "No dispatchers matched requested URL"+       x:_ -> x++globalMimeTypes :: Map.Map B.ByteString MediaType+globalMimeTypes = unsafePerformIO $+  parseMimeTypesTotal "/etc/mime.types" >>= return . maybe Map.empty id++serve :: Int -- ^ port number+      -> (Socket -> IO ())  -- ^ accept loop       -> IO ()-serve portno action = do-  -- Switch these two lines to use IPv6 (which works for IPv4 clients too)+serve portno acceptLoop = do+  --  Switch these two lines to use IPv6 (which works for IPv4 clients too). Not+  --  all systems support this   --acceptingSocket <- socket AF_INET6 Stream 0-  --let sockaddr = SockAddrInet6 (PortNum $ htons $ fromIntegral portno) 0 iN6ADDR_ANY 0-+  --let sockaddr = SockAddrInet6 (fromIntegral portno) 0 iN6ADDR_ANY 0   acceptingSocket <- socket AF_INET Stream 0-  let sockaddr = SockAddrInet (PortNum $ htons $ fromIntegral portno) iNADDR_ANY+  let sockaddr = SockAddrInet (fromIntegral portno) iNADDR_ANY+   setSocketOption acceptingSocket ReuseAddr 1   bindSocket acceptingSocket sockaddr   listen acceptingSocket 1-  mimetypes <- parseMimeTypesTotal "/etc/mime.types" >>= return . maybe Map.empty id -  catch (acceptLoop acceptingSocket $ pipeline mimetypes action)+  -- Ignore SIGPIPE+  Signal.installHandler Signal.sigPIPE Signal.Ignore Nothing++  catch (acceptLoop acceptingSocket)         (const $ sClose acceptingSocket)+++-- | Start an IPv4 HTTP server+serveHTTP :: Int  -- ^ the port number to listen on+          -> WebMonad ()  -- ^ the processing action+          -> IO ()+serveHTTP portno action = do+  serve portno $ acceptLoop $ pipeline globalMimeTypes action++-- | Start an IPv4 HTTPS server. Plese remember to have wrapped your main+--   function in 'OpenSSL.withOpenSSL' otherwise you'll probably crash the+--   process.+serveHTTPS :: Int  -- ^ the port number to listen on+           -> FilePath  -- ^ path to public key (certificate)+           -> FilePath  -- ^ path to private key+           -> WebMonad ()  -- ^ the processing action+           -> IO ()+serveHTTPS portno public private action = do+  ctx <- SSL.context+  SSL.contextSetPrivateKeyFile ctx private+  SSL.contextSetCertificateFile ctx public+  SSL.contextSetDefaultCiphers ctx+  goodp <- SSL.contextCheckPrivateKey ctx+  when (not goodp) $ fail "Public/private key mismatch"++  serve portno $ acceptLoopHTTPS ctx $ pipeline globalMimeTypes action
+ Network/MiniHTTP/Session.hs view
@@ -0,0 +1,155 @@+{-# LANGUAGE OverloadedStrings #-}+-- | Basic session support. Sessions are (currently) just maps of strings to+--   strings which are serialised and sent to the client using a Cookie.+--   Serialised, they should be less than 4K, so the sum of all your strings in+--   the map should be < 3900 bytes to be safe.+--+--   The cookies are HMACed and encrypted so that the client can't inspect nor+--   alter them. The key is, by default, generated randomly every time the+--   server starts. If you want the cookies to be reusable across restarts or+--   servers you need to set the key yourself.+--+--   Also, by the default, the cookies are set to expire when the browser+--   session ends.+module Network.MiniHTTP.Session+  ( getSession+  , putSession+  , addSession+  , setSessionSecretKey+  ) where++import           Control.Concurrent.STM+import           Control.Monad (forM_)+import           Control.Monad.Trans (liftIO)++import qualified Data.Map as Map+import qualified Data.Binary.Put           as P+import qualified Data.Binary.Strict.Get           as G+import           Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 ()+import qualified Data.ByteString.Lazy as BL+import           Data.ByteString.Internal (w2c)+import           Data.Word (Word32)++import           Network.MiniHTTP.Server+import           Network.MiniHTTP.Marshal++import           System.IO.Unsafe (unsafePerformIO)++import qualified OpenSSL.EVP.Base64 as Base64+import qualified OpenSSL.EVP.Cipher as Cipher+import qualified OpenSSL.EVP.Digest           as Digest+import           OpenSSL.Random (randBytes)++toString :: B.ByteString -> String+toString = map w2c . B.unpack++secretKey :: TVar B.ByteString+secretKey = unsafePerformIO $ do+  rand <- randBytes 16+  newTVarIO rand++-- | Set the secret key used to HMAC and encrypt the session cookies.+setSessionSecretKey :: B.ByteString -> STM ()+setSessionSecretKey = writeTVar secretKey++sha256 :: Digest.Digest+Just sha256 = unsafePerformIO $ Digest.getDigestByName "SHA256"+aes128 :: Cipher.Cipher+Just aes128 = unsafePerformIO $ Cipher.getCipherByName "AES128"++parseMap :: G.Get [(B.ByteString, B.ByteString)]+parseMap = do+  emptyp <- G.isEmpty+  if emptyp+     then return []+     else do+       keylen <- varInt32+       key <- G.getByteString keylen+       valuelen <- varInt32+       value <- G.getByteString valuelen+       rest <- parseMap+       rest `seq` (return $ (key, value) : rest)++putMap :: [(B.ByteString, B.ByteString)] -> P.Put+putMap vs = do+  forM_ vs $ \(key, value) -> do+    putVarInt32 $ B.length key+    P.putByteString key+    putVarInt32 $ B.length value+    P.putByteString value++decode :: B.ByteString -> IO (Map.Map B.ByteString B.ByteString)+decode bs = do+  secret <- atomically $ readTVar secretKey+  let bs' = Base64.decodeBase64BS bs+      (mac, bs'') = B.splitAt 32 bs'+  if Digest.hmacBS sha256 secret bs'' /= mac+     then return Map.empty+     else do+       let (iv, ciphertext) = B.splitAt 16 bs''+       plaintext <- Cipher.cipherBS aes128 (toString secret) (toString iv) Cipher.Decrypt ciphertext+       case G.runGet parseMap plaintext of+            (Left _, _) -> return Map.empty+            (Right m, _) -> return $ Map.fromList m++encode :: Map.Map B.ByteString B.ByteString -> IO B.ByteString+encode map = do+  secret <- atomically $ readTVar secretKey+  iv <- randBytes 16+  let plaintext = B.concat $ BL.toChunks $ P.runPut $ putMap $ Map.toList map+  ciphertext <- Cipher.cipherBS aes128 (toString secret) (toString iv) Cipher.Encrypt plaintext+  let bs = iv `B.append` ciphertext+      mac = Digest.hmacBS sha256 secret bs+  return $ Base64.encodeBase64BS $ mac `B.append` bs++-- | Return the current session. If the user didn't present a cookie, or the+--   cookie is invalid, an empty map is returned.+getSession :: WebMonad (Map.Map B.ByteString B.ByteString)+getSession = do+  req <- getRequest+  case filter (\cookie -> cookieName cookie == "nmhs") $ httpCookie $ reqHeaders req of+       [] -> return Map.empty+       x:_ -> liftIO $ decode $ cookieValue x++-- | Set the current session. This alters the headers of the current request,+--   so future actions which reset the headers (like 'setReply') will undo+--   this.+putSession :: Map.Map B.ByteString B.ByteString -> WebMonad ()+putSession m = do+  v <- liftIO $ encode m+  setCookie $ emptyCookie { cookieName = "nmhs"+                          , cookieValue = v+                          , cookiePath = Just "/" }++-- | Add a key value pair to the session+addSession :: B.ByteString -> B.ByteString -> WebMonad ()+addSession key value = do+  s <- getSession+  putSession $ Map.insert key value s++-- | This puts a generic Integral type, but first it casts to a Word32 because+--   it makes a different when it comes to negative numbers.+varInt32 :: (Integral i) => G.Get i+varInt32 = varIntWord32 >>= return . fromIntegral++varIntWord32 :: G.Get Word32+varIntWord32 = do+  byte <- G.getWord8+  if byte `testBit` 7+     then do rest <- varIntWord32+             return $ (rest `shiftL` 7) .|. (fromIntegral $ byte .&. 0x7f)+     else return $ fromIntegral byte++-- | This puts a generic Integral type, but first it casts to a Word32 because+--   it makes a difference with negative numbers+putVarInt32 :: (Integral i) => i -> P.Put+putVarInt32 = putVarWord32 . fromIntegral++putVarWord32 :: Word32 -> P.Put+putVarWord32 value+  | value < 128 = P.putWord8 $ fromIntegral value+  | otherwise = do+      P.putWord8 $ 0x80 .|. (fromIntegral $ value .&. 0x7f)+      putVarWord32 $ value `shiftR` 7
+ Network/MiniHTTP/URL.hs view
@@ -0,0 +1,387 @@+{-# LANGUAGE OverloadedStrings, ForeignFunctionInterface #-}+-- | This module contains a structure for representing web URLs. We don't try+--   to be a fully general URI parser (so no @mailto:@ etc), but it's a lot+--   better than "Network.URI" for dealing with HTTP(S)+module Network.MiniHTTP.URL+  ( URL(..)+  , RelativeURL(..)+  , Scheme(..)+  , Host(..)+  , toRelative+  , parse+  , parseRelative+  , parseArguments+  , serialise+  , serialiseRelative+  , serialiseArguments+  ) where++import           Control.Applicative(Alternative(..))+import           Control.Exception (handle)+import           Control.Monad (when, liftM)++import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import           Data.ByteString.Char8 ()+import           Data.ByteString.Internal (c2w, w2c)+import qualified Data.Binary.Put as P+import qualified Data.Binary.Strict.Class as C+import qualified Data.Binary.Strict.Get as G+import qualified Data.Binary.Strict.ByteSet as BSet+import           Data.Char (toLower)+import           Data.List (intersperse)+import qualified Data.Map as Map+import           Data.Maybe (fromMaybe, isJust, catMaybes)+import           Data.String (fromString)++import           Foreign+import           Foreign.C.Types++import           System.IO.Unsafe (unsafePerformIO)++import           Text.Printf (printf)++import qualified Network.Socket as S+++-- | A web URL+data URL = URL { urlScheme :: Scheme+               , urlUser :: Maybe B.ByteString+               , urlPassword :: Maybe B.ByteString+               , urlHost :: Host+               , urlPort :: Int  -- ^ defaults based on the scheme+               , urlPath :: B.ByteString  -- ^ does not include leading '/'+               , urlArguments :: Map.Map B.ByteString B.ByteString+               , urlFragment :: Maybe B.ByteString  -- ^ doesn't include '#'+               } deriving (Eq)++instance Show URL where+  show = map w2c . B.unpack . serialise++-- | This is a relative URL. It just copies (and renames) the last three+--   members of URL. However, it's good to keep these two different types of+--   URL apart in the type system.+data RelativeURL = RelativeURL { rurlPath :: B.ByteString  -- ^ does not include leading '/'+                               , rurlArguments :: Map.Map B.ByteString B.ByteString+                               , rurlFragment :: Maybe B.ByteString+                               } deriving (Show, Eq)++-- | The transport layer to be used+data Scheme = HTTP | HTTPS deriving (Show, Eq)++-- | The host where the resource can be found+data Host = IPv4Literal S.HostAddress+          | IPv6Literal S.HostAddress6+          | Hostname B.ByteString+          deriving (Show, Eq)++wrapGet :: G.Get a -> B.ByteString -> Maybe a+wrapGet f bs =+  case G.runGet f bs of+       (Left _, _) -> Nothing+       (Right u, _) -> Just u++-- | Parse a URL+parse :: B.ByteString -> Maybe URL+parse = wrapGet parseURL++-- | Parse just an arguments map. Can be useful for POST requests. Warning: it+--   occurs to be that the arguments in a POST request might include unescaped+--   '#' symbols. In a URL that would be illegal, but the parser may need to be+--   reworked for that.+parseArguments :: B.ByteString -> Maybe (Map.Map B.ByteString B.ByteString)+parseArguments = fmap Map.fromList . wrapGet parseKVs++-- | Parse a relative URL+parseRelative :: B.ByteString -> Maybe RelativeURL+parseRelative = wrapGet parseRelativeURL++-- | Extract a relative URL from a URL+toRelative :: URL -> RelativeURL+toRelative (URL { urlPath = path, urlArguments = args, urlFragment = frag }) =+  RelativeURL path args frag++foreign import ccall unsafe "inet_ntop"+  inet_ntop :: CInt           -> Ptr Word32 -> Ptr CChar -> CSize -> IO ()++foreign import ccall unsafe "htonl"+  htonl :: Word32 -> IO Word32++serialiseIPv4 :: S.HostAddress -> B.ByteString+serialiseIPv4 v4 = unsafePerformIO $ do+  alloca $ \ptr -> do+  poke ptr v4+  allocaBytes 17 $ \str -> do+  inet_ntop (S.packFamily S.AF_INET) ptr str 17+  B.packCString str++serialiseIPv6 :: S.HostAddress6 -> B.ByteString+serialiseIPv6 (a, b, c, d) = unsafePerformIO $ do+  allocaArray 4 $ \ptr -> do+  mapM htonl [a,b,c,d] >>= pokeArray ptr+  allocaBytes 47 $ \str -> do+  inet_ntop (S.packFamily S.AF_INET6) ptr str 47+  B.packCString str++defaultPort :: Scheme -> Int+defaultPort HTTP = 80+defaultPort HTTPS = 443++doPut :: (a -> P.Put) -> a -> B.ByteString+doPut p = B.concat . BL.toChunks . P.runPut . p++-- | Convert a URL to a ByteString. It's the same as "show", except for the+--   type of the return.+serialise :: URL -> B.ByteString+serialise = doPut putURL++serialiseRelative :: RelativeURL -> B.ByteString+serialiseRelative = doPut putRelativeURL++-- | Serialise just an arguments map. Can be useful for POST requests.+serialiseArguments :: Map.Map B.ByteString B.ByteString -> B.ByteString+serialiseArguments = doPut putArguments++putURL :: URL -> P.Put+putURL url = do+  case urlScheme url of+       HTTP -> P.putByteString "http"+       HTTPS -> P.putByteString "https"+  P.putByteString "://"++  maybe (return ()) P.putByteString $ urlUser url+  maybe (return ()) (\x -> P.putWord8 (c2w ':') >> P.putByteString x) $ urlPassword url+  when (isJust (urlUser url) || isJust (urlPassword url)) $ P.putWord8 $ c2w '@'++  case urlHost url of+       IPv4Literal v4 -> P.putByteString $ serialiseIPv4 v4+       IPv6Literal v6 -> do+         P.putWord8 $ c2w '['+         P.putByteString $ serialiseIPv6 v6+         P.putWord8 $ c2w ']'+       Hostname h -> P.putByteString h++  when (urlPort url /= defaultPort (urlScheme url)) $ do+    P.putWord8 $ c2w ':'+    P.putByteString $ fromString $ show $ urlPort url++  putRelativeURL $ toRelative url++putRelativeURL :: RelativeURL -> P.Put+putRelativeURL rurl = do+  P.putWord8 $ c2w '/'+  P.putByteString $ encodeString pathSafeChars $ rurlPath rurl+  when (not $ Map.null $ rurlArguments rurl) $ do+    P.putWord8 $ c2w '?'+    putArguments $ rurlArguments rurl+    maybe (return ()) P.putByteString $ rurlFragment rurl++putArguments :: Map.Map B.ByteString B.ByteString -> P.Put+putArguments args = do+  let f ("", "") = Nothing+      f (k, "") = Just $ P.putByteString $ encodeString safeChars k+      f (k, v) = Just $ do+        P.putByteString $ encodeString safeChars k+        P.putWord8 $ c2w '='+        P.putByteString $ encodeString safeChars v+  let vs = flip map (Map.toList args) f+  sequence_ $ intersperse (P.putWord8 $ c2w '&') $ catMaybes vs++digits :: BSet.ByteSet+digits = BSet.range (c2w '0') (c2w '9')++hexChars :: BSet.ByteSet+hexChars = BSet.fromList $ map c2w "0123456789abcdefABCDEF"++isHexChar :: Word8 -> Bool+isHexChar = BSet.member hexChars++-- | The list of unsafe charactors in a URL, from RFC 1738+unsafeChars :: BSet.ByteSet+unsafeChars = BSet.range 0 0x1f `BSet.union`+              BSet.range 0x7f 0xff `BSet.union`+              BSet.fromList (map c2w "$&+,/:;=?@ \"<>#%{}[]|\\^~`")++-- | Bytes which aren't unsafe, are safe.+safeChars :: BSet.ByteSet+safeChars = BSet.complement unsafeChars++-- | These are the charactors which are safe in a path+pathSafeChars :: BSet.ByteSet+pathSafeChars = safeChars `BSet.union` BSet.singleton (c2w '/')++-- | Parse a single key, value pair+parseKV :: G.Get (B.ByteString, B.ByteString)+parseKV = do+  key <- C.spanOf (\x -> x /= c2w '#' && x /= c2w '=' && x /= c2w '&')+  value <- C.optional $ do+    C.word8 $ c2w '='+    C.spanOf (\x -> x /= c2w '#' && x /= c2w '&')++  return (decodeString key, decodeString $ fromMaybe "" value)++-- | Parse a set of URL encoded key, value pairs.+parseKVs :: G.Get [(B.ByteString, B.ByteString)]+parseKVs = do+  first <- parseKV+  rest <- C.many $ (C.word8 $ c2w '&') >> parseKV++  return $ first : rest++parseIPv6 :: G.Get B.ByteString+parseIPv6 = do+  C.word8 $ c2w '['+  s <- C.spanOf1 (/= c2w ']')+  C.word8 $ c2w ']'++  return s++toString :: B.ByteString -> String+toString = map w2c . B.unpack++parseRelativeURL :: G.Get RelativeURL+parseRelativeURL = do+  emptyp <- C.isEmpty+  if emptyp+     then return $ RelativeURL "" Map.empty Nothing+     else do+       C.word8 $ c2w '/'+       path <- C.spanOf (\x -> x /= c2w '?' && x /= c2w '#')+       margs <- C.optional $ do+         C.word8 $ c2w '?'+         liftM Map.fromList parseKVs+       mfrag <- C.optional $ do+         C.word8 $ c2w '#'+         rem <- C.remaining+         C.getByteString rem++       emptyp <- C.isEmpty+       when (not emptyp) $ fail "Trailing garbage"++       return $ RelativeURL path (fromMaybe Map.empty margs) mfrag++parseURL :: G.Get URL+parseURL = do+  scheme' <- C.spanOf1 (/= c2w ':')+  scheme <- case map (toLower . w2c) $ B.unpack scheme' of+                 "http" -> return HTTP+                 "https" -> return HTTPS+                 _ -> fail "Unknown scheme"+  C.string "://"++  muserpw <- C.optional $ do+    user <- C.spanOf (\x -> x /= c2w '@' && x /= c2w ':')+    pw <- C.optional $ do+      C.word8 $ c2w ':'+      C.spanOf (/= c2w '@')+    C.word8 $ c2w '@'+    return (user, pw)++  host' <- parseIPv6 <|> C.spanOf1 (\x -> x /= c2w ':' && x /= c2w '/')++  mhost <- return $ unsafePerformIO $ handle (const $ return Nothing) $ do+    ai <- S.getAddrInfo (Just (S.defaultHints { S.addrFlags = [S.AI_NUMERICHOST] }))+                        (Just $ toString host') Nothing+    case ai of+         [] -> return Nothing+         ai:_ ->+           case S.addrFamily ai of+                S.AF_INET ->+                  case S.addrAddress ai of+                       (S.SockAddrInet _ host) -> return $ Just $ IPv4Literal host+                       _ -> return Nothing+                S.AF_INET6 ->+                  case S.addrAddress ai of+                       (S.SockAddrInet6 _ _ host _) -> return $ Just $ IPv6Literal host+                       _ -> return Nothing+                _ -> return Nothing++  mport <- C.optional $ do+    C.word8 $ c2w ':'+    s <- C.spanOf1 $ BSet.member digits+    case reads $ map w2c $ B.unpack s of+         [(x, "")] ->+           if x > 0 && x < 65536+              then return x+              else fail "Port number out of range"+         _ -> fail "Invalid port number"++  rurl <- parseRelativeURL++  let url = URL { urlScheme = scheme+                , urlHost = case mhost of+                                 Just h -> h+                                 Nothing -> Hostname host'+                , urlPort = case mport of+                                 Just p -> p+                                 Nothing -> defaultPort scheme+                , urlPath = rurlPath rurl+                , urlArguments = rurlArguments rurl+                , urlFragment = rurlFragment rurl+                , urlUser = Nothing+                , urlPassword = Nothing }+      url'' = case muserpw of+                   Just (user, mpw) -> url { urlUser = Just user+                                           , urlPassword = mpw }+                   Nothing -> url++  return url''++toHexNibble :: Word8 -> Word8+toHexNibble 0x30 = 0+toHexNibble 0x31 = 1+toHexNibble 0x32 = 2+toHexNibble 0x33 = 3+toHexNibble 0x34 = 4+toHexNibble 0x35 = 5+toHexNibble 0x36 = 6+toHexNibble 0x37 = 7+toHexNibble 0x38 = 8+toHexNibble 0x39 = 9+toHexNibble 0x41 = 10+toHexNibble 0x42 = 11+toHexNibble 0x43 = 12+toHexNibble 0x44 = 13+toHexNibble 0x45 = 14+toHexNibble 0x46 = 15+toHexNibble 0x61 = 10+toHexNibble 0x62 = 11+toHexNibble 0x63 = 12+toHexNibble 0x64 = 13+toHexNibble 0x65 = 14+toHexNibble 0x66 = 15+toHexNibble _ = error "toHexNibble passed non-hex char"++toHexByte :: B.ByteString -> Word8+toHexByte bs = (toHexNibble (bs `B.index` 0) `shiftL` 4) .|.+               (toHexNibble (bs `B.index` 1))++-- | Replace any % escaped bytes in the given string with their Word8 values.+decodePercents :: B.ByteString -> B.ByteString+decodePercents bs = f (left, right) where+  (left, right) = B.span (/= c2w '%') bs+  f (left, right)+    | B.null right = left+    | B.length right >= 3 &&+      isHexChar (right `B.index` 1) &&+      isHexChar (right `B.index` 2) =+        left `B.append` B.singleton (toHexByte $ B.tail $ B.take 3 right) `B.append` decodeString (B.drop 3 right)+    | otherwise = bs++-- | Replace pluses in the given string with spaces and then perform percent+--   decoding.+decodeString :: B.ByteString -> B.ByteString+decodeString = decodePercents . B.map f where+  f 0x2b = 0x20+  f x = x++-- | Percent encode any unsafe bytes in the given string+encodeString :: BSet.ByteSet -> B.ByteString -> B.ByteString+encodeString safeChars bs = f (left, right) where+  (left, right) = B.span (BSet.member safeChars) bs+  f (left, right)+    | B.null right = left+    | otherwise = left `B.append` escaped `B.append` encodeString safeChars right' where+        right' = B.tail right+        unsafe = B.head right+        escaped = B.pack $ map c2w $ printf "%%%X" ((fromIntegral unsafe) :: Int)
+ examples/css/ie.css view
@@ -0,0 +1,22 @@+/* -----------------------------------------------------------------------++   Blueprint CSS Framework 0.7.1+   http://blueprintcss.googlecode.com++   * Copyright (c) 2007-2008. See LICENSE for more info.+   * See README for instructions on how to use Blueprint.+   * For credits and origins, see AUTHORS.+   * This is a compressed file. See the sources in the 'src' directory.++----------------------------------------------------------------------- */++/* ie.css */+body {text-align:center;}+.container {text-align:left;}+* html .column {overflow-x:hidden;}+* html legend {margin:-18px -8px 16px 0;padding:0;}+ol {margin-left:2em;}+sup {vertical-align:text-top;}+sub {vertical-align:text-bottom;}+html>body p code {*white-space:normal;}+hr {margin:-8px auto 11px;}
+ examples/css/print.css view
@@ -0,0 +1,29 @@+/* -----------------------------------------------------------------------++   Blueprint CSS Framework 0.7.1+   http://blueprintcss.googlecode.com++   * Copyright (c) 2007-2008. See LICENSE for more info.+   * See README for instructions on how to use Blueprint.+   * For credits and origins, see AUTHORS.+   * This is a compressed file. See the sources in the 'src' directory.++----------------------------------------------------------------------- */++/* print.css */+body {line-height:1.5;font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;color:#000;background:none;font-size:10pt;}+.container {background:none;}+hr {background:#ccc;color:#ccc;width:100%;height:2px;margin:2em 0;padding:0;border:none;}+hr.space {background:#fff;color:#fff;}+h1, h2, h3, h4, h5, h6 {font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;}+code {font:.9em "Courier New", Monaco, Courier, monospace;}+img {float:left;margin:1.5em 1.5em 1.5em 0;}+a img {border:none;}+p img.top {margin-top:0;}+blockquote {margin:1.5em;padding:1em;font-style:italic;font-size:.9em;}+.small {font-size:.9em;}+.large {font-size:1.1em;}+.quiet {color:#999;}+.hide {display:none;}+a:link, a:visited {background:transparent;font-weight:700;text-decoration:underline;}+a:link:after, a:visited:after {content:" (" attr(href) ") ";font-size:90%;}
+ examples/css/screen.css view
@@ -0,0 +1,226 @@+/* -----------------------------------------------------------------------++   Blueprint CSS Framework 0.7.1+   http://blueprintcss.googlecode.com++   * Copyright (c) 2007-2008. See LICENSE for more info.+   * See README for instructions on how to use Blueprint.+   * For credits and origins, see AUTHORS.+   * This is a compressed file. See the sources in the 'src' directory.++----------------------------------------------------------------------- */++/* reset.css */+html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}+body {line-height:1.5;}+table {border-collapse:separate;border-spacing:0;}+caption, th, td {text-align:left;font-weight:normal;}+table, td, th {vertical-align:middle;}+blockquote:before, blockquote:after, q:before, q:after {content:"";}+blockquote, q {quotes:"" "";}+a img {border:none;}++/* typography.css */+body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Helvetica, Arial, sans-serif;}+h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}+h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}+h2 {font-size:2em;margin-bottom:0.75em;}+h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}+h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;height:1.25em;}+h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}+h6 {font-size:1em;font-weight:bold;}+h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}+p {margin:0 0 1.5em;}+p img {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}+p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}+a:focus, a:hover {color:#000;}+a {color:#009;text-decoration:underline;}+blockquote {margin:1.5em;color:#666;font-style:italic;}+strong {font-weight:bold;}+em, dfn {font-style:italic;}+dfn {font-weight:bold;}+sup, sub {line-height:0;}+abbr, acronym {border-bottom:1px dotted #666;}+address {margin:0 0 1.5em;font-style:italic;}+del {color:#666;}+pre, code {margin:1.5em 0;white-space:pre;}+pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}+li ul, li ol {margin:0 1.5em;}+ul, ol {margin:0 1.5em 1.5em 1.5em;}+ul {list-style-type:disc;}+ol {list-style-type:decimal;}+dl {margin:0 0 1.5em 0;}+dl dt {font-weight:bold;}+dd {margin-left:1.5em;}+table {margin-bottom:1.4em;width:100%;}+th {font-weight:bold;background:#C3D9FF;}+th, td {padding:4px 10px 4px 5px;}+tr.even td {background:#E5ECF9;}+tfoot {font-style:italic;}+caption {background:#eee;}+.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}+.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}+.hide {display:none;}+.quiet {color:#666;}+.loud {color:#000;}+.highlight {background:#ff0;}+.added {background:#060;color:#fff;}+.removed {background:#900;color:#fff;}+.first {margin-left:0;padding-left:0;}+.last {margin-right:0;padding-right:0;}+.top {margin-top:0;padding-top:0;}+.bottom {margin-bottom:0;padding-bottom:0;}++/* grid.css */+.container {width:950px;margin:0 auto;}+.showgrid {background:url(src/grid.png);}+body {margin:1.5em 0;}+div.span-1, div.span-2, div.span-3, div.span-4, div.span-5, div.span-6, div.span-7, div.span-8, div.span-9, div.span-10, div.span-11, div.span-12, div.span-13, div.span-14, div.span-15, div.span-16, div.span-17, div.span-18, div.span-19, div.span-20, div.span-21, div.span-22, div.span-23, div.span-24 {float:left;margin-right:10px;}+div.last {margin-right:0;}+.span-1 {width:30px;}+.span-2 {width:70px;}+.span-3 {width:110px;}+.span-4 {width:150px;}+.span-5 {width:190px;}+.span-6 {width:230px;}+.span-7 {width:270px;}+.span-8 {width:310px;}+.span-9 {width:350px;}+.span-10 {width:390px;}+.span-11 {width:430px;}+.span-12 {width:470px;}+.span-13 {width:510px;}+.span-14 {width:550px;}+.span-15 {width:590px;}+.span-16 {width:630px;}+.span-17 {width:670px;}+.span-18 {width:710px;}+.span-19 {width:750px;}+.span-20 {width:790px;}+.span-21 {width:830px;}+.span-22 {width:870px;}+.span-23 {width:910px;}+.span-24, div.span-24 {width:950px;margin:0;}+.append-1 {padding-right:40px;}+.append-2 {padding-right:80px;}+.append-3 {padding-right:120px;}+.append-4 {padding-right:160px;}+.append-5 {padding-right:200px;}+.append-6 {padding-right:240px;}+.append-7 {padding-right:280px;}+.append-8 {padding-right:320px;}+.append-9 {padding-right:360px;}+.append-10 {padding-right:400px;}+.append-11 {padding-right:440px;}+.append-12 {padding-right:480px;}+.append-13 {padding-right:520px;}+.append-14 {padding-right:560px;}+.append-15 {padding-right:600px;}+.append-16 {padding-right:640px;}+.append-17 {padding-right:680px;}+.append-18 {padding-right:720px;}+.append-19 {padding-right:760px;}+.append-20 {padding-right:800px;}+.append-21 {padding-right:840px;}+.append-22 {padding-right:880px;}+.append-23 {padding-right:920px;}+.prepend-1 {padding-left:40px;}+.prepend-2 {padding-left:80px;}+.prepend-3 {padding-left:120px;}+.prepend-4 {padding-left:160px;}+.prepend-5 {padding-left:200px;}+.prepend-6 {padding-left:240px;}+.prepend-7 {padding-left:280px;}+.prepend-8 {padding-left:320px;}+.prepend-9 {padding-left:360px;}+.prepend-10 {padding-left:400px;}+.prepend-11 {padding-left:440px;}+.prepend-12 {padding-left:480px;}+.prepend-13 {padding-left:520px;}+.prepend-14 {padding-left:560px;}+.prepend-15 {padding-left:600px;}+.prepend-16 {padding-left:640px;}+.prepend-17 {padding-left:680px;}+.prepend-18 {padding-left:720px;}+.prepend-19 {padding-left:760px;}+.prepend-20 {padding-left:800px;}+.prepend-21 {padding-left:840px;}+.prepend-22 {padding-left:880px;}+.prepend-23 {padding-left:920px;}+div.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;}+div.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;}+.pull-1 {margin-left:-40px;}+.pull-2 {margin-left:-80px;}+.pull-3 {margin-left:-120px;}+.pull-4 {margin-left:-160px;}+.pull-5 {margin-left:-200px;}+.pull-6 {margin-left:-240px;}+.pull-7 {margin-left:-280px;}+.pull-8 {margin-left:-320px;}+.pull-9 {margin-left:-360px;}+.pull-10 {margin-left:-400px;}+.pull-11 {margin-left:-440px;}+.pull-12 {margin-left:-480px;}+.pull-13 {margin-left:-520px;}+.pull-14 {margin-left:-560px;}+.pull-15 {margin-left:-600px;}+.pull-16 {margin-left:-640px;}+.pull-17 {margin-left:-680px;}+.pull-18 {margin-left:-720px;}+.pull-19 {margin-left:-760px;}+.pull-20 {margin-left:-800px;}+.pull-21 {margin-left:-840px;}+.pull-22 {margin-left:-880px;}+.pull-23 {margin-left:-920px;}+.pull-24 {margin-left:-960px;}+.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;}+.push-1 {margin:0 -40px 1.5em 40px;}+.push-2 {margin:0 -80px 1.5em 80px;}+.push-3 {margin:0 -120px 1.5em 120px;}+.push-4 {margin:0 -160px 1.5em 160px;}+.push-5 {margin:0 -200px 1.5em 200px;}+.push-6 {margin:0 -240px 1.5em 240px;}+.push-7 {margin:0 -280px 1.5em 280px;}+.push-8 {margin:0 -320px 1.5em 320px;}+.push-9 {margin:0 -360px 1.5em 360px;}+.push-10 {margin:0 -400px 1.5em 400px;}+.push-11 {margin:0 -440px 1.5em 440px;}+.push-12 {margin:0 -480px 1.5em 480px;}+.push-13 {margin:0 -520px 1.5em 520px;}+.push-14 {margin:0 -560px 1.5em 560px;}+.push-15 {margin:0 -600px 1.5em 600px;}+.push-16 {margin:0 -640px 1.5em 640px;}+.push-17 {margin:0 -680px 1.5em 680px;}+.push-18 {margin:0 -720px 1.5em 720px;}+.push-19 {margin:0 -760px 1.5em 760px;}+.push-20 {margin:0 -800px 1.5em 800px;}+.push-21 {margin:0 -840px 1.5em 840px;}+.push-22 {margin:0 -880px 1.5em 880px;}+.push-23 {margin:0 -920px 1.5em 920px;}+.push-24 {margin:0 -960px 1.5em 960px;}+.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;}+.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}+hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}+hr.space {background:#fff;color:#fff;}+.clearfix:after, .container:after {content:".";display:block;height:0;clear:both;visibility:hidden;}+.clearfix, .container {display:inline-block;}+* html .clearfix, * html .container {height:1%;}+.clearfix, .container {display:block;}+.clear {clear:both;}++/* forms.css */+label {font-weight:bold;}+fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}+legend {font-weight:bold;font-size:1.2em;}+input.text, input.title, textarea, select {margin:0.5em 0;border:1px solid #bbb;}+input.text:focus, input.title:focus, textarea:focus, select:focus {border:1px solid #666;}+input.text, input.title {width:300px;padding:5px;}+input.title {font-size:1.5em;}+textarea {width:390px;height:250px;padding:5px;}+.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd;}+.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}+.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}+.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}+.error a {color:#8a1f11;}+.notice a {color:#514721;}+.success a {color:#264409;}
+ examples/example.tmpl view
@@ -0,0 +1,116 @@+<html>+  <head>+   <link rel="stylesheet" href="screen.css" type="text/css" media="screen, projection">+   <link rel="stylesheet" href="print.css" type="text/css" media="print">+   <!--[if IE]><link rel="stylesheet" href="ie.css" type="text/css" media="screen, projection"><![endif]-->++   <style type="text/css">+     #header {+       background-color:  #2A3B63;+       border-bottom: 4px solid #000014;+       margin-bottom: 2em;+     }++     #header h1 {+       padding-top: 0.4em;+       padding-left: 0.3em;+       font-family: "Helvetica";+       color:   #ddd;+     }++     body {+       background-color:  #717C8C;+     }++     #main {+       background-color:  #FFF7CB;+       font-family: "Ariel";+     }++     #body {+       margin-left: 0.3em;+     }++     #body pre {+       margin-left: 1em;+       color: #555;+     }++     #body form {+       margin-bottom: 1em;+     }++     #body p {+       font-size: 1.05em;+     }+   </style>+  </head>+  <body>+    <div class="container" id="main">+      <div class="span-24" id="header">+        <h1><tt>Network.MiniHTTP</tt> Test Page</h1>+      </div>++      <div class="span-24" id="body">+        <h3>Session support</h3>++        <p>Sessions, at the moment, are serialised, encrypted, signed and+        stored in cookies on the user's browser. In <tt>MiniHTTP</tt> they are+        availible as a Map with bytestrings as the keys and values.</p>++        <p>Here's the current value of your session:</p>+        <pre>{{SESSION:h}}</pre>++        <p>You can set a value using the following form:</p>++        <form method="POST" action="/setsession">+          <input name="name" value="" type="text"/>+          <input name="submit" value="Set name" type="submit"/>+        </form>++        <h3>Templating</h3>++        <p>Templating is up to the user, nothing is built into+        <tt>MiniHTTP</tt>, however this code is using <a+          href="http://hackage.haskell.org/cgi-bin/hackage-scripts/package/ctemplate-0.1"><tt>ctemplate</tt></a>,+        which is Google's internal templating system. It's pretty basic+        compared to other templating systems (which the reader may take to be+        a virtue or a deficiency), but it does escaping very well.</p>++        <p>The wrapper still needs a little efficiency work, but is currently+        functional (as you can see). Due to ctemplate being a C++ library, you+        appear to need to compile anything which uses it; <tt>ghci</tt> will+        fail in glibc.</p>++        <h3>OpenID</h3>++        <p>My initial test of the code was to write an <a+          href="http://www.openid.net">OpenID</a> consumer. This+        involves both server and client HTTP code. It's currently fairly+        functional, though incomplete. I only have an account with+        <a href="http://www.myopenid.com">MyOpenID.com</a>, so I've not tested it with anything else.</p>++        <p><i>For the moment, the input must be a valid URL. So don't forget+        the <tt>http://</tt> at the beginning.</i></p>++        <p>Once you have logged in, you should see your OpenID in the session+        output, above.</p>++        <form method="POST" action="/openidlogin">+          <input name="id" value="" type="text"/>+          <input name="submit" value="Login via OpenID" type="submit"/>+        </form>++        <h3>HTTPS support</h3>++        <p>Running an HTTPS server is as easy as running an HTTP server (as+        long as you have the certificate and private key in the correct+        format). However, it's been known to tickle a bug which causes the RTS+        to fall into an infinite loop.</p>++        <p>HTTPS client support is comming.</p>++      </div>+    </div>+  </body>+</html>
− examples/fileserver.hs
@@ -1,20 +0,0 @@--- | This is a very simple fileserver HTTP server which serves from the current---   directory. It will handle If-Modified-Since and Range requests etc. It---   doesn't do directory indexes yet, so GETting '/' will give a 404-module Main where--import Network.MiniHTTP.Server--main = serve 4112 $ do-  handleFromFilesystem "."-  handleConditionalRequest-  handleRangeRequests-  handleDecoration---- If you wanted to process some URLs differently, you would do something like:---   req <- getRequest---   if iWantToHandle $ reqUrl req---      then xyx---      else handleFromFilesystem "."---   handleConditionalRequest---   ...
+ examples/test.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Control.Monad.State.Strict++import qualified Data.ByteString as B+import Data.ByteString.Char8 ()+import qualified Data.Map as Map++import Network.MiniHTTP.Server+import Network.MiniHTTP.Marshal+import Network.MiniHTTP.HTTPConnection+import Network.MiniHTTP.Session+import qualified Network.MiniHTTP.OpenID as OpenID+import qualified Network.MiniHTTP.Session as Session+import qualified Network.MiniHTTP.URL as URL+import qualified Text.CTemplate as T++import qualified OpenSSL as SSL++handleFromTemplate :: T.Dictionary -> WebMonad ()+handleFromTemplate tmpl = do+  Just r <- lift $ T.expand T.DontStrip tmpl+  source <- lift $ bsSource r+  setReply 200+  s <- get+  put $ s { wsSource = Just source }+  setHeader $ \h -> h { httpContentLength = Just $ fromIntegral $ B.length r }++index :: WebMonad ()+index = do+  session <- getSession+  handleFromTemplate $ T.Dictionary "example.tmpl" [("SESSION", T.StringV $ show session)]+  setHeader $ \h -> h { httpOtherHeaders = Map.fromList [("Cache-Control", "private")] }++setSession :: WebMonad ()+setSession = do+  postValues <- getPOST 1024+  setReply 301+  case Map.lookup "name" postValues of+       Nothing -> return ()+       Just name -> addSession "name" name+  setHeader $ \h -> h { httpLocation = Just "/" }++openIDLogin :: WebMonad ()+openIDLogin = do+  postValues <- getPOST 1024+  id <- Map.lookup "id" postValues+  let Just uri = URL.parse id+  discovery <- liftIO $ OpenID.discover uri+  (handle, _) <- liftIO $ OpenID.associate discovery+  setReply 302+  let url = OpenID.checkID OpenID.CheckIDSetup uri discovery handle "http://localhost.com/openidreturn" Nothing+  setHeader (\h -> h { httpLocation = Just $ URL.serialise url })++openIDReturn :: WebMonad ()+openIDReturn = do+  args <- getGET+  r <- liftIO $ OpenID.processCheckIDReply args+  case r of+       Left error -> errorPage error+       Right id -> do+         setReply 301+         addSession "openid" id+         setHeader $ \h -> h { httpLocation = Just "/" }++handleFile :: WebMonad ()+handleFile = do+  handleFromFilesystem "css/"+  handleConditionalRequest+  handleRangeRequests+  handleHandleToSource+  handleDecoration++main' :: IO ()+main' = serveHTTP 4112 $ dispatchOnURL+  [ (Exact "", index), (Exact "setsession", setSession)+  , (Exact "openidlogin", openIDLogin), (Exact "openidreturn", openIDReturn)+  , (Prefix "", handleFile)]++main :: IO ()+main = SSL.withOpenSSL main'
+ examples/webcat.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import qualified Data.ByteString.Lazy as BL+import           Data.String (fromString)++import qualified Network.Connection as C+import qualified Network.MiniHTTP.Client as Client+import qualified Network.MiniHTTP.URL as URL+import           Network.MiniHTTP.Marshal (emptyHeaders)+import           Network.MiniHTTP.HTTPConnection (sourceToLBS)++import           OpenSSL (withOpenSSL)++import System.Environment (getArgs)++main = withOpenSSL main'++main' = do+  args <- getArgs+  case args of+       [urlstring] -> do+         case URL.parse $ fromString urlstring of+              Nothing -> fail "Cannot parse URL"+              Just url -> do+                (conn, reply, msource) <- Client.fetchBasic emptyHeaders url+                print reply+                case msource of+                     Nothing -> C.close conn+                     Just source -> do+                       lbs <- sourceToLBS source+                       BL.putStr lbs+                       C.close conn+       _ -> fail "Please give a URL as the only argument"
network-minihttp.cabal view
@@ -1,18 +1,29 @@ name:            network-minihttp-version:         0.1+version:         0.2 license:         BSD3 license-file:    LICENSE author:          Adam Langley <agl@imperialviolet.org>-description:     A very minimal webserver-synopsis:        A very minimal webserver+description:     A ByteString based HTTP(S) library+synopsis:        A ByteString based library for writing HTTP(S) servers and clients.+homepage:        http://darcs.imperialviolet.org/network-minihttp category:        Network-build-depends:   base, containers, bytestring>=0.9, network-bytestring>=0.1.1.2, network>=2.1, stm>=2.1, binary>=0.4, binary-strict>=0.3, filepath>=1.1.0.0, mtl>=1.1.0.0, unix>=2.3.0.0, time>=1.1.2.0, old-locale>=1.0.0.0+build-depends:   base, containers, bytestring>=0.9, network-bytestring>=0.1.1.2, network>=2.1, stm>=2.1, binary>=0.4, binary-strict>=0.3, filepath>=1.1.0.0, mtl>=1.1.0.0, unix>=2.3.0.0, time>=1.1.2.0, old-locale>=1.0.0.0, HsOpenSSL>=0.4.1, network-connection>=0.1, network-dns>=0.1.2, tagsoup>=0.5 stability:       provisional tested-with:     GHC == 6.8.2 exposed-modules: Network.MiniHTTP.Server,-                 Network.MiniHTTP.Marshal-                 Network.MiniHTTP.MimeTypesParse-other-modules:   Network.MiniHTTP.Connection-extra-source-files: examples/fileserver.hs+                 Network.MiniHTTP.Marshal,+                 Network.MiniHTTP.Client,+                 Network.MiniHTTP.HTTPConnection,+                 Network.MiniHTTP.OpenID,+                 Network.MiniHTTP.Session,+                 Network.MiniHTTP.URL+other-modules:   Network.MiniHTTP.MimeTypesParse+extra-source-files: examples/test.hs,+                    examples/example.tmpl,+                    examples/css/screen.css,+                    examples/css/print.css,+                    examples/css/ie.css+                    examples/webcat.hs ghc-options:     -Wall -fno-warn-name-shadowing extensions:      OverloadedStrings+build-type: Simple