packages feed

gemini-server 0.1.0.0 → 0.2.0.0

raw patch · 3 files changed

+98/−37 lines, 3 filesdep +HsOpenSSLPVP ok

version bump matches the API change (PVP)

Dependencies added: HsOpenSSL

API changes (from Hackage documentation)

- Network.Gemini.Server: type Request = URI
+ Network.Gemini.Server: Request :: URI -> Maybe X509 -> Request
+ Network.Gemini.Server: [requestCert] :: Request -> Maybe X509
+ Network.Gemini.Server: [requestURI] :: Request -> URI
+ Network.Gemini.Server: data Request
- Network.Gemini.Server: runServer :: Maybe HostName -> ServiceName -> (Request -> IO Response) -> IO ()
+ Network.Gemini.Server: runServer :: Maybe HostName -> ServiceName -> FilePath -> (Request -> IO Response) -> IO ()

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for gemini-server +## 0.2.0.0 -- 2021-02-22++* SSL/TLS support, including client certificates.+ ## 0.1.0.0 -- 2020-07-24  * First version. Released on an unsuspecting world.
Network/Gemini/Server.hs view
@@ -1,5 +1,5 @@ module Network.Gemini.Server (-  Request+  Request(..) , Response(..) , Handler , runServer@@ -8,30 +8,46 @@ , redirect ) where -import Network.Socket (HostName, ServiceName, SockAddr, getPeerName)-import Network.Socket.ByteString.Lazy (recv, sendAll)+import Network.Socket (Socket, HostName, ServiceName, SockAddr, getPeerName)+import OpenSSL (withOpenSSL)+import qualified OpenSSL.Session as SSL+import OpenSSL.Session (SSL)+import OpenSSL.X509 (X509) import Network.Run.TCP (runTCPServer) import Network.URI (URI(URI), parseURI, uriToString)  import qualified Data.ByteString.Lazy as LBS-import Data.ByteString.Lazy.UTF8 (toString)+import Data.ByteString.UTF8 (toString)  import Data.String (fromString) -import Control.Exception (SomeException, try)+import Control.Exception (SomeException, try, bracket, catch)  import System.Log.Logger   ( updateGlobalLogger, setLevel, logM, Priority(INFO, ERROR) ) + --MAYBE switch to a more modern/efficient uri library---TODO add client cert-type Request = URI +-- | A Gemini client's request+--+-- @since 0.2.0.0+data Request = Request+  { requestURI :: URI+  , requestCert :: Maybe X509 }++-- | A Gemini server's response+--+-- @since 0.1.0.0 data Response = Response   { responseStatus :: Int   , responseMeta :: String   , responseBody :: LBS.ByteString } +-- | A request handler specifies how the server should respond to the clients'+-- requests+--+-- @since 0.1.0.0 type Handler = Request -> IO Response  renderHeader :: Int -> String -> LBS.ByteString@@ -41,34 +57,69 @@   fromString meta <>   fromString "\CR\LF" -runServer :: Maybe HostName -> ServiceName -> (Request -> IO Response) -> IO ()-runServer host service handler = do+-- | Start a Gemini server.+--+-- @since 0.2.0.0+runServer :: Maybe HostName+          -> ServiceName+          -> FilePath -- ^ Path to the server certificate+          -> (Request -> IO Response) -- ^ Request handler+          -> IO ()+runServer host service cert handler = withOpenSSL $ do   updateGlobalLogger "Network.Gemini.Server" $ setLevel INFO-  runTCPServer host service talk -- MAYBE server config-  where-    talk s = do --TODO timeouts on send and receive (and maybe on handler)-      msg <- toString <$> recv s 1025 -- 1024 + CR or LF-      -- It makes sense to be very lenient here-      let mURI = parseURI $ takeWhile (not . (`elem` ['\CR', '\LF'])) msg-      peer <- getPeerName s-      case mURI of-        Nothing -> do-          logRequest INFO peer (Left msg) 59 Nothing-          sendAll s $ renderHeader 59 $ fromString "Invalid URL"-        Just uri@(URI "gemini:" _ _ _ _) -> do-          response <- try $ handler uri-          case response of-            Right (Response status meta body) -> do-              logRequest INFO peer (Right uri) status $ Just meta-              sendAll s $ renderHeader status meta-              sendAll s body-            Left e -> do-              logRequest ERROR peer (Right uri) 42 $ Just $ show (e :: SomeException)-              sendAll s $ renderHeader 42 $ fromString "Internal server error"-        Just uri@(URI scheme _ _ _ _) -> do-              logRequest INFO peer (Right uri) 59 Nothing-              sendAll s $ renderHeader 59 $ fromString $ "Invalid scheme: " <> scheme+  -- MAYBE server config+  sslCtx <- setupSSL cert+  runTCPServer host service $ \sock -> do+    peer <- getPeerName sock+    catch -- the ssl session may fail+      (bracket+        (acceptSSL sslCtx sock)+        (`SSL.shutdown` SSL.Unidirectional)+        (talk handler peer))+      (\e -> logM "Network.Gemini.Server" ERROR $ show (e :: SomeException)) +setupSSL :: FilePath -> IO SSL.SSLContext+setupSSL cert = do+  sslCtx <- SSL.context+  SSL.contextSetDefaultCiphers sslCtx+  SSL.contextSetCertificateFile sslCtx cert+  SSL.contextSetPrivateKeyFile sslCtx cert+  SSL.contextSetVerificationMode sslCtx $ SSL.VerifyPeer False True $+    -- Accept all certificates, since we don't really care about validity wrt CAs+    -- but only about the public key+    Just $ \_ _ -> pure True+  pure sslCtx++acceptSSL :: SSL.SSLContext -> Socket -> IO SSL+acceptSSL sslCtx sock = do+  ssl <- SSL.connection sslCtx sock+  SSL.accept ssl+  pure ssl++talk :: (Request -> IO Response) -> SockAddr -> SSL -> IO ()+talk handler peer s = do --TODO timeouts on send and receive (and maybe on handler)+  msg <- toString <$> SSL.read s 1025 -- 1024 + CR or LF+  -- It makes sense to be very lenient here+  let mURI = parseURI $ takeWhile (not . (`elem` ['\CR', '\LF'])) msg+  case mURI of+    Nothing -> do+      logRequest INFO peer (Left msg) 59 Nothing+      SSL.lazyWrite s $ renderHeader 59 $ fromString "Invalid URL"+    Just uri@(URI "gemini:" _ _ _ _) -> do+      clientCert <- SSL.getPeerCertificate s+      response <- try $ handler $ Request uri clientCert+      case response of+        Right (Response status meta body) -> do+          logRequest INFO peer (Right uri) status $ Just meta+          SSL.lazyWrite s $ renderHeader status meta+          SSL.lazyWrite s body+        Left e -> do+          logRequest ERROR peer (Right uri) 42 $ Just $ show (e :: SomeException)+          SSL.lazyWrite s $ renderHeader 42 $ fromString "Internal server error"+    Just uri@(URI scheme _ _ _ _) -> do+          logRequest INFO peer (Right uri) 59 Nothing+          SSL.lazyWrite s $ renderHeader 59 $ fromString $ "Invalid scheme: " <> scheme+ logRequest :: Priority -> SockAddr -> Either String URI -> Int -> Maybe String -> IO () logRequest p peer uri code meta = logM "Network.Gemini.Server" p $ unwords   [ show peer@@ -77,13 +128,19 @@   , maybe "-" show meta ]  -- | Shorthand for @Response 20 "text/gemini"@+--+-- @since 0.1.0.0 okGemini :: LBS.ByteString -> Response okGemini = Response 20 $ fromString "text/gemini"  -- | Shorthand for @Response 20 "text/plain"@+--+-- @since 0.1.0.0 okPlain :: LBS.ByteString -> Response okPlain = Response 20 $ fromString "text/plain" +-- | Shorthand for @Response 30 uri@+--+-- @since 0.1.0.0 redirect :: URI -> Response redirect uri = Response 30 (uriToString id uri "") mempty-
gemini-server.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.2  name:                gemini-server-version:             0.1.0.0+version:             0.2.0.0 synopsis:            A lightweight server for the Gemini protocol description:   This package contains a server for the@@ -25,7 +25,7 @@  library   exposed-modules:     Network.Gemini.Server-  build-depends:       base ^>=4.13.0.0 || ^>=4.14.0.0+  build-depends:       base ^>=4.12.0.0 || ^>=4.13.0.0 || ^>=4.14.0.0                      , network ^>=3.1.1.1                      , network-run ^>=0.2.3                      , network-uri ^>=2.6.3.0 || ^>=2.7.0.0@@ -33,6 +33,6 @@                      , utf8-string ^>=1.0.1.1                      , bytestring ^>=0.10.10.0                      , hslogger ^>=1.3.1.0+                     , HsOpenSSL ^>=0.11.5.1   ghc-options:         -Wall   default-language:    Haskell2010-