Initial commit
Only Bind and Search operations are (partially) implemented. More tests and documentation are needed.
This commit is contained in:
commit
7aa2703319
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
dist/
|
||||
.cabal-sandbox/
|
||||
cabal.sandbox.config
|
||||
27
.vim.custom
Normal file
27
.vim.custom
Normal file
@ -0,0 +1,27 @@
|
||||
function s:hdevtools_options(rgs)
|
||||
return join(map(a:rgs, "'-g ' . v:val"))
|
||||
endfunction
|
||||
|
||||
function s:discover_cabal_sandbox(glob)
|
||||
let l:sandboxes = split(glob(a:glob, "."), "\n")
|
||||
if len(l:sandboxes) > 0
|
||||
return ['-no-user-package-db', '-package-db=' . l:sandboxes[-1]]
|
||||
else
|
||||
return []
|
||||
endif
|
||||
endfunction
|
||||
|
||||
let g:syntastic_haskell_hdevtools_args = s:hdevtools_options
|
||||
\ (
|
||||
\ [ '-isrc'
|
||||
\ , '-ibin'
|
||||
\ , '-itest'
|
||||
\ , '-idist/build/autogen'
|
||||
\ , '-DTEST'
|
||||
\ , '-O0'
|
||||
\ , '-fdefer-type-errors'
|
||||
\ , '-Wall'
|
||||
\ , '-fno-warn-unused-do-bind'
|
||||
\ , '-fno-warn-type-defaults'
|
||||
\ ] + s:discover_cabal_sandbox(".cabal-sandbox/*.conf.d")
|
||||
\ )
|
||||
26
LICENSE
Normal file
26
LICENSE
Normal file
@ -0,0 +1,26 @@
|
||||
Copyright (c) 2015, Matvey Aksenov
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the
|
||||
distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
28
README.markdown
Normal file
28
README.markdown
Normal file
@ -0,0 +1,28 @@
|
||||
ldap-client
|
||||
-----------
|
||||
|
||||
This library implements (the parts of) [RFC 4511][rfc4511]
|
||||
|
||||
Feature | RFC Section | Support
|
||||
:--------------------------- |:-----------:|:-----------:
|
||||
Bind Operation | 4.2 | ✔
|
||||
Unbind Operation | 4.3 | ✔
|
||||
Notice of Disconnection | 4.4.1 | ✘
|
||||
Search Operation | 4.5 | ✔ (partial)
|
||||
Modify Operation | 4.6 | ✘
|
||||
Add Operation | 4.7 | ✘
|
||||
Delete Operation | 4.8 | ✘
|
||||
Modify DN Operation | 4.9 | ✘
|
||||
Compare Operation | 4.10 | ✘
|
||||
Abandon Operation | 4.11 | ✘
|
||||
Extended Operation | 4.12 | ✘
|
||||
IntermediateResponse Message | 4.13 | ✘
|
||||
StartTLS Operation | 4.14 | ✘
|
||||
LDAP over TLS | - | ✔
|
||||
|
||||
```
|
||||
% git grep '\bString\b' | wc -l
|
||||
2
|
||||
```
|
||||
|
||||
[rfc4511]: https://tools.ietf.org/html/rfc4511
|
||||
102
example/login.hs
Normal file
102
example/login.hs
Normal file
@ -0,0 +1,102 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
-- | An example of how to do LDAP logins with ldap-client.
|
||||
--
|
||||
-- First, the assumptions this example makes. It defaults to LDAP over TLS,
|
||||
-- so if you only have a plaintext server, please replace `Secure` with `Plain`.
|
||||
-- It also assumes the accounts you may want to log in as all have
|
||||
-- `objectClass` "Person".
|
||||
--
|
||||
-- To run the example you have to provide a bunch of environment variables:
|
||||
--
|
||||
-- - `HOST` is the LDAP host to connect to (without "ldap://", "ldaps://", etc).
|
||||
-- - `POST` is the port LDAP server listens on.
|
||||
-- - `MANAGER_DN` is the DN of the account the first bind is made with.
|
||||
-- - `MANAGER_PASSWORD` is its password.
|
||||
-- - `BASE_OBJECT` is the search root
|
||||
module Main (main) where
|
||||
|
||||
import Control.Exception (bracket_) -- base
|
||||
import Control.Monad (when) -- base
|
||||
import Data.Function (fix) -- base
|
||||
import Data.Text (Text) -- text
|
||||
import qualified Data.Text.Encoding as Text -- text
|
||||
import qualified Data.Text.IO as Text -- text
|
||||
import Env -- envparse
|
||||
import qualified Ldap.Client as Ldap -- ldap-client
|
||||
import Ldap.Client -- ldap-client
|
||||
( LdapError
|
||||
, Scope(..)
|
||||
, Filter(..)
|
||||
, Attr(..)
|
||||
)
|
||||
import System.Exit (die) -- base
|
||||
import qualified System.IO as IO -- base
|
||||
|
||||
|
||||
data Conf = Conf
|
||||
{ host :: String
|
||||
, port :: Ldap.PortNumber
|
||||
, dn :: Ldap.Dn
|
||||
, password :: Ldap.Password
|
||||
, base :: Ldap.Dn
|
||||
} deriving (Show, Eq)
|
||||
|
||||
getConf :: IO Conf
|
||||
getConf = Env.parse (header "LDAP login example") $ Conf
|
||||
<$> var str "HOST" (help "LDAP hostname")
|
||||
<*> var (fmap fromIntegral . auto) "PORT" (help "LDAP port")
|
||||
<*> var (fmap Ldap.Dn . str) "MANAGER_DN" (help "Manager login DN")
|
||||
<*> var (fmap Ldap.Password . str) "MANAGER_PASSWORD" (help "Manager password")
|
||||
<*> var (fmap Ldap.Dn . str) "BASE_OBJECT" (help "Search root")
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
conf <- getConf
|
||||
res <- login conf
|
||||
case res of
|
||||
Left e -> die (show e)
|
||||
Right _ -> return ()
|
||||
|
||||
login :: Conf -> IO (Either LdapError ())
|
||||
login conf =
|
||||
Ldap.with (Ldap.Secure (host conf)) (port conf) $ \l -> do
|
||||
Ldap.bind l (dn conf) (password conf)
|
||||
fix $ \loop -> do
|
||||
uid <- prompt "Username: "
|
||||
us <- Ldap.search l (base conf)
|
||||
(Ldap.scope WholeSubtree <> Ldap.typesOnly True)
|
||||
(And [ Attr "objectClass" := "Person"
|
||||
, Attr "uid" := Text.encodeUtf8 uid
|
||||
])
|
||||
[]
|
||||
case us of
|
||||
Ldap.SearchEntry udn _ : _ ->
|
||||
fix $ \loop' -> do
|
||||
pwd <- bracket_ hideOutput
|
||||
showOutput
|
||||
(do pwd <- prompt ("Password for ‘" <> uid <> "’: ")
|
||||
Text.putStr "\n"
|
||||
return pwd)
|
||||
res <- Ldap.bindEither l udn (Ldap.Password (Text.encodeUtf8 pwd))
|
||||
case res of
|
||||
Left _ -> do again <- question "Invalid password. Try again? [y/n] "
|
||||
when again loop'
|
||||
Right _ -> Text.putStrLn "OK"
|
||||
[] -> do again <- question "Invalid username. Try again? [y/n] "
|
||||
when again loop
|
||||
|
||||
prompt :: Text -> IO Text
|
||||
prompt msg = do Text.putStr msg; IO.hFlush IO.stdout; Text.getLine
|
||||
|
||||
question :: Text -> IO Bool
|
||||
question msg = fix $ \loop -> do
|
||||
res <- prompt msg
|
||||
case res of
|
||||
"y" -> return True
|
||||
"n" -> return False
|
||||
_ -> do Text.putStrLn "Please, answer either ‘y’ or ‘n’."; loop
|
||||
|
||||
hideOutput, showOutput :: IO ()
|
||||
hideOutput = IO.hSetEcho IO.stdout False
|
||||
showOutput = IO.hSetEcho IO.stdout True
|
||||
43
ldap-client.cabal
Normal file
43
ldap-client.cabal
Normal file
@ -0,0 +1,43 @@
|
||||
name: ldap-client
|
||||
version: 0.1.0
|
||||
synopsis: Pure Haskell LDAP Client Library
|
||||
description:
|
||||
Pure Haskell LDAP client library implementing (the parts of) RFC 4511.
|
||||
homepage: https://supki.github.io/ldap-client
|
||||
license: BSD2
|
||||
license-file: LICENSE
|
||||
author: Matvey Aksenov
|
||||
maintainer: matvey.aksenov@gmail.com
|
||||
copyright: 2015 Matvey Aksenov
|
||||
category: Network
|
||||
build-type: Simple
|
||||
cabal-version: >= 1.10
|
||||
extra-source-files:
|
||||
README.markdown
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
location: git@github.com:supki/ldap-client
|
||||
|
||||
library
|
||||
default-language:
|
||||
Haskell2010
|
||||
hs-source-dirs:
|
||||
src
|
||||
exposed-modules:
|
||||
Ldap.Asn1.FromAsn1
|
||||
Ldap.Asn1.ToAsn1
|
||||
Ldap.Asn1.Type
|
||||
Ldap.Client
|
||||
build-depends:
|
||||
asn1-encoding >= 0.9
|
||||
, asn1-types >= 0.3
|
||||
, async
|
||||
, base >= 4.7 && < 5
|
||||
, bytestring
|
||||
, connection >= 0.2
|
||||
, containers
|
||||
, network >= 2.6
|
||||
, semigroups >= 0.16
|
||||
, stm
|
||||
, text
|
||||
303
src/Ldap/Asn1/FromAsn1.hs
Normal file
303
src/Ldap/Asn1/FromAsn1.hs
Normal file
@ -0,0 +1,303 @@
|
||||
module Ldap.Asn1.FromAsn1
|
||||
( FromAsn1(..)
|
||||
, Parser
|
||||
, parseAsn1
|
||||
, parse
|
||||
, next
|
||||
) where
|
||||
|
||||
import Control.Applicative (Alternative(..), optional)
|
||||
import Control.Monad ((>=>), MonadPlus(..))
|
||||
import Data.ASN1.Types (ASN1)
|
||||
import qualified Data.ASN1.Types as Asn1
|
||||
import Data.Foldable (asum)
|
||||
import Data.List.NonEmpty (some1)
|
||||
import qualified Data.Set as Set
|
||||
import qualified Data.Text.Encoding as Text
|
||||
|
||||
import Ldap.Asn1.Type
|
||||
|
||||
|
||||
class FromAsn1 a where
|
||||
fromAsn1 :: Parser [ASN1] a
|
||||
|
||||
{- |
|
||||
LDAPMessage ::= SEQUENCE {
|
||||
messageID MessageID,
|
||||
protocolOp CHOICE {
|
||||
bindRequest BindRequest,
|
||||
bindResponse BindResponse,
|
||||
unbindRequest UnbindRequest,
|
||||
searchRequest SearchRequest,
|
||||
searchResEntry SearchResultEntry,
|
||||
searchResDone SearchResultDone,
|
||||
searchResRef SearchResultReference,
|
||||
... },
|
||||
controls [0] Controls OPTIONAL }
|
||||
-}
|
||||
instance FromAsn1 op => FromAsn1 (LdapMessage op) where
|
||||
fromAsn1 = do
|
||||
Asn1.Start Asn1.Sequence <- next
|
||||
i <- fromAsn1
|
||||
op <- fromAsn1
|
||||
Asn1.End Asn1.Sequence <- next
|
||||
return (LdapMessage i op Nothing)
|
||||
|
||||
{- |
|
||||
MessageID ::= INTEGER (0 .. maxInt)
|
||||
-}
|
||||
instance FromAsn1 Id where
|
||||
fromAsn1 = do
|
||||
Asn1.IntVal i <- next
|
||||
return (Id (fromIntegral i))
|
||||
|
||||
{- |
|
||||
LDAPString ::= OCTET STRING -- UTF-8 encoded,
|
||||
-}
|
||||
instance FromAsn1 LdapString where
|
||||
fromAsn1 = do
|
||||
Asn1.OctetString s <- next
|
||||
case Text.decodeUtf8' s of
|
||||
Right t -> return (LdapString t)
|
||||
Left _ -> empty
|
||||
|
||||
{- |
|
||||
LDAPDN ::= LDAPString
|
||||
-}
|
||||
instance FromAsn1 LdapDn where
|
||||
fromAsn1 = fmap LdapDn fromAsn1
|
||||
|
||||
{- |
|
||||
AttributeDescription ::= LDAPString
|
||||
-}
|
||||
instance FromAsn1 AttributeDescription where
|
||||
fromAsn1 = fmap AttributeDescription fromAsn1
|
||||
|
||||
{- |
|
||||
AttributeValue ::= OCTET STRING
|
||||
-}
|
||||
instance FromAsn1 AttributeValue where
|
||||
fromAsn1 = do
|
||||
Asn1.OctetString s <- next
|
||||
return (AttributeValue s)
|
||||
|
||||
{- |
|
||||
PartialAttribute ::= SEQUENCE {
|
||||
type AttributeDescription,
|
||||
vals SET OF value AttributeValue }
|
||||
-}
|
||||
instance FromAsn1 PartialAttribute where
|
||||
fromAsn1 = do
|
||||
Asn1.Start Asn1.Sequence <- next
|
||||
d <- fromAsn1
|
||||
Asn1.Start Asn1.Set <- next
|
||||
vs <- many fromAsn1
|
||||
Asn1.End Asn1.Set <- next
|
||||
Asn1.End Asn1.Sequence <- next
|
||||
return (PartialAttribute d (Set.fromList vs))
|
||||
|
||||
{- |
|
||||
LDAPResult ::= SEQUENCE {
|
||||
resultCode ENUMERATED {
|
||||
success (0),
|
||||
operationsError (1),
|
||||
protocolError (2),
|
||||
timeLimitExceeded (3),
|
||||
sizeLimitExceeded (4),
|
||||
compareFalse (5),
|
||||
compareTrue (6),
|
||||
authMethodNotSupported (7),
|
||||
strongerAuthRequired (8),
|
||||
-- 9 reserved --
|
||||
referral (10),
|
||||
adminLimitExceeded (11),
|
||||
unavailableCriticalExtension (12),
|
||||
confidentialityRequired (13),
|
||||
saslBindInProgress (14),
|
||||
noSuchAttribute (16),
|
||||
undefinedAttributeType (17),
|
||||
inappropriateMatching (18),
|
||||
constraintViolation (19),
|
||||
attributeOrValueExists (20),
|
||||
invalidAttributeSyntax (21),
|
||||
-- 22-31 unused --
|
||||
noSuchObject (32),
|
||||
aliasProblem (33),
|
||||
invalidDNSyntax (34),
|
||||
-- 35 reserved for undefined isLeaf --
|
||||
aliasDereferencingProblem (36),
|
||||
-- 37-47 unused --
|
||||
inappropriateAuthentication (48),
|
||||
invalidCredentials (49),
|
||||
insufficientAccessRights (50),
|
||||
busy (51),
|
||||
unavailable (52),
|
||||
unwillingToPerform (53),
|
||||
loopDetect (54),
|
||||
-- 55-63 unused --
|
||||
namingViolation (64),
|
||||
objectClassViolation (65),
|
||||
notAllowedOnNonLeaf (66),
|
||||
notAllowedOnRDN (67),
|
||||
entryAlreadyExists (68),
|
||||
objectClassModsProhibited (69),
|
||||
-- 70 reserved for CLDAP --
|
||||
affectsMultipleDSAs (71),
|
||||
-- 72-79 unused --
|
||||
other (80),
|
||||
... },
|
||||
matchedDN LDAPDN,
|
||||
diagnosticMessage LDAPString,
|
||||
referral [3] Referral OPTIONAL }
|
||||
-}
|
||||
instance FromAsn1 LdapResult where
|
||||
fromAsn1 = do
|
||||
resultCode <- do
|
||||
Asn1.Enumerated x <- next
|
||||
case x of
|
||||
0 -> pure Success
|
||||
1 -> pure OperationError
|
||||
2 -> pure ProtocolError
|
||||
3 -> pure TimeLimitExceeded
|
||||
4 -> pure SizeLimitExceeded
|
||||
5 -> pure CompareFalse
|
||||
6 -> pure CompareTrue
|
||||
7 -> pure AuthMethodNotSupported
|
||||
8 -> pure StrongerAuthRequired
|
||||
10 -> pure Referral
|
||||
11 -> pure AdminLimitExceeded
|
||||
12 -> pure UnavailableCriticalExtension
|
||||
13 -> pure ConfidentialityRequired
|
||||
14 -> pure SaslBindInProgress
|
||||
16 -> pure NoSuchAttribute
|
||||
17 -> pure UndefinedAttributeType
|
||||
18 -> pure InappropriateMatching
|
||||
19 -> pure ConstraintViolation
|
||||
20 -> pure AttributeOrValueExists
|
||||
21 -> pure InvalidAttributeSyntax
|
||||
32 -> pure NoSuchObject
|
||||
33 -> pure AliasProblem
|
||||
34 -> pure InvalidDNSyntax
|
||||
36 -> pure AliasDereferencingProblem
|
||||
48 -> pure InappropriateAuthentication
|
||||
49 -> pure InvalidCredentials
|
||||
50 -> pure InsufficientAccessRights
|
||||
51 -> pure Busy
|
||||
52 -> pure Unavailable
|
||||
53 -> pure UnwillingToPerform
|
||||
54 -> pure LoopDetect
|
||||
64 -> pure NamingViolation
|
||||
65 -> pure ObjectClassViolation
|
||||
66 -> pure NotAllowedOnNonLeaf
|
||||
67 -> pure NotAllowedOnRDN
|
||||
68 -> pure EntryAlreadyExists
|
||||
69 -> pure ObjectClassModsProhibited
|
||||
71 -> pure AffectsMultipleDSAs
|
||||
80 -> pure Other
|
||||
_ -> empty
|
||||
matchedDn <- fromAsn1
|
||||
diagnosticMessage
|
||||
<- fromAsn1
|
||||
referral <- optional $ do
|
||||
Asn1.Start (Asn1.Container Asn1.Context 0) <- next
|
||||
x <- fromAsn1
|
||||
Asn1.End (Asn1.Container Asn1.Context 0) <- next
|
||||
return x
|
||||
return (LdapResult resultCode matchedDn diagnosticMessage referral)
|
||||
|
||||
{- |
|
||||
Referral ::= SEQUENCE SIZE (1..MAX) OF uri URI
|
||||
-}
|
||||
instance FromAsn1 ReferralUris where
|
||||
fromAsn1 = do
|
||||
Asn1.Start Asn1.Sequence <- next
|
||||
xs <- some1 fromAsn1
|
||||
Asn1.End Asn1.Sequence <- next
|
||||
return (ReferralUris xs)
|
||||
|
||||
{- |
|
||||
URI ::= LDAPString
|
||||
-}
|
||||
instance FromAsn1 Uri where
|
||||
fromAsn1 = fmap Uri fromAsn1
|
||||
|
||||
{- |
|
||||
BindResponse ::= [APPLICATION 1] SEQUENCE {
|
||||
COMPONENTS OF LDAPResult,
|
||||
serverSaslCreds [7] OCTET STRING OPTIONAL }
|
||||
|
||||
SearchResultEntry ::= [APPLICATION 4] SEQUENCE {
|
||||
objectName LDAPDN,
|
||||
attributes PartialAttributeList }
|
||||
|
||||
SearchResultDone ::= [APPLICATION 5] LDAPResult
|
||||
-}
|
||||
instance FromAsn1 ProtocolServerOp where
|
||||
fromAsn1 = asum
|
||||
[ do
|
||||
Asn1.Start (Asn1.Container Asn1.Application 1) <- next
|
||||
result <- fromAsn1
|
||||
Asn1.End (Asn1.Container Asn1.Application 1) <- next
|
||||
return (BindResponse result Nothing)
|
||||
|
||||
, do
|
||||
Asn1.Start (Asn1.Container Asn1.Application 4) <- next
|
||||
ldapDn <- fromAsn1
|
||||
partialAttributeList <- fromAsn1
|
||||
Asn1.End (Asn1.Container Asn1.Application 4) <- next
|
||||
return (SearchResultEntry ldapDn partialAttributeList)
|
||||
|
||||
, do
|
||||
Asn1.Start (Asn1.Container Asn1.Application 5) <- next
|
||||
result <- fromAsn1
|
||||
Asn1.End (Asn1.Container Asn1.Application 5) <- next
|
||||
return (SearchResultDone result)
|
||||
]
|
||||
|
||||
{- |
|
||||
PartialAttributeList ::= SEQUENCE OF partialAttribute PartialAttribute
|
||||
-}
|
||||
instance FromAsn1 PartialAttributeList where
|
||||
fromAsn1 = do
|
||||
Asn1.Start Asn1.Sequence <- next
|
||||
xs <- many fromAsn1
|
||||
Asn1.End Asn1.Sequence <- next
|
||||
return (PartialAttributeList xs)
|
||||
|
||||
|
||||
newtype Parser s a = Parser { unParser :: s -> Maybe (s, a) }
|
||||
|
||||
instance Functor (Parser s) where
|
||||
fmap f (Parser g) = Parser (fmap (fmap f) . g)
|
||||
|
||||
instance Applicative (Parser s) where
|
||||
pure x = Parser (\s -> pure (s, x))
|
||||
Parser mf <*> Parser mx = Parser $ \s -> do
|
||||
(s', f) <- mf s
|
||||
(s'', x) <- mx s'
|
||||
pure (s'', f x)
|
||||
|
||||
instance Alternative (Parser s) where
|
||||
empty = Parser (\_ -> empty)
|
||||
Parser ma <|> Parser mb =
|
||||
Parser (\s -> ma s <|> mb s)
|
||||
|
||||
instance Monad (Parser s) where
|
||||
return x = Parser (\s -> return (s, x))
|
||||
Parser mx >>= k =
|
||||
Parser (mx >=> \(s', x) -> unParser (k x) s')
|
||||
fail _ = empty
|
||||
|
||||
instance MonadPlus (Parser s) where
|
||||
mzero = Parser (\_ -> mzero)
|
||||
Parser ma `mplus` Parser mb =
|
||||
Parser (\s -> ma s `mplus` mb s)
|
||||
|
||||
parseAsn1 :: FromAsn1 a => [ASN1] -> Maybe ([ASN1], a)
|
||||
parseAsn1 = parse fromAsn1
|
||||
|
||||
parse :: Parser s a -> s -> Maybe (s, a)
|
||||
parse = unParser
|
||||
|
||||
next :: Parser [s] s
|
||||
next = Parser (\s -> case s of [] -> Nothing; x : xs -> Just (xs, x))
|
||||
258
src/Ldap/Asn1/ToAsn1.hs
Normal file
258
src/Ldap/Asn1/ToAsn1.hs
Normal file
@ -0,0 +1,258 @@
|
||||
module Ldap.Asn1.ToAsn1
|
||||
( ToAsn1(..)
|
||||
) where
|
||||
|
||||
import Data.ASN1.Types (ASN1, ASN1Class, ASN1Tag, ASN1ConstructionType)
|
||||
import qualified Data.ASN1.Types as Asn1
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Foldable (fold, foldMap)
|
||||
import Data.Maybe (Maybe, maybe)
|
||||
import Data.Monoid (Endo(Endo), (<>), mempty)
|
||||
import qualified Data.Text.Encoding as Text
|
||||
import Prelude ((.), fromIntegral)
|
||||
|
||||
import Ldap.Asn1.Type
|
||||
|
||||
|
||||
class ToAsn1 a where
|
||||
toAsn1 :: a -> Endo [ASN1]
|
||||
|
||||
{- |
|
||||
LDAPMessage ::= SEQUENCE {
|
||||
messageID MessageID,
|
||||
protocolOp CHOICE {
|
||||
bindRequest BindRequest,
|
||||
bindResponse BindResponse,
|
||||
unbindRequest UnbindRequest,
|
||||
searchRequest SearchRequest,
|
||||
searchResEntry SearchResultEntry,
|
||||
searchResDone SearchResultDone,
|
||||
searchResRef SearchResultReference,
|
||||
... },
|
||||
controls [0] Controls OPTIONAL }
|
||||
-}
|
||||
instance ToAsn1 op => ToAsn1 (LdapMessage op) where
|
||||
toAsn1 (LdapMessage i op mc) =
|
||||
sequence (toAsn1 i <> toAsn1 op <> context 0 (optional mc))
|
||||
|
||||
{- |
|
||||
MessageID ::= INTEGER (0 .. maxInt)
|
||||
-}
|
||||
instance ToAsn1 Id where
|
||||
toAsn1 (Id i) = single (Asn1.IntVal (fromIntegral i))
|
||||
|
||||
{- |
|
||||
LDAPString ::= OCTET STRING -- UTF-8 encoded
|
||||
-}
|
||||
instance ToAsn1 LdapString where
|
||||
toAsn1 (LdapString s) = single (Asn1.OctetString (Text.encodeUtf8 s))
|
||||
|
||||
{- |
|
||||
LDAPOID ::= OCTET STRING -- Constrained to <numericoid>
|
||||
-}
|
||||
instance ToAsn1 LdapOid where
|
||||
toAsn1 (LdapOid s) = single (Asn1.OctetString s)
|
||||
|
||||
{- |
|
||||
LDAPDN ::= LDAPString -- Constrained to <distinguishedName>
|
||||
-}
|
||||
instance ToAsn1 LdapDn where
|
||||
toAsn1 (LdapDn s) = toAsn1 s
|
||||
|
||||
{- |
|
||||
AttributeDescription ::= LDAPString
|
||||
-}
|
||||
instance ToAsn1 AttributeDescription where
|
||||
toAsn1 (AttributeDescription s) = toAsn1 s
|
||||
|
||||
{- |
|
||||
AttributeValue ::= OCTET STRING
|
||||
-}
|
||||
instance ToAsn1 AttributeValue where
|
||||
toAsn1 (AttributeValue s) = single (Asn1.OctetString s)
|
||||
|
||||
{- |
|
||||
AttributeValueAssertion ::= SEQUENCE {
|
||||
attributeDesc AttributeDescription,
|
||||
assertionValue AssertionValue }
|
||||
-}
|
||||
instance ToAsn1 AttributeValueAssertion where
|
||||
toAsn1 (AttributeValueAssertion d v) = toAsn1 d <> toAsn1 v
|
||||
|
||||
{- |
|
||||
AssertionValue ::= OCTET STRING
|
||||
-}
|
||||
instance ToAsn1 AssertionValue where
|
||||
toAsn1 (AssertionValue s) = single (Asn1.OctetString s)
|
||||
|
||||
{- |
|
||||
MatchingRuleId ::= LDAPString
|
||||
-}
|
||||
instance ToAsn1 MatchingRuleId where
|
||||
toAsn1 (MatchingRuleId s) = toAsn1 s
|
||||
|
||||
{- |
|
||||
Controls ::= SEQUENCE OF control Control
|
||||
-}
|
||||
instance ToAsn1 Controls where
|
||||
toAsn1 (Controls cs) = sequence (foldMap toAsn1 cs)
|
||||
|
||||
{- |
|
||||
Control ::= SEQUENCE {
|
||||
controlType LDAPOID,
|
||||
criticality BOOLEAN DEFAULT FALSE,
|
||||
controlValue OCTET STRING OPTIONAL }
|
||||
-}
|
||||
instance ToAsn1 Control where
|
||||
toAsn1 (Control t c v) =
|
||||
sequence (fold
|
||||
[ toAsn1 t
|
||||
, single (Asn1.Boolean c)
|
||||
, maybe mempty (single . Asn1.OctetString) v
|
||||
])
|
||||
|
||||
{- |
|
||||
BindRequest ::= [APPLICATION 0] SEQUENCE {
|
||||
version INTEGER (1 .. 127),
|
||||
name LDAPDN,
|
||||
authentication AuthenticationChoice }
|
||||
|
||||
UnbindRequest ::= [APPLICATION 2] NULL
|
||||
|
||||
SearchRequest ::= [APPLICATION 3] SEQUENCE {
|
||||
baseObject LDAPDN,
|
||||
scope ENUMERATED {
|
||||
baseObject (0),
|
||||
singleLevel (1),
|
||||
wholeSubtree (2),
|
||||
... },
|
||||
derefAliases ENUMERATED {
|
||||
neverDerefAliases (0),
|
||||
derefInSearching (1),
|
||||
derefFindingBaseObj (2),
|
||||
derefAlways (3) },
|
||||
sizeLimit INTEGER (0 .. maxInt),
|
||||
timeLimit INTEGER (0 .. maxInt),
|
||||
typesOnly BOOLEAN,
|
||||
filter Filter,
|
||||
attributes AttributeSelection }
|
||||
-}
|
||||
instance ToAsn1 ProtocolClientOp where
|
||||
toAsn1 (BindRequest v n a) =
|
||||
application 0 (single (Asn1.IntVal (fromIntegral v)) <> toAsn1 n <> toAsn1 a)
|
||||
toAsn1 UnbindRequest =
|
||||
other Asn1.Application 2 mempty
|
||||
toAsn1 (SearchRequest bo s da sl tl to f a) =
|
||||
application 3 (fold
|
||||
[ toAsn1 bo
|
||||
, single (Asn1.Enumerated s')
|
||||
, single (Asn1.Enumerated da')
|
||||
, single (Asn1.IntVal (fromIntegral sl))
|
||||
, single (Asn1.IntVal (fromIntegral tl))
|
||||
, single (Asn1.Boolean to)
|
||||
, toAsn1 f
|
||||
, toAsn1 a
|
||||
])
|
||||
where
|
||||
s' = case s of
|
||||
BaseObject -> 0
|
||||
SingleLevel -> 1
|
||||
WholeSubtree -> 2
|
||||
da' = case da of
|
||||
NeverDerefAliases -> 0
|
||||
DerefInSearching -> 1
|
||||
DerefFindingBaseObject -> 2
|
||||
DerefAlways -> 3
|
||||
|
||||
{- |
|
||||
AuthenticationChoice ::= CHOICE {
|
||||
simple [0] OCTET STRING,
|
||||
... }
|
||||
-}
|
||||
instance ToAsn1 AuthenticationChoice where
|
||||
toAsn1 (Simple s) = other Asn1.Context 0 s
|
||||
|
||||
{- |
|
||||
AttributeSelection ::= SEQUENCE OF selector LDAPString
|
||||
-}
|
||||
instance ToAsn1 AttributeSelection where
|
||||
toAsn1 (AttributeSelection as) = sequence (foldMap toAsn1 as)
|
||||
|
||||
{- |
|
||||
Filter ::= CHOICE {
|
||||
and [0] SET SIZE (1..MAX) OF filter Filter,
|
||||
or [1] SET SIZE (1..MAX) OF filter Filter,
|
||||
not [2] Filter,
|
||||
equalityMatch [3] AttributeValueAssertion,
|
||||
substrings [4] SubstringFilter,
|
||||
greaterOrEqual [5] AttributeValueAssertion,
|
||||
lessOrEqual [6] AttributeValueAssertion,
|
||||
present [7] AttributeDescription,
|
||||
approxMatch [8] AttributeValueAssertion,
|
||||
extensibleMatch [9] MatchingRuleAssertion,
|
||||
... }
|
||||
-}
|
||||
instance ToAsn1 Filter where
|
||||
toAsn1 f = case f of
|
||||
And xs -> context 0 (foldMap toAsn1 xs)
|
||||
Or xs -> context 1 (foldMap toAsn1 xs)
|
||||
Not x -> context 2 (toAsn1 x)
|
||||
EqualityMatch x -> context 3 (toAsn1 x)
|
||||
Substrings x -> context 4 (toAsn1 x)
|
||||
GreaterOrEqual x -> context 5 (toAsn1 x)
|
||||
LessOrEqual x -> context 6 (toAsn1 x)
|
||||
Present x -> context 7 (toAsn1 x)
|
||||
ApproxMatch x -> context 8 (toAsn1 x)
|
||||
ExtensibleMatch x -> context 9 (toAsn1 x)
|
||||
|
||||
{- |
|
||||
SubstringFilter ::= SEQUENCE {
|
||||
type AttributeDescription,
|
||||
substrings SEQUENCE SIZE (1..MAX) OF substring CHOICE {
|
||||
initial [0] AssertionValue, -- can occur at most once
|
||||
any [1] AssertionValue,
|
||||
final [2] AssertionValue } -- can occur at most once
|
||||
}
|
||||
-}
|
||||
instance ToAsn1 SubstringFilter where
|
||||
toAsn1 (SubstringFilter ad ss) =
|
||||
toAsn1 ad <> sequence (foldMap (\s -> case s of
|
||||
Initial (AssertionValue v) -> other Asn1.Context 0 v
|
||||
Any (AssertionValue v) -> other Asn1.Context 1 v
|
||||
Final (AssertionValue v) -> other Asn1.Context 2 v) ss)
|
||||
|
||||
{- |
|
||||
MatchingRuleAssertion ::= SEQUENCE {
|
||||
matchingRule [1] MatchingRuleId OPTIONAL,
|
||||
type [2] AttributeDescription OPTIONAL,
|
||||
matchValue [3] AssertionValue,
|
||||
dnAttributes [4] BOOLEAN DEFAULT FALSE }
|
||||
-}
|
||||
instance ToAsn1 MatchingRuleAssertion where
|
||||
toAsn1 (MatchingRuleAssertion mmr mad av b) = sequence (fold
|
||||
[ context 1 (optional mmr)
|
||||
, context 2 (optional mad)
|
||||
, context 3 (toAsn1 av)
|
||||
, context 4 (single (Asn1.Boolean b))
|
||||
])
|
||||
|
||||
sequence :: Endo [ASN1] -> Endo [ASN1]
|
||||
sequence = construction Asn1.Sequence
|
||||
|
||||
application :: ASN1Tag -> Endo [ASN1] -> Endo [ASN1]
|
||||
application = construction . Asn1.Container Asn1.Application
|
||||
|
||||
context :: ASN1Tag -> Endo [ASN1] -> Endo [ASN1]
|
||||
context = construction . Asn1.Container Asn1.Context
|
||||
|
||||
construction :: ASN1ConstructionType -> Endo [ASN1] -> Endo [ASN1]
|
||||
construction t x = single (Asn1.Start t) <> x <> single (Asn1.End t)
|
||||
|
||||
other :: ASN1Class -> ASN1Tag -> ByteString -> Endo [ASN1]
|
||||
other c t = single . Asn1.Other c t
|
||||
|
||||
optional :: ToAsn1 a => Maybe a -> Endo [ASN1]
|
||||
optional = maybe mempty toAsn1
|
||||
|
||||
single :: a -> Endo [a]
|
||||
single x = Endo (x :)
|
||||
165
src/Ldap/Asn1/Type.hs
Normal file
165
src/Ldap/Asn1/Type.hs
Normal file
@ -0,0 +1,165 @@
|
||||
module Ldap.Asn1.Type where
|
||||
|
||||
import Data.ByteString (ByteString)
|
||||
import Data.Int (Int8, Int32)
|
||||
import Data.List.NonEmpty (NonEmpty)
|
||||
import Data.Set (Set)
|
||||
import Data.Text (Text)
|
||||
|
||||
|
||||
data LdapMessage op = LdapMessage
|
||||
{ ldapMessageId :: !Id
|
||||
, ldapMessageOp :: !op
|
||||
, ldapMessageControls :: !(Maybe Controls)
|
||||
} deriving (Show, Eq, Ord)
|
||||
|
||||
newtype Id = Id { unId :: Int32 }
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data ProtocolClientOp =
|
||||
BindRequest Int8 LdapDn AuthenticationChoice
|
||||
| UnbindRequest
|
||||
| SearchRequest LdapDn Scope DerefAliases Int32 Int32 Bool Filter AttributeSelection
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data ProtocolServerOp =
|
||||
BindResponse LdapResult (Maybe ByteString)
|
||||
| SearchResultEntry LdapDn PartialAttributeList
|
||||
| SearchResultReference (NonEmpty Uri)
|
||||
| SearchResultDone (LdapResult)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data AuthenticationChoice = Simple ByteString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data Scope =
|
||||
BaseObject
|
||||
| SingleLevel
|
||||
| WholeSubtree
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data DerefAliases =
|
||||
NeverDerefAliases
|
||||
| DerefInSearching
|
||||
| DerefFindingBaseObject
|
||||
| DerefAlways
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data Filter =
|
||||
And (NonEmpty Filter)
|
||||
| Or (NonEmpty Filter)
|
||||
| Not Filter
|
||||
| EqualityMatch AttributeValueAssertion
|
||||
| Substrings SubstringFilter
|
||||
| GreaterOrEqual AttributeValueAssertion
|
||||
| LessOrEqual AttributeValueAssertion
|
||||
| Present AttributeDescription
|
||||
| ApproxMatch AttributeValueAssertion
|
||||
| ExtensibleMatch MatchingRuleAssertion
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data SubstringFilter = SubstringFilter AttributeDescription (NonEmpty Substring)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data Substring =
|
||||
Initial AssertionValue
|
||||
| Any AssertionValue
|
||||
| Final AssertionValue
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data MatchingRuleAssertion = MatchingRuleAssertion (Maybe MatchingRuleId) (Maybe AttributeDescription) AssertionValue Bool
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype MatchingRuleId = MatchingRuleId LdapString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype AttributeSelection = AttributeSelection [LdapString]
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype PartialAttributeList = PartialAttributeList [PartialAttribute]
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype Controls = Controls [Control]
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data Control = Control LdapOid Bool (Maybe ByteString)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data LdapResult = LdapResult ResultCode LdapDn LdapString (Maybe ReferralUris)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data ResultCode =
|
||||
Success
|
||||
| OperationError
|
||||
| ProtocolError
|
||||
| TimeLimitExceeded
|
||||
| SizeLimitExceeded
|
||||
| CompareFalse
|
||||
| CompareTrue
|
||||
| AuthMethodNotSupported
|
||||
| StrongerAuthRequired
|
||||
| Referral
|
||||
| AdminLimitExceeded
|
||||
| UnavailableCriticalExtension
|
||||
| ConfidentialityRequired
|
||||
| SaslBindInProgress
|
||||
| NoSuchAttribute
|
||||
| UndefinedAttributeType
|
||||
| InappropriateMatching
|
||||
| ConstraintViolation
|
||||
| AttributeOrValueExists
|
||||
| InvalidAttributeSyntax
|
||||
| NoSuchObject
|
||||
| AliasProblem
|
||||
| InvalidDNSyntax
|
||||
| AliasDereferencingProblem
|
||||
| InappropriateAuthentication
|
||||
| InvalidCredentials
|
||||
| InsufficientAccessRights
|
||||
| Busy
|
||||
| Unavailable
|
||||
| UnwillingToPerform
|
||||
| LoopDetect
|
||||
| NamingViolation
|
||||
| ObjectClassViolation
|
||||
| NotAllowedOnNonLeaf
|
||||
| NotAllowedOnRDN
|
||||
| EntryAlreadyExists
|
||||
| ObjectClassModsProhibited
|
||||
| AffectsMultipleDSAs
|
||||
| Other
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype AttributeDescription = AttributeDescription LdapString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype AttributeValue = AttributeValue ByteString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data AttributeValueAssertion = AttributeValueAssertion AttributeDescription AssertionValue
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype AssertionValue = AssertionValue ByteString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data PartialAttribute = PartialAttribute AttributeDescription (Set AttributeValue)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype LdapDn = LdapDn LdapString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype ReferralUris = ReferralUris (NonEmpty Uri)
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype Uri = Uri LdapString
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
-- | The LDAPString is a notational convenience to indicate that, although
|
||||
-- strings of LDAPString type encode as ASN.1 OCTET STRING types, the
|
||||
-- [ISO10646] character set (a superset of [Unicode]) is used, encoded
|
||||
-- following the UTF-8 [RFC3629] algorithm.
|
||||
newtype LdapString = LdapString Text
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
newtype LdapOid = LdapOid ByteString
|
||||
deriving (Show, Eq, Ord)
|
||||
446
src/Ldap/Client.hs
Normal file
446
src/Ldap/Client.hs
Normal file
@ -0,0 +1,446 @@
|
||||
{-# LANGUAGE BangPatterns #-}
|
||||
{-# LANGUAGE DeriveDataTypeable #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
module Ldap.Client
|
||||
( Host(..)
|
||||
, PortNumber
|
||||
, Ldap
|
||||
, LdapError(..)
|
||||
, Async
|
||||
, with
|
||||
-- * Bind Request
|
||||
, Dn(..)
|
||||
, Password(..)
|
||||
, bind
|
||||
, bindEither
|
||||
, bindAsync
|
||||
, bindAsyncSTM
|
||||
-- * Search Request
|
||||
, Type.Scope(..)
|
||||
, Attr(..)
|
||||
, SearchEntry(..)
|
||||
, search
|
||||
, searchEither
|
||||
, searchAsync
|
||||
, searchAsyncSTM
|
||||
, Search
|
||||
, defaultSearch
|
||||
, scope
|
||||
, size
|
||||
, time
|
||||
, typesOnly
|
||||
, derefAliases
|
||||
, Filter(..)
|
||||
-- * Unbind Request
|
||||
, unbindAsync
|
||||
, unbindAsyncSTM
|
||||
-- * Waiting for Request Completion
|
||||
, wait
|
||||
, waitSTM
|
||||
) where
|
||||
|
||||
import qualified Control.Concurrent.Async as Async
|
||||
import Control.Concurrent.STM (STM, atomically)
|
||||
import Control.Concurrent.STM.TMVar (TMVar, newEmptyTMVar, putTMVar, readTMVar)
|
||||
import Control.Concurrent.STM.TQueue (TQueue, newTQueueIO, writeTQueue, readTQueue)
|
||||
import Control.Exception (Exception, Handler(..), bracket, throwIO, catches)
|
||||
import Control.Monad (forever, void)
|
||||
import qualified Data.ASN1.BinaryEncoding as Asn1
|
||||
import qualified Data.ASN1.Encoding as Asn1
|
||||
import qualified Data.ASN1.Error as Asn1
|
||||
import Data.ByteString (ByteString)
|
||||
import qualified Data.ByteString as ByteString
|
||||
import qualified Data.ByteString.Lazy as ByteString.Lazy
|
||||
import Data.Foldable (traverse_, asum)
|
||||
import Data.Function (fix)
|
||||
import Data.Int (Int32)
|
||||
import Data.List.NonEmpty (NonEmpty((:|)))
|
||||
import qualified Data.List.NonEmpty as NonEmpty
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.Monoid (Endo(appEndo))
|
||||
import Data.Set (Set)
|
||||
import qualified Data.Set as Set
|
||||
import Data.Text (Text)
|
||||
import Data.Typeable (Typeable)
|
||||
import Network.Connection (Connection)
|
||||
import qualified Network.Connection as Conn
|
||||
import Network (PortNumber)
|
||||
import qualified System.IO.Error as IO
|
||||
|
||||
import Ldap.Asn1.ToAsn1 (ToAsn1(toAsn1))
|
||||
import Ldap.Asn1.FromAsn1 (FromAsn1, parseAsn1)
|
||||
import qualified Ldap.Asn1.Type as Type
|
||||
|
||||
|
||||
data Host =
|
||||
Plain String
|
||||
| Secure String
|
||||
deriving (Show, Eq, Ord)
|
||||
|
||||
data Ldap = Ldap
|
||||
{ client :: TQueue ClientMessage
|
||||
} deriving (Eq)
|
||||
|
||||
data ClientMessage = New Request (TMVar (NonEmpty Type.ProtocolServerOp))
|
||||
type Request = Type.ProtocolClientOp
|
||||
type InMessage = Type.ProtocolServerOp
|
||||
type Response = NonEmpty InMessage
|
||||
|
||||
newLdap :: IO Ldap
|
||||
newLdap = Ldap
|
||||
<$> newTQueueIO
|
||||
|
||||
data LdapError =
|
||||
IOError IOError
|
||||
| ParseError Asn1.ASN1Error
|
||||
| BindError BindError
|
||||
| SearchError SearchError
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- | The entrypoint into LDAP.
|
||||
with :: Host -> PortNumber -> (Ldap -> IO a) -> IO (Either LdapError a)
|
||||
with host port f = do
|
||||
context <- Conn.initConnectionContext
|
||||
bracket (Conn.connectTo context params) Conn.connectionClose (\conn ->
|
||||
bracket newLdap unbindAsync (\l -> do
|
||||
inq <- newTQueueIO
|
||||
outq <- newTQueueIO
|
||||
Async.withAsync (input inq conn) $ \i ->
|
||||
Async.withAsync (output outq conn) $ \o ->
|
||||
Async.withAsync (dispatch l inq outq) $ \d ->
|
||||
Async.withAsync (f l) $ \u ->
|
||||
fmap (Right . snd) (Async.waitAnyCancel [i, o, d, u])))
|
||||
`catches`
|
||||
[ Handler (return . Left . IOError)
|
||||
, Handler (return . Left . ParseError)
|
||||
, Handler (return . Left . BindError)
|
||||
, Handler (return . Left . SearchError)
|
||||
]
|
||||
where
|
||||
params = Conn.ConnectionParams
|
||||
{ Conn.connectionHostname =
|
||||
case host of
|
||||
Plain h -> h
|
||||
Secure h -> h
|
||||
, Conn.connectionPort = port
|
||||
, Conn.connectionUseSecure =
|
||||
case host of
|
||||
Plain _ -> Nothing
|
||||
Secure _ -> Just Conn.TLSSettingsSimple
|
||||
{ Conn.settingDisableCertificateValidation = False
|
||||
, Conn.settingDisableSession = False
|
||||
, Conn.settingUseServerName = False
|
||||
}
|
||||
, Conn.connectionUseSocks = Nothing
|
||||
}
|
||||
|
||||
input :: FromAsn1 a => TQueue a -> Connection -> IO b
|
||||
input inq conn = flip fix [] $ \loop chunks -> do
|
||||
chunk <- Conn.connectionGet conn 8192
|
||||
case ByteString.length chunk of
|
||||
0 -> throwIO (IO.mkIOError IO.eofErrorType "Ldap.Client.input" Nothing Nothing)
|
||||
_ -> do
|
||||
let chunks' = chunk : chunks
|
||||
case Asn1.decodeASN1 Asn1.DER (ByteString.Lazy.fromChunks (reverse chunks')) of
|
||||
Left Asn1.ParsingPartial
|
||||
-> loop chunks'
|
||||
Left e -> throwIO e
|
||||
Right asn1 -> do
|
||||
flip fix asn1 $ \loop' asn1' ->
|
||||
case parseAsn1 asn1' of
|
||||
Nothing -> return ()
|
||||
Just (asn1'', a) -> do
|
||||
atomically (writeTQueue inq a)
|
||||
loop' asn1''
|
||||
loop []
|
||||
|
||||
output :: ToAsn1 a => TQueue a -> Connection -> IO b
|
||||
output out conn = forever $
|
||||
Conn.connectionPut conn . encode . toAsn1 =<< atomically (readTQueue out)
|
||||
where
|
||||
encode x = Asn1.encodeASN1' Asn1.DER (appEndo x [])
|
||||
|
||||
dispatch :: Ldap -> TQueue (Type.LdapMessage InMessage) -> TQueue (Type.LdapMessage Request) -> IO a
|
||||
dispatch Ldap { client } inq outq =
|
||||
flip fix (Map.empty, Map.empty, 1) $ \loop (!got, !results, !counter) -> do
|
||||
loop =<< atomically (asum
|
||||
[ do New new var <- readTQueue client
|
||||
writeTQueue outq (Type.LdapMessage (Type.Id counter) new Nothing)
|
||||
return (got, Map.insert (Type.Id counter) var results, counter + 1)
|
||||
, do Type.LdapMessage mid op _ <- readTQueue inq
|
||||
case op of
|
||||
Type.BindResponse {} -> do
|
||||
traverse_ (\var -> putTMVar var (op :| [])) (Map.lookup mid results)
|
||||
return (Map.delete mid got, Map.delete mid results, counter)
|
||||
Type.SearchResultEntry {} -> do
|
||||
return (Map.insertWith (++) mid [op] got, results, counter)
|
||||
Type.SearchResultReference {} -> do
|
||||
return (got, results, counter)
|
||||
Type.SearchResultDone {} -> do
|
||||
let stack = Map.findWithDefault [] mid got
|
||||
traverse_ (\var -> putTMVar var (op :| stack)) (Map.lookup mid results)
|
||||
return (Map.delete mid got, Map.delete mid results, counter)
|
||||
])
|
||||
|
||||
|
||||
data Async e a = Async (STM (Either e a))
|
||||
|
||||
instance Functor (Async e) where
|
||||
fmap f (Async stm) = Async (fmap (fmap f) stm)
|
||||
|
||||
|
||||
newtype Dn = Dn Text
|
||||
deriving (Show, Eq)
|
||||
newtype Password = Password ByteString
|
||||
deriving (Show, Eq)
|
||||
|
||||
data BindError =
|
||||
BindInvalidResponse Response
|
||||
| BindErrorCode Type.ResultCode
|
||||
deriving (Show, Eq, Typeable)
|
||||
|
||||
instance Exception BindError
|
||||
|
||||
-- | Throws 'BindError' on failure. Don't worry, the nearest 'with'
|
||||
-- will catch it, so it won't destroy your program.
|
||||
bind :: Ldap -> Dn -> Password -> IO ()
|
||||
bind l username password =
|
||||
raise =<< bindEither l username password
|
||||
|
||||
bindEither :: Ldap -> Dn -> Password -> IO (Either BindError ())
|
||||
bindEither l username password =
|
||||
wait =<< bindAsync l username password
|
||||
|
||||
bindAsync :: Ldap -> Dn -> Password -> IO (Async BindError ())
|
||||
bindAsync l username password =
|
||||
atomically (bindAsyncSTM l username password)
|
||||
|
||||
bindAsyncSTM :: Ldap -> Dn -> Password -> STM (Async BindError ())
|
||||
bindAsyncSTM l username password =
|
||||
sendRequest l bindResult (bindRequest username password)
|
||||
|
||||
bindRequest :: Dn -> Password -> Request
|
||||
bindRequest (Dn username) (Password password) =
|
||||
Type.BindRequest ldapVersion
|
||||
(Type.LdapDn (Type.LdapString username))
|
||||
(Type.Simple password)
|
||||
where
|
||||
ldapVersion = 3
|
||||
|
||||
bindResult :: Response -> Either BindError ()
|
||||
bindResult (Type.BindResponse (Type.LdapResult code _ _ _) _ :| [])
|
||||
| Type.Success <- code = Right ()
|
||||
| otherwise = Left (BindErrorCode code)
|
||||
bindResult res = Left (BindInvalidResponse res)
|
||||
|
||||
|
||||
data SearchError =
|
||||
SearchInvalidResponse Response
|
||||
| SearchErrorCode Type.ResultCode
|
||||
deriving (Show, Eq, Typeable)
|
||||
|
||||
instance Exception SearchError
|
||||
|
||||
search
|
||||
:: Ldap
|
||||
-> Dn
|
||||
-> Mod Search
|
||||
-> Filter
|
||||
-> [Attr]
|
||||
-> IO [SearchEntry]
|
||||
search l base opts flt attributes =
|
||||
raise =<< searchEither l base opts flt attributes
|
||||
|
||||
searchEither
|
||||
:: Ldap
|
||||
-> Dn
|
||||
-> Mod Search
|
||||
-> Filter
|
||||
-> [Attr]
|
||||
-> IO (Either SearchError [SearchEntry])
|
||||
searchEither l base opts flt attributes =
|
||||
wait =<< searchAsync l base opts flt attributes
|
||||
|
||||
searchAsync
|
||||
:: Ldap
|
||||
-> Dn
|
||||
-> Mod Search
|
||||
-> Filter
|
||||
-> [Attr]
|
||||
-> IO (Async SearchError [SearchEntry])
|
||||
searchAsync l base opts flt attributes =
|
||||
atomically (searchAsyncSTM l base opts flt attributes)
|
||||
|
||||
searchAsyncSTM
|
||||
:: Ldap
|
||||
-> Dn
|
||||
-> Mod Search
|
||||
-> Filter
|
||||
-> [Attr]
|
||||
-> STM (Async SearchError [SearchEntry])
|
||||
searchAsyncSTM l base opts flt attributes =
|
||||
sendRequest l searchResult (searchRequest base opts flt attributes)
|
||||
|
||||
searchResult :: Response -> Either SearchError [SearchEntry]
|
||||
searchResult (Type.SearchResultDone (Type.LdapResult code _ _ _) :| xs)
|
||||
| Type.Success <- code = Right (mapMaybe g xs)
|
||||
| otherwise = Left (SearchErrorCode code)
|
||||
where
|
||||
g (Type.SearchResultEntry (Type.LdapDn (Type.LdapString dn))
|
||||
(Type.PartialAttributeList ys)) =
|
||||
Just (SearchEntry (Dn dn) (map h ys))
|
||||
g _ = Nothing
|
||||
h (Type.PartialAttribute (Type.AttributeDescription (Type.LdapString x))
|
||||
y) = (Attr x, Set.map j y)
|
||||
j (Type.AttributeValue x) = x
|
||||
searchResult res = Left (SearchInvalidResponse res)
|
||||
|
||||
searchRequest :: Dn -> Mod Search -> Filter -> [Attr] -> Request
|
||||
searchRequest (Dn base) (Mod m) flt attributes =
|
||||
Type.SearchRequest (Type.LdapDn (Type.LdapString base))
|
||||
_scope
|
||||
_derefAliases
|
||||
_size
|
||||
_time
|
||||
_typesOnly
|
||||
(fromFilter flt)
|
||||
(Type.AttributeSelection (map (Type.LdapString . unAttr) attributes))
|
||||
where
|
||||
Search { _scope, _derefAliases, _size, _time, _typesOnly } =
|
||||
m defaultSearch
|
||||
fromFilter (Not x) = Type.Not (fromFilter x)
|
||||
fromFilter (And xs) = Type.And (fmap fromFilter xs)
|
||||
fromFilter (Or xs) = Type.Or (fmap fromFilter xs)
|
||||
fromFilter (Present (Attr x)) =
|
||||
Type.Present (Type.AttributeDescription (Type.LdapString x))
|
||||
fromFilter (Attr x := y) =
|
||||
Type.EqualityMatch
|
||||
(Type.AttributeValueAssertion (Type.AttributeDescription (Type.LdapString x))
|
||||
(Type.AssertionValue y))
|
||||
fromFilter (Attr x :>= y) =
|
||||
Type.GreaterOrEqual
|
||||
(Type.AttributeValueAssertion (Type.AttributeDescription (Type.LdapString x))
|
||||
(Type.AssertionValue y))
|
||||
fromFilter (Attr x :<= y) =
|
||||
Type.LessOrEqual
|
||||
(Type.AttributeValueAssertion (Type.AttributeDescription (Type.LdapString x))
|
||||
(Type.AssertionValue y))
|
||||
fromFilter (Attr x :~= y) =
|
||||
Type.ApproxMatch
|
||||
(Type.AttributeValueAssertion (Type.AttributeDescription (Type.LdapString x))
|
||||
(Type.AssertionValue y))
|
||||
fromFilter (Attr x :=* (mi, xs, mf)) =
|
||||
Type.Substrings
|
||||
(Type.SubstringFilter (Type.AttributeDescription (Type.LdapString x))
|
||||
(NonEmpty.fromList (concat
|
||||
[ maybe [] (\i -> [Type.Initial (Type.AssertionValue i)]) mi
|
||||
, fmap (Type.Any . Type.AssertionValue) xs
|
||||
, maybe [] (\f -> [Type.Final (Type.AssertionValue f)]) mf
|
||||
])))
|
||||
fromFilter ((mx, mr, b) ::= y) =
|
||||
Type.ExtensibleMatch
|
||||
(Type.MatchingRuleAssertion (fmap (\(Attr r) -> Type.MatchingRuleId (Type.LdapString r)) mr)
|
||||
(fmap (\(Attr x) -> Type.AttributeDescription (Type.LdapString x)) mx)
|
||||
(Type.AssertionValue y)
|
||||
b)
|
||||
|
||||
data Search = Search
|
||||
{ _scope :: Type.Scope
|
||||
, _derefAliases :: Type.DerefAliases
|
||||
, _size :: Int32
|
||||
, _time :: Int32
|
||||
, _typesOnly :: Bool
|
||||
} deriving (Show, Eq)
|
||||
|
||||
defaultSearch :: Search
|
||||
defaultSearch = Search
|
||||
{ _scope = Type.BaseObject
|
||||
, _size = 0
|
||||
, _time = 0
|
||||
, _typesOnly = False
|
||||
, _derefAliases = Type.NeverDerefAliases
|
||||
}
|
||||
|
||||
scope :: Type.Scope -> Mod Search
|
||||
scope x = Mod (\y -> y { _scope = x })
|
||||
|
||||
size :: Int32 -> Mod Search
|
||||
size x = Mod (\y -> y { _size = x })
|
||||
|
||||
time :: Int32 -> Mod Search
|
||||
time x = Mod (\y -> y { _time = x })
|
||||
|
||||
typesOnly :: Bool -> Mod Search
|
||||
typesOnly x = Mod (\y -> y { _typesOnly = x })
|
||||
|
||||
derefAliases :: Type.DerefAliases -> Mod Search
|
||||
derefAliases x = Mod (\y -> y { _derefAliases = x })
|
||||
|
||||
newtype Mod a = Mod (a -> a)
|
||||
|
||||
instance Monoid (Mod a) where
|
||||
mempty = Mod id
|
||||
Mod f `mappend` Mod g = Mod (g . f)
|
||||
|
||||
data Filter =
|
||||
Not Filter
|
||||
| And (NonEmpty Filter)
|
||||
| Or (NonEmpty Filter)
|
||||
| Present Attr
|
||||
| Attr := ByteString
|
||||
| Attr :>= ByteString
|
||||
| Attr :<= ByteString
|
||||
| Attr :~= ByteString
|
||||
| Attr :=* (Maybe ByteString, [ByteString], Maybe ByteString)
|
||||
| (Maybe Attr, Maybe Attr, Bool) ::= ByteString
|
||||
|
||||
newtype Attr = Attr Text
|
||||
deriving (Show, Eq)
|
||||
|
||||
-- 'Attr' unwrapper. This is a separate function not to turn 'Attr''s
|
||||
-- 'Show' instance into complete and utter shit.
|
||||
unAttr :: Attr -> Text
|
||||
unAttr (Attr a) = a
|
||||
|
||||
data SearchEntry = SearchEntry Dn [(Attr, Set ByteString)]
|
||||
deriving (Show, Eq)
|
||||
|
||||
|
||||
-- | Note that 'unbindAsync' does not return an 'Async',
|
||||
-- because LDAP server never responds to @UnbindRequest@s, hence
|
||||
-- a call to 'wait' on a hypothetical 'Async' would have resulted
|
||||
-- in an exception anyway.
|
||||
unbindAsync :: Ldap -> IO ()
|
||||
unbindAsync =
|
||||
atomically . unbindAsyncSTM
|
||||
|
||||
-- | Note that 'unbindAsyncSTM' does not return an 'Async',
|
||||
-- because LDAP server never responds to @UnbindRequest@s, hence
|
||||
-- a call to 'wait' on a hypothetical 'Async' would have resulted
|
||||
-- in an exception anyway.
|
||||
unbindAsyncSTM :: Ldap -> STM ()
|
||||
unbindAsyncSTM l =
|
||||
void (sendRequest l die Type.UnbindRequest)
|
||||
where
|
||||
die = error "Ldap.Client: do not wait for the response to UnbindRequest"
|
||||
|
||||
|
||||
wait :: Async e a -> IO (Either e a)
|
||||
wait = atomically . waitSTM
|
||||
|
||||
waitSTM :: Async e a -> STM (Either e a)
|
||||
waitSTM (Async stm) = stm
|
||||
|
||||
|
||||
sendRequest :: Ldap -> (Response -> Either e a) -> Request -> STM (Async e a)
|
||||
sendRequest l p msg =
|
||||
do var <- newEmptyTMVar
|
||||
writeRequest l var msg
|
||||
return (Async (fmap p (readTMVar var)))
|
||||
|
||||
writeRequest :: Ldap -> TMVar Response -> Request -> STM ()
|
||||
writeRequest Ldap { client } var msg = writeTQueue client (New msg var)
|
||||
|
||||
raise :: Exception e => Either e a -> IO a
|
||||
raise = either throwIO return
|
||||
Loading…
Reference in New Issue
Block a user