20 Commits
Author SHA1 Message Date
jlamothe a45a75c3e2 get the client certificate from the connection's context
...instead of the janky thing we were doing before
2023-01-04 10:26:56 -05:00
jlamothe 77cfcce5df version 0.1.0.1 2023-01-04 10:09:48 -05:00
jlamothe 115dcf3998 Merge pull request 'catch *any* exception' (#3) from crashfix into dev
Reviewed-on: https://codeberg.org/jlamothe/gemcap/pulls/3
2023-01-04 15:02:01 +00:00
jlamothe 554efdabcb catch *any* exception
`listenLoop` was crashing when the client closed the connection during the handshake.  How this doesn't qualify as an `IOException` is beyond me.
2023-01-01 11:52:59 -05:00
jlamothe a69a9385b6 use tls 1.5.8 (or better) 2022-12-31 09:25:02 -05:00
jlamothe fa8ef1104a use current LTS resolver 2022-12-31 01:17:30 -05:00
jlamothe 465b5a9115 don't worry about length of response status code 2021-11-25 03:26:36 -05:00
jlamothe 14f2064050 updated README 2021-11-25 03:24:58 -05:00
jlamothe 84b6d0bcae version 0.1.0 2021-11-24 19:46:02 -05:00
jlamothe 43b76aa39c implemented encodeGemResponse 2021-11-24 19:44:17 -05:00
jlamothe 391ffd3eea implemented stripCRLF 2021-11-24 19:44:17 -05:00
jlamothe 05e83857a7 tests for runConnection 2021-11-24 18:56:12 -05:00
jlamothe 7126838eb0 fixed a bug in readLoop that would cause the server hang while reading the request 2021-11-23 11:10:34 -05:00
jlamothe 7b8a887b3c made failure reason ob readLoop more obvious 2021-11-22 23:25:43 -05:00
jlamothe 61fca70a5e refactored readLoop 2021-11-21 02:22:40 -05:00
jlamothe 440aee7536 System.IO.Streams should be a qualified import 2021-11-20 11:43:54 -05:00
jlamothe 63702be1e7 whitespace fix 2021-11-20 11:43:11 -05:00
jlamothe 64444bbc81 implemented readMax 2021-11-19 20:58:41 -05:00
jlamothe 4a99e5cb0b implemented high-level logic for strFromConn 2021-11-19 19:30:54 -05:00
jlamothe 489d0fdb78 linting of readURL function
I disagree with the use of `<$>` in place of `.`, but not enough to fight hlint on it.
2021-11-19 19:28:46 -05:00
11 changed files with 299 additions and 116 deletions
+4 -1
View File
@@ -1,3 +1,6 @@
# Changelog for gemcap # Changelog for gemcap
## Unreleased changes ## 0.1.0.1
- updated underlying libraries to more current versions
- fixed a bug that would cause the server to crash when the client aborted the handshake by rejecting the key
+5 -5
View File
@@ -16,11 +16,6 @@ You should have received a copy of the GNU Affero General Public
License along with this program. If not, see License along with this program. If not, see
<https://www.gnu.org/licenses/>. <https://www.gnu.org/licenses/>.
## Important Note
This project is not yet ready for release. Everything within it should
be considered unstable and subject to change at any time.
## Executive Summary ## Executive Summary
This library is inspired very heavily by the This library is inspired very heavily by the
@@ -28,3 +23,8 @@ This library is inspired very heavily by the
package, but does not need to be linked against OpenSSL. Instead, it package, but does not need to be linked against OpenSSL. Instead, it
uses [tcp-streams](https://hackage.haskell.org/package/tcp-streams) to uses [tcp-streams](https://hackage.haskell.org/package/tcp-streams) to
provide TLS functionality. provide TLS functionality.
## Pull Requests
Pull requests welcome. That said, please make your pull requests to
the `dev` branch.
+9 -7
View File
@@ -1,11 +1,11 @@
cabal-version: 2.2 cabal-version: 2.2
-- This file has been generated from package.yaml by hpack version 0.34.4. -- This file has been generated from package.yaml by hpack version 0.35.0.
-- --
-- see: https://github.com/sol/hpack -- see: https://github.com/sol/hpack
name: gemcap name: gemcap
version: 0.0.0 version: 0.1.0.1
synopsis: a simple Gemini capsule (server) synopsis: a simple Gemini capsule (server)
description: a simple Gemini capsule (server) - see README.md for details description: a simple Gemini capsule (server) - see README.md for details
category: Gemini category: Gemini
@@ -34,12 +34,13 @@ library
ghc-options: -Wall ghc-options: -Wall
build-depends: build-depends:
base >=4.7 && <5 base >=4.7 && <5
, bytestring >=0.10.12.0 && <0.11 , bytestring >=0.11.3.1 && <0.12
, io-streams , io-streams
, network , network
, tcp-streams >=1.0.1.1 && <1.1 , tcp-streams >=1.0.1.1 && <1.1
, text >=1.2.4.1 && <1.3 , text >=1.2.4.1 && <1.3
, tls , tls >=1.5.8 && <1.6
, transformers
, x509 , x509
default-language: Haskell2010 default-language: Haskell2010
autogen-modules: Paths_gemcap autogen-modules: Paths_gemcap
@@ -57,14 +58,15 @@ test-suite gemcap-test
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
build-depends: build-depends:
base >=4.7 && <5 base >=4.7 && <5
, bytestring >=0.10.12.0 && <0.11 , bytestring >=0.11.3.1 && <0.12
, gemcap , gemcap
, hspec >=2.7.10 && <2.8 , hspec >=2.9.7 && <2.10
, io-streams , io-streams
, network , network
, tcp-streams >=1.0.1.1 && <1.1 , tcp-streams >=1.0.1.1 && <1.1
, text >=1.2.4.1 && <1.3 , text >=1.2.4.1 && <1.3
, tls , tls >=1.5.8 && <1.6
, transformers
, x509 , x509
default-language: Haskell2010 default-language: Haskell2010
autogen-modules: Paths_gemcap autogen-modules: Paths_gemcap
+5 -4
View File
@@ -1,5 +1,5 @@
name: gemcap name: gemcap
version: 0.0.0 version: 0.1.0.1
license: AGPL-3.0-or-later license: AGPL-3.0-or-later
author: "Jonathan Lamothe" author: "Jonathan Lamothe"
maintainer: "jonathan@jlamothe.net" maintainer: "jonathan@jlamothe.net"
@@ -25,12 +25,13 @@ ghc-options:
dependencies: dependencies:
- base >= 4.7 && < 5 - base >= 4.7 && < 5
- bytestring >= 0.10.12.0 && < 0.11 - bytestring >= 0.11.3.1 && < 0.12
- tcp-streams >= 1.0.1.1 && < 1.1 - tcp-streams >= 1.0.1.1 && < 1.1
- text >= 1.2.4.1 && < 1.3 - text >= 1.2.4.1 && < 1.3
- io-streams - io-streams
- network - network
- tls - tls >= 1.5.8 && < 1.6
- transformers
- x509 - x509
library: library:
@@ -48,6 +49,6 @@ tests:
- -with-rtsopts=-N - -with-rtsopts=-N
dependencies: dependencies:
- gemcap - gemcap
- hspec >= 2.7.10 && < 2.8 - hspec >= 2.9.7 && < 2.10
verbatim: verbatim:
<<: *paths <<: *paths
+14 -42
View File
@@ -31,15 +31,15 @@ module Network.Gemini.Capsule (
) where ) where
import Control.Concurrent (forkIO) import Control.Concurrent (forkIO)
import Control.Exception (IOException, try) import Control.Exception (SomeException, try)
import Control.Exception.Base (bracket, finally) import Control.Exception.Base (bracket, finally)
import Control.Monad (void) import Control.Monad (void)
import Control.Monad.Trans.Maybe (MaybeT (..), runMaybeT)
import qualified Data.Connection as C import qualified Data.Connection as C
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.TLSSetting (makeServerParams) import Data.TLSSetting (makeServerParams)
import Data.X509 (Certificate, CertificateChain (..), getSigned, signedObject) import Data.X509 (CertificateChain (..), getSigned, signedObject)
import qualified Network.Socket as S import qualified Network.Socket as S
import Network.TLS (ServerParams, onClientCertificate, serverHooks) import Network.TLS (ServerParams, getClientCertificateChain)
import System.IO.Streams.TCP (bindAndListen) import System.IO.Streams.TCP (bindAndListen)
import System.IO.Streams.TLS (accept) import System.IO.Streams.TLS (accept)
@@ -69,46 +69,18 @@ runGemCapsule settings handler = bracket
listenLoop :: S.Socket -> ServerParams -> GemHandler -> IO a listenLoop :: S.Socket -> ServerParams -> GemHandler -> IO a
listenLoop sock params handler = do listenLoop sock params handler = do
certRef <- newIORef Nothing try (accept params sock) >>= \case
let params' = adjustServerParams certRef params Left (_::SomeException) -> return ()
try (accept params' sock) >>= \case
Left (_::IOException) -> return ()
Right conn -> void $ forkIO $ finally Right conn -> void $ forkIO $ finally
(readIORef certRef >>= runConnection conn handler) ( do
let (context, _) = C.connExtraInfo conn
mCert <- runMaybeT $
MaybeT (getClientCertificateChain context) >>= \case
CertificateChain [] -> fail "no certificate"
CertificateChain (se:_) -> return $ signedObject $ getSigned se
runConnection conn handler mCert
)
(C.close conn) (C.close conn)
listenLoop sock params handler listenLoop sock params handler
adjustServerParams
:: IORef (Maybe Certificate)
-> ServerParams
-> ServerParams
adjustServerParams certRef params = let
hooks = serverHooks params
certHook = onClientCertificate hooks
certHook' chain = do
case chain of
CertificateChain [] -> return ()
CertificateChain (se:_) -> do
let cert = signedObject $ getSigned se
writeIORef certRef (Just cert)
certHook chain
hooks' = hooks { onClientCertificate = certHook' }
in params { serverHooks = hooks' }
runConnection
:: C.Connection a
-> GemHandler
-> Maybe Certificate
-> IO ()
runConnection conn handler mCert =
( readURL conn >>= \case
Nothing -> return $ newGemResponse
{ respStatus = 59
, respMeta = "bad request"
}
Just url -> handler (newGemRequest url) { reqCert = mCert }
) >>= sendResponse conn
--jl --jl
+25 -3
View File
@@ -30,15 +30,21 @@ module Network.Gemini.Capsule.Encoding (
encodeGemURL, encodeGemURL,
decodeGemURL, decodeGemURL,
escapeString, escapeString,
unescapeString unescapeString,
encodeGemResponse
) where ) where
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import Data.ByteString.Builder (charUtf8, stringUtf8, toLazyByteString) import Data.ByteString.Builder (
charUtf8,
lazyByteString,
stringUtf8,
toLazyByteString,
word8Dec)
import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy as BSL
import Data.Char (chr, ord, toLower) import Data.Char (chr, ord, toLower)
import Data.List (find, intercalate) import Data.List (find, intercalate)
import Data.Maybe (fromJust) import Data.Maybe (fromJust, fromMaybe)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8') import Data.Text.Encoding (decodeUtf8')
@@ -137,6 +143,22 @@ unescapeString str = case decodeUtf8' $ BS.pack $ toBytes str of
toNum ch = fst $ fromJust $ toNum ch = fst $ fromJust $
find (\x -> snd x == ch) $ zip [0..] hexDigits find (\x -> snd x == ch) $ zip [0..] hexDigits
-- | encodes a 'GemResponse' into a lazy ByteString
encodeGemResponse :: GemResponse -> BSL.ByteString
encodeGemResponse resp = let
code = respStatus resp
meta = respMeta resp
body = fromMaybe "" $ respBody resp
builder
= word8Dec code
<> charUtf8 ' '
<> stringUtf8 meta
<> stringUtf8 "\r\n"
<> lazyByteString body
in toLazyByteString builder
hexDigits :: String hexDigits :: String
hexDigits = ['0'..'9'] ++ ['a'..'f'] hexDigits = ['0'..'9'] ++ ['a'..'f']
+83 -13
View File
@@ -33,11 +33,24 @@ time.
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
module Network.Gemini.Capsule.Internal ( module Network.Gemini.Capsule.Internal (
runConnection,
readURL, readURL,
sendResponse strFromConn,
readMax,
stripCRLF
) where ) where
import Data.Connection (Connection) import Control.Monad (when)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Maybe (MaybeT (..))
import qualified Data.ByteString as BS
import Data.ByteString.Builder (Builder, byteString, toLazyByteString)
import qualified Data.ByteString.Lazy as BSL
import Data.Connection (Connection, send, source)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8')
import Data.X509 (Certificate)
import qualified System.IO.Streams as S
import Network.Gemini.Capsule.Encoding import Network.Gemini.Capsule.Encoding
import Network.Gemini.Capsule.Types import Network.Gemini.Capsule.Types
@@ -48,25 +61,31 @@ import Network.Gemini.Capsule.Types
inBufSize :: Int inBufSize :: Int
inBufSize = 1026 inBufSize = 1026
-- | process a request and return a response over a 'Connection'
runConnection
:: Connection a
-> GemHandler
-> Maybe Certificate
-> IO ()
runConnection conn handler mCert =
( readURL conn >>= \case
Nothing -> return $ newGemResponse
{ respStatus = 59
, respMeta = "bad request"
}
Just url -> handler (newGemRequest url) { reqCert = mCert }
) >>= sendResponse conn
-- | Reads a 'GemURL' from a 'Connection' -- | Reads a 'GemURL' from a 'Connection'
readURL readURL
:: Connection a :: Connection a
-- ^ the connection -- ^ the connection
-> IO (Maybe GemURL) -> IO (Maybe GemURL)
readURL conn = readURL conn =
strFromConn inBufSize conn >>= return . \case strFromConn inBufSize conn >>= return <$> \case
Nothing -> Nothing Nothing -> Nothing
Just str -> decodeGemURL str Just str -> decodeGemURL str
-- | Sends a 'GemResponse' to a 'Connection'
sendResponse
:: Connection a
-- ^ the connection
-> GemResponse
-- ^ the response being sent
-> IO ()
sendResponse = undefined
-- | Reads up to a maxumum number of bytes from a 'Connection', UTF-8 -- | Reads up to a maxumum number of bytes from a 'Connection', UTF-8
-- decodes it, and returns the resulting string (if possible) without -- decodes it, and returns the resulting string (if possible) without
-- the trailing CR/LF -- the trailing CR/LF
@@ -76,6 +95,57 @@ strFromConn
-> Connection a -> Connection a
-- ^ The connection to read from -- ^ The connection to read from
-> IO (Maybe String) -> IO (Maybe String)
strFromConn = undefined strFromConn maxLen conn = do
mbs <- readMax maxLen conn
return $ do
bs <- mbs
txt <- case decodeUtf8' bs of
Left _ -> Nothing
Right s -> Just s
stripCRLF $ T.unpack txt
-- | Reads from a connection up to a maximum number of bytes or a
-- newline character is encountered, returning 'Nothing' if the limit
-- is exceeded
readMax
:: Int
-- ^ the maximum number of bytes
-> Connection a
-- ^ the 'Connection' to read from
-> IO (Maybe BS.ByteString)
readMax maxLen conn = do
let src = source conn
runMaybeT $
BS.pack . BSL.unpack . toLazyByteString
<$> readLoop maxLen src
-- | Strips the CR/LF characters from the end of a string, retuning
-- Nothing if they are not present
stripCRLF :: String -> Maybe String
stripCRLF = \case
"" -> Nothing
"\r\n" -> Just ""
c:str -> (c:) <$> stripCRLF str
readLoop :: Int -> S.InputStream BS.ByteString -> MaybeT IO Builder
readLoop maxLen src = lift (S.read src) >>= \case
Nothing -> return mempty
Just bs -> do
let
len = BS.length bs
b = byteString bs
when (len > maxLen) $
fail "maximum length exceeded"
if BS.any (== 0xa) bs
then return b
else (b <>) <$> readLoop (maxLen - len) src
sendResponse
:: Connection a
-- ^ the connection
-> GemResponse
-- ^ the response being sent
-> IO ()
sendResponse conn resp = send conn $ encodeGemResponse resp
--jl --jl
+1 -4
View File
@@ -17,8 +17,7 @@
# #
# resolver: ./custom-snapshot.yaml # resolver: ./custom-snapshot.yaml
# resolver: https://example.com/snapshots/2018-01-01.yaml # resolver: https://example.com/snapshots/2018-01-01.yaml
resolver: resolver: lts-20.5
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/13.yaml
# User packages to be built. # User packages to be built.
# Various formats can be used as shown in the example below. # Various formats can be used as shown in the example below.
@@ -40,8 +39,6 @@ packages:
# - git: https://github.com/commercialhaskell/stack.git # - git: https://github.com/commercialhaskell/stack.git
# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a
# #
extra-deps:
- tcp-streams-1.0.1.1@sha256:35e9ecfa515797052f8c3c01834d2daebd5e93f3152c7fc98b32652bf6f0c052,2329
# Override default flag values for local packages and extra-deps # Override default flag values for local packages and extra-deps
# flags: {} # flags: {}
+5 -13
View File
@@ -3,18 +3,10 @@
# For more information, please see the documentation at: # For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files # https://docs.haskellstack.org/en/stable/lock_files
packages: packages: []
- completed:
hackage: tcp-streams-1.0.1.1@sha256:35e9ecfa515797052f8c3c01834d2daebd5e93f3152c7fc98b32652bf6f0c052,2329
pantry-tree:
size: 1004
sha256: 572071fca40a0b6c4cc950d10277a6f12e83cf4846882b6ef83fcccaa2c18c45
original:
hackage: tcp-streams-1.0.1.1@sha256:35e9ecfa515797052f8c3c01834d2daebd5e93f3152c7fc98b32652bf6f0c052,2329
snapshots: snapshots:
- completed: - completed:
size: 586268 sha256: a684cdbdf9304b325a503e0fe1d9648e9c18155ce4c7cfebbe8a7f93674e6295
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/13.yaml size: 649106
sha256: d9e658a22cfe8d87a64fdf219885f942fef5fe2bcb156a9800174911c5da2443 url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/5.yaml
original: original: lts-20.5
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/13.yaml
@@ -20,6 +20,8 @@ License along with this program. If not, see
-} -}
{-# LANGUAGE OverloadedStrings #-}
module Network.Gemini.Capsule.EncodingSpec (spec) where module Network.Gemini.Capsule.EncodingSpec (spec) where
import Test.Hspec (Spec, context, describe, it, shouldBe) import Test.Hspec (Spec, context, describe, it, shouldBe)
@@ -33,6 +35,7 @@ spec = describe "Encoding" $ do
decodeGemURLSpec decodeGemURLSpec
escapeStringSpec escapeStringSpec
unescapeStringSpec unescapeStringSpec
encodeGemResponseSpec
encodeGemURLSpec :: Spec encodeGemURLSpec :: Spec
encodeGemURLSpec = describe "encodeGemURL" $ mapM_ encodeGemURLSpec = describe "encodeGemURL" $ mapM_
@@ -143,4 +146,12 @@ unescapeStringSpec = describe "unescapeString" $ mapM_
, ( "foo%ff", Nothing ) , ( "foo%ff", Nothing )
] ]
encodeGemResponseSpec :: Spec
encodeGemResponseSpec = describe "encodeGemResponse" $
it ("should be " ++ show expect) $
encodeGemResponse resp `shouldBe` expect
where
resp = newGemResponse { respBody = Just "Success!\r\n" }
expect = "20 text/gemini\r\nSuccess!\r\n"
--jl --jl
+137 -24
View File
@@ -20,29 +20,75 @@ License along with this program. If not, see
-} -}
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings, RecordWildCards #-}
module Network.Gemini.Capsule.InternalSpec (spec) where module Network.Gemini.Capsule.InternalSpec (spec) where
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Char (ord) import Data.Char (ord)
import Data.Connection (Connection (..)) import Data.Connection (Connection (..))
import Data.IORef (IORef, modifyIORef', newIORef, readIORef)
import Data.X509 (Certificate (..))
import System.IO.Streams (nullInput, unRead) import System.IO.Streams (nullInput, unRead)
import Test.Hspec (Spec, context, describe, shouldReturn, xit) import Test.Hspec (
Spec,
context,
describe,
it,
shouldBe,
shouldReturn)
import Network.Gemini.Capsule.Types import Network.Gemini.Capsule.Types
import Network.Gemini.Capsule.Internal import Network.Gemini.Capsule.Internal
spec :: Spec spec :: Spec
spec = describe "Internal" $ do spec = describe "Internal" $ do
runConnectionSpec
readURLSpec readURLSpec
sendResponseSpec
strFromConnSpec strFromConnSpec
readMaxSpec
stripCRLFSpec
runConnectionSpec :: Spec
runConnectionSpec = describe "runConnection" $ mapM_
( \(desc, ioConnRef, handler, mCert, expect) -> context desc $
it ("should return " ++ show expect) $ do
(conn, outRef) <- ioConnRef
runConnection conn handler mCert
readIORef outRef `shouldReturn` expect
)
-- description, connection, handler, certificate, expectation
[ ( "basic connection", basicConn, basicH, Nothing, basicExp )
, ( "no certificate", basicConn, certH, Nothing, noCertExp )
, ( "with certificate", basicConn, certH, Just sampleCert, basicExp )
, ( "gibberish with CR/LF", gibConnCRLF, basicH, Nothing, gibExp )
, ( "gibberish w/o CR/LF", gibConn, basicH, Nothing, gibExp )
]
where
basicConn = mkIOConn ["gemini://example.com/\r\n"]
gibConnCRLF = mkIOConn ["aosidjgfoeribjeworifj\r\n"]
gibConn = mkIOConn ["sodifjboije"]
basicH _ = return newGemResponse { respBody = Just success }
certH req = return $ case reqCert req of
Nothing -> newGemResponse
{ respStatus = 60
, respMeta = "certificate required"
}
Just _ -> newGemResponse { respBody = Just success }
basicExp = ["20 text/gemini\r\nSuccess!\r\n"]
noCertExp = ["60 certificate required\r\n"]
gibExp = ["59 bad request\r\n"]
success = "Success!\r\n"
readURLSpec :: Spec readURLSpec :: Spec
readURLSpec = describe "readURL" $ mapM_ readURLSpec = describe "readURL" $ mapM_
( \(desc, ioConn, expect) -> context desc $ ( \(desc, ioConn, expect) -> context desc $
xit ("should return " ++ show expect) $ it ("should return " ++ show expect) $
do do
conn <- ioConn conn <- ioConn
readURL conn `shouldReturn` expect readURL conn `shouldReturn` expect
@@ -56,10 +102,10 @@ readURLSpec = describe "readURL" $ mapM_
] ]
where where
validConn = mkConn "gemini://example.com/\r\n" validConn = mkInConn ["gemini://example.com/\r\n"]
longConn = mkConn longBS longConn = mkInConn [longBS]
tooLongConn = mkConn tooLongBS tooLongConn = mkInConn [tooLongBS]
gibConn = mkConn "aosidjfwoeinboijwefr" gibConn = mkInConn ["aosidjfwoeinboijwefr"]
longBS = BS.pack (take 1024 bytes) <> "\r\n" longBS = BS.pack (take 1024 bytes) <> "\r\n"
tooLongBS = BS.pack (take 1025 bytes) <> "\r\n" tooLongBS = BS.pack (take 1025 bytes) <> "\r\n"
bytes = BS.unpack prefix ++ repeat (fromIntegral $ ord 'A') bytes = BS.unpack prefix ++ repeat (fromIntegral $ ord 'A')
@@ -67,23 +113,90 @@ readURLSpec = describe "readURL" $ mapM_
longExp = validExp { gemPath = [longDir] } longExp = validExp { gemPath = [longDir] }
longDir = replicate (1024 - BS.length prefix) 'A' longDir = replicate (1024 - BS.length prefix) 'A'
prefix = "gemini://example.com/" prefix = "gemini://example.com/"
mkConn bs = do
s <- nullInput
unRead bs s
return sampleConnection { source = s }
sendResponseSpec :: Spec
sendResponseSpec = describe "sendResponse" $ return ()
sampleConnection :: Connection a
sampleConnection = Connection
{ source = undefined
, send = const $ return ()
, close = return ()
, connExtraInfo = undefined
}
strFromConnSpec :: Spec strFromConnSpec :: Spec
strFromConnSpec = describe "strFromConn" $ return () strFromConnSpec = describe "strFromConn" $ mapM_
( \(desc, maxLen, ioConn, expect) -> context desc $
it ("should return " ++ show expect) $ do
conn <- ioConn
strFromConn maxLen conn `shouldReturn` expect
)
-- description, max size, connection, expected
[ ( "valid string", 100, mkInConn ["foo\r\n"], Just "foo" )
, ( "long string", 5, mkInConn ["too long\r\n"], Nothing )
, ( "no CR/LF", 100, mkInConn ["foo"], Nothing )
, ( "bad UTF-8", 100, mkInConn ["foo\xff\r\n"], Nothing )
, ( "non-ASCII", 100, mkInConn ["\xc3\xa9\r\n"], Just "\xe9" )
]
readMaxSpec :: Spec
readMaxSpec = describe "readMax" $ mapM_
( \(desc, maxLen, ioConn, expect) -> context desc $
it ("should return " ++ show expect) $ do
conn <- ioConn
readMax maxLen conn `shouldReturn` expect
)
-- description, max length, connection, expected
[ ( "single input", 1024, singleConn, Just singleBS )
, ( "multi input", 1024, multiConn, Just multiBS )
, ( "long input", longLen, longConn, Just longBS )
, ( "too long", pred longLen, longConn, Nothing )
, ( "empty input", 1024, mkInConn [], Just "" )
]
where
singleConn = mkInConn ["foo"]
multiConn = mkInConn ["foo", "bar", "baz"]
longConn = mkInConn [longBS]
longLen = BS.length longBS
singleBS = "foo"
multiBS = "foobarbaz"
longBS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
stripCRLFSpec :: Spec
stripCRLFSpec = describe "stripCRLF" $ mapM_
( \(input, expected) -> context (show input) $
it ("should be" ++ show expected) $
stripCRLF input `shouldBe` expected
)
-- input, expectation
[ ( "foo\r\n", Just "foo" )
, ( "foo\n", Nothing )
, ( "foo", Nothing )
, ( "\r\n", Just "" )
]
mkIOConn :: [BS.ByteString] -> IO (Connection (), IORef [BSL.ByteString])
mkIOConn input = do
ref <- newIORef []
conn <-
( \c -> c { send = \bs -> modifyIORef' ref (++[bs]) }
) <$> mkInConn input
return (conn, ref)
mkInConn :: [BS.ByteString] -> IO (Connection ())
mkInConn bss = do
source <- nullInput
mapM_ (`unRead` source) (reverse bss)
let
send = const $ return ()
close = return ()
connExtraInfo = ()
return Connection {..}
sampleCert :: Certificate
sampleCert = Certificate
{ certVersion = undefined
, certSerial = undefined
, certSignatureAlg = undefined
, certIssuerDN = undefined
, certValidity = undefined
, certSubjectDN = undefined
, certPubKey = undefined
, certExtensions = undefined
}
--jl --jl