///|
/// A simple sequential LDAP session. It allocates message IDs, sends
/// requests and matches the corresponding responses.
pub struct Session[T] {
config : LdapConfig
transport : T
priv mut next_id : Int
priv mut bound : Bool
priv mut tls_active : Bool
priv mut retry : RetryPolicy?
}
///|
pub fn[T] Session::new(config : LdapConfig, transport : T) -> Session[T] {
{
config,
transport,
next_id: 1,
bound: false,
tls_active: false,
retry: None,
}
}
///|
pub fn[T] Session::is_bound(self : Session[T]) -> Bool {
self.bound
}
///|
/// Establish the underlying connection.
pub async fn[T : LdapTransport] Session::connect(
self : Session[T],
) -> Result[Unit, LdapError] {
self.transport.connect(self.config)
}
///|
/// Allocate and consume the next message ID.
fn[T] Session::next_message_id(self : Session[T]) -> Int {
let id = self.next_id
self.next_id = self.next_id + 1
id
}
///|
/// Send a request and read one matching response, validating the message ID.
/// When a retry policy is configured, transport-level failures trigger a
/// reconnect and resend (up to `max_retries` times, waiting `backoff_ms`
/// between attempts).
async fn[T : LdapTransport] Session::round_trip(
self : Session[T],
request : ProtocolOp,
) -> Result[LdapMessage, LdapError] {
let mut attempt = 0
let id = self.next_message_id()
while true {
match self.round_trip_once(request, id) {
Ok(m) => return Ok(m)
Err(e) => {
let retriable = match e {
Transport(_) | PrematureClose => true
_ => false
}
match self.retry {
Some(policy) if retriable =>
if attempt >= policy.max_retries {
return Err(e)
} else {
attempt = attempt + 1
if policy.backoff_ms > 0 {
@async.sleep(policy.backoff_ms)
}
// Session state is gone with the broken connection: rebind
// is the caller's concern, but the channel itself must be
// re-established.
self.bound = false
self.tls_active = false
let _ = self.transport.connect(self.config)
}
_ => return Err(e)
}
}
}
} nobreak {
abort("unreachable")
}
}
///|
async fn[T : LdapTransport] Session::round_trip_once(
self : Session[T],
request : ProtocolOp,
id : Int,
) -> Result[LdapMessage, LdapError] {
let message = LdapMessage::new(id, request)
let bytes = match encode_message(message) {
Ok(b) => b
Err(e) => return Err(e)
}
match self.transport.write(bytes) {
Ok(_) => ()
Err(e) => return Err(e)
}
let response = match self.transport.read() {
Ok(b) => b
Err(e) => return Err(e)
}
let decoded = match decode_message(response, None) {
Ok(m) => m
Err(e) => return Err(e)
}
if decoded.message_id != id {
return Err(LdapError::InvalidMessageId(decoded.message_id))
}
Ok(decoded)
}
///|
/// Perform a simple bind (RFC 4513 5.2.1). Marks the session as bound on
/// success.
pub async fn[T : LdapTransport] Session::bind_simple(
self : Session[T],
name : String,
password : String,
) -> Result[BindResponse, LdapError] {
self.bind_op(simple_bind_request(name, password))
}
///|
/// Perform a SASL PLAIN bind (RFC 4616). Marks the session as bound on
/// success.
pub async fn[T : LdapTransport] Session::bind_plain(
self : Session[T],
authzid : String,
authcid : String,
password : String,
) -> Result[BindResponse, LdapError] {
self.bind_op(sasl_plain_bind_request("", authzid, authcid, password))
}
///|
/// Perform a bind with a custom bind request.
pub async fn[T : LdapTransport] Session::bind(
self : Session[T],
request : BindRequest,
) -> Result[BindResponse, LdapError] {
self.bind_op(request)
}
///|
async fn[T : LdapTransport] Session::bind_op(
self : Session[T],
request : BindRequest,
) -> Result[BindResponse, LdapError] {
if self.config.require_tls && !self.tls_active {
return Err(LdapError::TlsRequired)
}
let message = match self.round_trip(BindRequest(request)) {
Ok(m) => m
Err(e) => return Err(e)
}
let response = match message.op {
BindResponse(resp) => resp
_ => return Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
self.record_bind_result(response)
Ok(response)
}
///|
fn[T] Session::record_bind_result(
self : Session[T],
response : BindResponse,
) -> Unit {
if response.result.result_code.is_success() {
self.bound = true
}
}
///|
/// Send an unbind request and close the transport. The server does not reply
/// to unbind (RFC 4511 4.3).
pub async fn[T : LdapTransport] Session::unbind(
self : Session[T],
) -> Result[Unit, LdapError] {
let id = self.next_message_id()
let bytes = match encode_message(LdapMessage::new(id, UnbindRequest)) {
Ok(b) => b
Err(e) => return Err(e)
}
match self.transport.write(bytes) {
Ok(_) => ()
Err(e) => return Err(e)
}
self.transport.close()
self.bound = false
Ok(())
}
///|
/// Abandon an outstanding request (RFC 4511 4.11). No response is expected.
pub async fn[T : LdapTransport] Session::abandon(
self : Session[T],
message_id : Int,
) -> Result[Unit, LdapError] {
let id = self.next_message_id()
let bytes = match
encode_message(LdapMessage::new(id, AbandonRequest(message_id))) {
Ok(b) => b
Err(e) => return Err(e)
}
match self.transport.write(bytes) {
Ok(_) => Ok(())
Err(e) => Err(e)
}
}
///|
/// Send one request op and expect a specific response op back, validating
/// the message ID.
async fn[T : LdapTransport] Session::op_round_trip(
self : Session[T],
request : ProtocolOp,
response_tag : Int,
) -> Result[LdapMessage, LdapError] {
let message = match self.round_trip(request) {
Ok(m) => m
Err(e) => return Err(e)
}
if protocol_op_tag(message.op) != response_tag {
return Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
Ok(message)
}
///|
/// Modify an entry (RFC 4511 4.6).
pub async fn[T : LdapTransport] Session::modify(
self : Session[T],
request : ModifyRequest,
) -> Result[ModifyResponse, LdapError] {
let message = match self.op_round_trip(ModifyRequest(request), 7) {
Ok(m) => m
Err(e) => return Err(e)
}
match message.op {
ModifyResponse(resp) => Ok(resp)
_ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
}
///|
/// Add an entry (RFC 4511 4.8).
pub async fn[T : LdapTransport] Session::add(
self : Session[T],
request : AddRequest,
) -> Result[AddResponse, LdapError] {
let message = match self.op_round_trip(AddRequest(request), 9) {
Ok(m) => m
Err(e) => return Err(e)
}
match message.op {
AddResponse(resp) => Ok(resp)
_ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
}
///|
/// Delete an entry (RFC 4511 4.10).
pub async fn[T : LdapTransport] Session::delete(
self : Session[T],
entry : String,
) -> Result[DelResponse, LdapError] {
let message = match
self.op_round_trip(DelRequest(DelRequest::new(entry)), 11) {
Ok(m) => m
Err(e) => return Err(e)
}
match message.op {
DelResponse(resp) => Ok(resp)
_ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
}
///|
/// Rename or move an entry (RFC 4511 4.9).
pub async fn[T : LdapTransport] Session::modify_dn(
self : Session[T],
request : ModifyDnRequest,
) -> Result[ModifyDnResponse, LdapError] {
let message = match self.op_round_trip(ModifyDnRequest(request), 13) {
Ok(m) => m
Err(e) => return Err(e)
}
match message.op {
ModifyDnResponse(resp) => Ok(resp)
_ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
}
///|
/// Compare an attribute value on an entry (RFC 4511 4.11). The assertion
/// outcome is carried in the result code (compareTrue / compareFalse).
pub async fn[T : LdapTransport] Session::compare(
self : Session[T],
request : CompareRequest,
) -> Result[CompareResponse, LdapError] {
let message = match self.op_round_trip(CompareRequest(request), 15) {
Ok(m) => m
Err(e) => return Err(e)
}
match message.op {
CompareResponse(resp) => Ok(resp)
_ => Err(LdapError::UnexpectedOp(protocol_op_tag(message.op)))
}
}