aboutsummaryrefslogtreecommitdiffstats
path: root/src/Reaktor/Plugins/SASL.hs
blob: e70a65d7c0ce26782431fb326100bbdd3db86398 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Reaktor.Plugins.SASL where

import Data.Aeson
import Data.Aeson.Types (typeMismatch)
import Data.Base64.Types (extractBase64)
import Data.Text qualified as Text
import Data.Text.Encoding.Base64 (encodeBase64)
import Prelude.Extended
import Reaktor
import Text.Read (readMaybe)


data Mechanism = PLAIN
  deriving (Read, Show)

instance ToJSON Mechanism where
  toJSON = String . Text.show

instance FromJSON Mechanism where
  parseJSON = withText "Mechanism" $ \s ->
    case readMaybe (Text.unpack s) of
      Just x  -> pure x
      Nothing -> fail "Invalid Mechanism value"


data Config = Config
    { cMechanism :: Mechanism
    , cUserName :: Text
    , cPassword :: Text
    }

instance Default Config where
  def = Config
          { cMechanism = PLAIN
          , cUserName = ""
          , cPassword = ""
          }

instance FromJSON Config where
  parseJSON = \case
    Object v ->
      Config
        <$> v .:? "mechanism" .!= PLAIN
        <*> v .: "username"
        <*> v .: "password"
    invalid ->
      typeMismatch "Config" invalid


new :: Config -> Actions -> IO (Message -> IO ())
new Config{..} Actions{..} =
    return $ \case
      Start | cUserName /= "" && cPassword /= "" -> do
        aSend (Message Nothing CAP ["REQ", "sasl"])

      Message _ CAP [_username, "ACK", "sasl"] -> do
        aSend (Message Nothing AUTHENTICATE [Text.show cMechanism])

      Message _ AUTHENTICATE ["+"] ->
        case cMechanism of
          PLAIN ->
            mapM_ (\chunk -> aSend (Message Nothing AUTHENTICATE [chunk]))
                  (toAuthChunks (toBase64 ("\0" <> cUserName <> "\0" <> cPassword)))

      Message _ RPL_SASLSUCCESS _ ->
        aSend (Message Nothing CAP ["END"])

      Message _ ERR_SASLFAIL _ ->
        aSend (Message Nothing CAP ["END"])

      Message _ ERR_SASLTOOLONG _ ->
        aSend (Message Nothing CAP ["END"])

      Message _ ERR_SASLALREADY _ ->
        aSend (Message Nothing CAP ["END"])

      _ -> pure ()


toBase64 :: Text -> Text
toBase64 = extractBase64 . encodeBase64


-- Split input Text into chunks compatibe with the AUTHENTICATE command.
-- This function assumes that each character in the input Text is exactly one
-- byte long, which is true for any Base64 encoded Text.
--
-- See also: https://ircv3.net/specs/extensions/sasl-3.1#the-authenticate-command
toAuthChunks :: Text -> [Text]
toAuthChunks s =
    if not (null chunks) && Text.length (last chunks) == 400 then
      chunks <> ["+"]
    else
      chunks
  where
    chunks = toChunksOf 400 s


toChunksOf :: Int -> Text -> [Text]
toChunksOf n s
    | Text.null s = []
    | otherwise = let (a,b) = Text.splitAt n s
                  in a : toChunksOf n b