feat: Initial implementation (bind/search/whoami)
This commit is contained in:
commit
d42508b15e
14 changed files with 2325 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/target
|
||||||
1164
Cargo.lock
generated
Normal file
1164
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
18
Cargo.toml
Normal file
18
Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "multildap"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1.0.104"
|
||||||
|
clap = { version = "4.6.6", features = ["derive"] }
|
||||||
|
futures-util = { version = "^0.3.32", features = [ "sink" ] }
|
||||||
|
indexmap = "2.14.0"
|
||||||
|
ldap3 = { version = "0.12.1", default-features = false }
|
||||||
|
ldap3_proto = { version = "0.8.0", features = ["serde"] }
|
||||||
|
log = "0.4.33"
|
||||||
|
pretty_env_logger = "0.5.0"
|
||||||
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
tokio = { version = "1.53.1", features = ["net", "rt", "macros", "time", "io-util", "fs"] }
|
||||||
|
tokio-util = "0.7.19"
|
||||||
|
toml = "1.1.4"
|
||||||
327
LICENSE.md
Normal file
327
LICENSE.md
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
# Mozilla Public License Version 2.0
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation
|
||||||
|
of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a
|
||||||
|
Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution" means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the
|
||||||
|
notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source
|
||||||
|
Code Form, in each case including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses" means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form" means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate
|
||||||
|
file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License" means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the
|
||||||
|
time of the initial grant or subsequently, any and all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications" means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation,
|
||||||
|
method, process, and apparatus claims, in any patent Licensable by such Contributor that would be
|
||||||
|
infringed, but for the grant of the License, by the making, using, selling, offering for sale,
|
||||||
|
having made, import, or transfer of either its Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser
|
||||||
|
General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any
|
||||||
|
later versions of those licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form" means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License.
|
||||||
|
For legal entities, "You" includes any entity that controls, is controlled by, or is under common
|
||||||
|
control with You. For purposes of this definition, "control" means (a) the power, direct or
|
||||||
|
indirect, to cause the direction or management of such entity, whether by contract or otherwise, or
|
||||||
|
(b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of
|
||||||
|
such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark) Licensable by such
|
||||||
|
Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise
|
||||||
|
exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger
|
||||||
|
Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import,
|
||||||
|
and otherwise transfer either its Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution become effective for each
|
||||||
|
Contribution on the date the Contributor first distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under this License. No additional
|
||||||
|
rights or licenses will be implied from the distribution or licensing of Covered Software under this
|
||||||
|
License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software; or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's modifications of Covered
|
||||||
|
Software, or (ii) the combination of its Contributions with other software (except as part of its
|
||||||
|
Contributor Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks, or logos of any Contributor
|
||||||
|
(except as may be necessary to comply with the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to distribute the Covered Software
|
||||||
|
under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary
|
||||||
|
License (if permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its Contributions are its original
|
||||||
|
creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this
|
||||||
|
License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under applicable copyright doctrines of
|
||||||
|
fair use, fair dealing, or other equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any Modifications that You
|
||||||
|
create or to which You contribute, must be under the terms of this License. You must inform
|
||||||
|
recipients that the Source Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not attempt to alter or restrict
|
||||||
|
the recipients' rights in the Source Code Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code Form, as described in Section
|
||||||
|
3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source
|
||||||
|
Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution
|
||||||
|
to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this License, or sublicense it under
|
||||||
|
different terms, provided that the license for the Executable Form does not attempt to limit or
|
||||||
|
alter the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice, provided that You also
|
||||||
|
comply with the requirements of this License for the Covered Software. If the Larger Work is a
|
||||||
|
combination of Covered Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this License permits You to
|
||||||
|
additionally distribute such Covered Software under the terms of such Secondary License(s), so that
|
||||||
|
the recipient of the Larger Work may, at their option, further distribute the Covered Software under
|
||||||
|
the terms of either this License or such Secondary License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices (including copyright notices,
|
||||||
|
patent notices, disclaimers of warranty, or limitations of liability) contained within the Source
|
||||||
|
Code Form of the Covered Software, except that You may alter any license notices to the extent
|
||||||
|
required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability
|
||||||
|
obligations to one or more recipients of Covered Software. However, You may do so only on Your own
|
||||||
|
behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such
|
||||||
|
warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree
|
||||||
|
to indemnify every Contributor for any liability incurred by such Contributor as a result of
|
||||||
|
warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of
|
||||||
|
warranty and limitations of liability specific to any jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this License with respect to some or
|
||||||
|
all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply
|
||||||
|
with the terms of this License to the maximum extent possible; and (b) describe the limitations and
|
||||||
|
the code they affect. Such description must be placed in a text file included with all distributions
|
||||||
|
of the Covered Software under this License. Except to the extent prohibited by statute or
|
||||||
|
regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be
|
||||||
|
able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically if You fail to comply with
|
||||||
|
any of its terms. However, if You become compliant, then the rights granted under this License from
|
||||||
|
a particular Contributor are reinstated (a) provisionally, unless and until such Contributor
|
||||||
|
explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
|
||||||
|
fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an
|
||||||
|
ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this
|
||||||
|
is the first time You have received notice of non-compliance with this License from such
|
||||||
|
Contributor, and You become compliant prior to 30 days after Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent infringement claim
|
||||||
|
(excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a
|
||||||
|
Contributor Version directly or indirectly infringes any patent, then the rights granted to You by
|
||||||
|
any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements
|
||||||
|
(excluding distributors and resellers) which have been validly granted by You or Your distributors
|
||||||
|
under this License prior to termination shall survive termination.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-
|
||||||
|
6. Disclaimer of Warranty *
|
||||||
|
- ------------------------- *
|
||||||
|
-
|
||||||
|
-
|
||||||
|
- Covered Software is provided under this License on an "as is" *
|
||||||
|
- basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
- statutory, including, without limitation, warranties that the *
|
||||||
|
- Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
- particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
- quality and performance of the Covered Software is with You. *
|
||||||
|
- Should any Covered Software prove defective in any respect, You *
|
||||||
|
- (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
- repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
- essential part of this License. No use of any Covered Software is *
|
||||||
|
- authorized under this License except under this disclaimer. *
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
-
|
||||||
|
-
|
||||||
|
-
|
||||||
|
7. Limitation of Liability *
|
||||||
|
- -------------------------- *
|
||||||
|
-
|
||||||
|
-
|
||||||
|
- Under no circumstances and under no legal theory, whether tort *
|
||||||
|
- (including negligence), contract, or otherwise, shall any *
|
||||||
|
- Contributor, or anyone who distributes Covered Software as *
|
||||||
|
- permitted above, be liable to You for any direct, indirect, *
|
||||||
|
- special, incidental, or consequential damages of any character *
|
||||||
|
- including, without limitation, damages for lost profits, loss of *
|
||||||
|
- goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
- and all other commercial damages or losses, even if such party *
|
||||||
|
- shall have been informed of the possibility of such damages. This *
|
||||||
|
- limitation of liability shall not apply to liability for death or *
|
||||||
|
- personal injury resulting from such party's negligence to the *
|
||||||
|
- extent applicable law prohibits such limitation. Some *
|
||||||
|
- jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
- incidental or consequential damages, so this exclusion and *
|
||||||
|
- limitation may not apply to You. *
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the courts of a jurisdiction where
|
||||||
|
the defendant maintains its principal place of business and such litigation shall be governed by
|
||||||
|
laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this
|
||||||
|
Section shall prevent a party's ability to bring cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject matter hereof. If any
|
||||||
|
provision of this License is held to be unenforceable, such provision shall be reformed only to the
|
||||||
|
extent necessary to make it enforceable. Any law or regulation which provides that the language of a
|
||||||
|
contract shall be construed against the drafter shall not be used to construe this License against a
|
||||||
|
Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the
|
||||||
|
license steward has the right to modify or publish new versions of this License. Each version will
|
||||||
|
be given a distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version of the License under which
|
||||||
|
You originally received the Covered Software, or under the terms of any subsequent version published
|
||||||
|
by the license steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to create a new license for such
|
||||||
|
software, you may create and use a modified version of this License if you rename the license and
|
||||||
|
remove any references to the name of the license steward (except to note that such modified license
|
||||||
|
differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the
|
||||||
|
terms of this version of the License, the notice described in Exhibit B of this License must be
|
||||||
|
attached.
|
||||||
|
|
||||||
|
## Exhibit A - Source Code Form License Notice
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of
|
||||||
|
the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular file, then You may include the
|
||||||
|
notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be
|
||||||
|
likely to look for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
## Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public
|
||||||
|
License, v. 2.0.
|
||||||
30
README.md
Normal file
30
README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# multildap
|
||||||
|
|
||||||
|
Proxy LDAP requests to different LDAP servers based on base DN. Based on code from [kanidm/ldap-proxy](https://github.com/kanidm/ldap-proxy/) under the [MPL license](LICENSE.md).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] No TLS setup ; use only in trusted networks
|
||||||
|
- [x] LDAP bind requests
|
||||||
|
- [x] LDAP search requests
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Create a configuration file `config.toml` with the following:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[mapping]]
|
||||||
|
from = "a.localhost"
|
||||||
|
to = "example.com"
|
||||||
|
backend = "127.0.0.1:4389"
|
||||||
|
[[mapping]]
|
||||||
|
from = "b.localhost"
|
||||||
|
to = "example.com"
|
||||||
|
backend = "127.0.0.1:5389"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```
|
||||||
|
cargo run -- --config config.toml
|
||||||
|
```
|
||||||
11
src/cli.rs
Normal file
11
src/cli.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(version, about, long_about = None)]
|
||||||
|
pub struct Cli {
|
||||||
|
/// Sets a custom config file
|
||||||
|
#[arg(short, long, value_name = "FILE")]
|
||||||
|
pub config: PathBuf,
|
||||||
|
}
|
||||||
176
src/client.rs
Normal file
176
src/client.rs
Normal file
|
|
@ -0,0 +1,176 @@
|
||||||
|
use futures_util::sink::SinkExt;
|
||||||
|
use futures_util::stream::StreamExt;
|
||||||
|
use ldap3_proto::LdapCodec;
|
||||||
|
use ldap3_proto::control::LdapControl;
|
||||||
|
use ldap3_proto::proto::*;
|
||||||
|
use tokio::net::TcpStream;
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::{CR, CW, LdapError};
|
||||||
|
|
||||||
|
pub struct BasicLdapClient {
|
||||||
|
r: FramedRead<CR, LdapCodec>,
|
||||||
|
w: FramedWrite<CW, LdapCodec>,
|
||||||
|
msg_counter: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasicLdapClient {
|
||||||
|
fn next_msgid(&mut self) -> i32 {
|
||||||
|
self.msg_counter += 1;
|
||||||
|
self.msg_counter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build(addr: &str) -> Result<Self, LdapError> {
|
||||||
|
let tcpstream = match timeout(Duration::from_secs(1), TcpStream::connect(addr)).await {
|
||||||
|
Ok(Ok(t)) => {
|
||||||
|
trace!("connection established to {addr}");
|
||||||
|
t
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
// trace!(?addr, ?err, "error");
|
||||||
|
error!("error to {addr}: {err}");
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!("timeout to {addr}");
|
||||||
|
panic!();
|
||||||
|
// continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let (r, w) = tokio::io::split(tcpstream);
|
||||||
|
|
||||||
|
let w = FramedWrite::new(w, LdapCodec::new(None, None));
|
||||||
|
let r = FramedRead::new(r, LdapCodec::new(None, None));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
r,
|
||||||
|
w,
|
||||||
|
msg_counter: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bind(
|
||||||
|
&mut self,
|
||||||
|
lbr: LdapBindRequest,
|
||||||
|
ctrl: Vec<LdapControl>,
|
||||||
|
) -> Result<(LdapBindResponse, Vec<LdapControl>), LdapError> {
|
||||||
|
let ck_msgid = self.next_msgid();
|
||||||
|
|
||||||
|
let msg = LdapMsg {
|
||||||
|
msgid: ck_msgid,
|
||||||
|
op: LdapOp::BindRequest(lbr),
|
||||||
|
ctrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.w.send(msg).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("unable to transmit to ldap server: {err}");
|
||||||
|
return Err(LdapError::Transport);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.r.next().await {
|
||||||
|
Some(Ok(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::BindResponse(bind_resp),
|
||||||
|
ctrl,
|
||||||
|
})) => {
|
||||||
|
if msgid == ck_msgid {
|
||||||
|
Ok((bind_resp, ctrl))
|
||||||
|
} else {
|
||||||
|
error!("invalid msgid, sequence error.");
|
||||||
|
Err(LdapError::InvalidProtocolState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(msg)) => {
|
||||||
|
trace!("{:?}", msg);
|
||||||
|
Err(LdapError::InvalidProtocolState)
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("unable to receive from ldap server: {e}");
|
||||||
|
Err(LdapError::Transport)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!("connection closed");
|
||||||
|
Err(LdapError::Transport)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search(
|
||||||
|
&mut self,
|
||||||
|
sr: LdapSearchRequest,
|
||||||
|
ctrl: Vec<LdapControl>,
|
||||||
|
) -> Result<
|
||||||
|
(
|
||||||
|
Vec<(LdapSearchResultEntry, Vec<LdapControl>)>,
|
||||||
|
LdapResult,
|
||||||
|
Vec<LdapControl>,
|
||||||
|
),
|
||||||
|
LdapError,
|
||||||
|
> {
|
||||||
|
let ck_msgid = self.next_msgid();
|
||||||
|
|
||||||
|
let msg = LdapMsg {
|
||||||
|
msgid: ck_msgid,
|
||||||
|
op: LdapOp::SearchRequest(sr),
|
||||||
|
ctrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.w.send(msg).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("unable to transmit to ldap server: {err}");
|
||||||
|
return Err(LdapError::Transport);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
loop {
|
||||||
|
match self.r.next().await {
|
||||||
|
// This terminates the iteration of entries.
|
||||||
|
Some(Ok(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchResultDone(search_res),
|
||||||
|
ctrl,
|
||||||
|
})) => {
|
||||||
|
if msgid == ck_msgid {
|
||||||
|
break Ok((entries, search_res, ctrl));
|
||||||
|
} else {
|
||||||
|
error!("invalid msgid, sequence error.");
|
||||||
|
break Err(LdapError::InvalidProtocolState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchResultEntry(search_entry),
|
||||||
|
ctrl,
|
||||||
|
})) => {
|
||||||
|
if msgid == ck_msgid {
|
||||||
|
entries.push((search_entry, ctrl))
|
||||||
|
} else {
|
||||||
|
error!("invalid msgid, sequence error.");
|
||||||
|
break Err(LdapError::InvalidProtocolState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Ok(msg)) => {
|
||||||
|
trace!("{:?}", msg);
|
||||||
|
break Err(LdapError::InvalidProtocolState);
|
||||||
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
error!("unable to receive from ldap server: {e}");
|
||||||
|
break Err(LdapError::Transport);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!("connection closed");
|
||||||
|
break Err(LdapError::Transport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/config.rs
Normal file
22
src/config.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Config {
|
||||||
|
pub mapping: Vec<Mapping>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub async fn from_path(path: &Path) -> anyhow::Result<Self> {
|
||||||
|
let content = tokio::fs::read(path).await?;
|
||||||
|
Ok(toml::from_slice(&content)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
pub struct Mapping {
|
||||||
|
pub from: String,
|
||||||
|
pub to: String,
|
||||||
|
pub backend: String,
|
||||||
|
}
|
||||||
100
src/dn.rs
Normal file
100
src/dn.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use ldap3::dn_escape;
|
||||||
|
|
||||||
|
use crate::LdapError;
|
||||||
|
|
||||||
|
/// A Dn is a key-value mapping which can contain the same key several times.
|
||||||
|
///
|
||||||
|
/// This implementation is not fully RFC compliant and will only parse simple DNs for
|
||||||
|
/// basic attribute manipulation.
|
||||||
|
///
|
||||||
|
/// Keys are lowercased, but scrambled keys in a broken order will be reordered. For example,
|
||||||
|
/// `dc=example,ou=people,dc=com` will become `dc=example,dc=com,ou=people`.
|
||||||
|
pub struct Dn {
|
||||||
|
pub keys: IndexMap<String, Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dn {
|
||||||
|
/// Parse a DN string. Here parsing escaped characters is not critical, if a
|
||||||
|
/// client sends us funny characters, the domain name simply won't match
|
||||||
|
/// and their request won't go anywhere.
|
||||||
|
///
|
||||||
|
/// However, if we find a really funny request such as `dc=foo=bar`, then
|
||||||
|
/// we return an error to the client.
|
||||||
|
pub fn from_dn_str(input: &str) -> Result<Self, LdapError> {
|
||||||
|
let mut keys: IndexMap<String, Vec<String>> = IndexMap::new();
|
||||||
|
|
||||||
|
for query in input.split(',') {
|
||||||
|
let mut query_parts = query.split('=');
|
||||||
|
// Here we have key=val pairs
|
||||||
|
if query_parts.clone().count() != 2 {
|
||||||
|
// Bad request
|
||||||
|
log::debug!("Invalid query DN: {input}");
|
||||||
|
return Err(LdapError::InvalidQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = query_parts.next().unwrap();
|
||||||
|
let val = query_parts.next().unwrap();
|
||||||
|
if let Some(previous) = keys.get_mut(key) {
|
||||||
|
previous.push(val.to_string());
|
||||||
|
} else {
|
||||||
|
keys.insert(key.to_string(), vec![val.to_string()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_dn_string(&self) -> String {
|
||||||
|
let mut s = String::new();
|
||||||
|
let mut first = true;
|
||||||
|
for (key, values) in &self.keys {
|
||||||
|
for value in values {
|
||||||
|
if first {
|
||||||
|
first = false;
|
||||||
|
} else {
|
||||||
|
s.push(',');
|
||||||
|
}
|
||||||
|
s.push_str(key);
|
||||||
|
s.push('=');
|
||||||
|
s.push_str(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the hostname defined in the `dc` fields of the DN.
|
||||||
|
///
|
||||||
|
/// For example, `dc=example,dc=com` becomes `Some(example.com)`.
|
||||||
|
///
|
||||||
|
/// The returned domain is not normalized and may require casing treatment
|
||||||
|
/// to compare meaningfully.
|
||||||
|
pub fn get_hostname(&self) -> Option<String> {
|
||||||
|
let domain_components = self.keys.get("dc")?;
|
||||||
|
|
||||||
|
// We don't populate the dc key if there was no value at all, so
|
||||||
|
// we have at least one component.
|
||||||
|
let mut domain_components = domain_components.iter();
|
||||||
|
let mut s = String::from(domain_components.next().unwrap());
|
||||||
|
for domain_component in domain_components {
|
||||||
|
s.push('.');
|
||||||
|
s.push_str(domain_component);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Overrides the DN hostname (`dc` fields) with the provided host.
|
||||||
|
///
|
||||||
|
/// When no `dc` fields are present, they are only added when `force` is true.
|
||||||
|
pub fn set_hostname(&mut self, host: &str, force: bool) {
|
||||||
|
let domain_components: Vec<String> =
|
||||||
|
host.split('.').map(|x| dn_escape(x).to_string()).collect();
|
||||||
|
if !force && !self.keys.contains_key("dc") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.keys.insert("dc".to_string(), domain_components);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
src/main.rs
Normal file
232
src/main.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
#[macro_use]
|
||||||
|
extern crate log;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use futures_util::StreamExt;
|
||||||
|
use ldap3_proto::LdapCodec;
|
||||||
|
use ldap3_proto::proto::*;
|
||||||
|
use tokio::io::{AsyncRead, AsyncWrite, ReadHalf, WriteHalf};
|
||||||
|
use tokio::net::{TcpListener, TcpStream};
|
||||||
|
use tokio::time::timeout;
|
||||||
|
use tokio_util::codec::{FramedRead, FramedWrite};
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
use cli::Cli;
|
||||||
|
mod client;
|
||||||
|
use crate::client::BasicLdapClient;
|
||||||
|
mod config;
|
||||||
|
use config::Config;
|
||||||
|
mod dn;
|
||||||
|
mod op;
|
||||||
|
use crate::dn::Dn;
|
||||||
|
|
||||||
|
const LDAP_CLIENT_IO_TIMEOUT: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
type CR = ReadHalf<TcpStream>;
|
||||||
|
type CW = WriteHalf<TcpStream>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LdapError {
|
||||||
|
TlsError,
|
||||||
|
ConnectError,
|
||||||
|
Transport,
|
||||||
|
InvalidProtocolState,
|
||||||
|
InvalidQuery,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We allow the large enum to exist as we always do a mem swap from unbound to authenticated, so
|
||||||
|
// the memory layout penalty doesn't apply.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
enum ClientState {
|
||||||
|
Unbound,
|
||||||
|
Authenticated {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
request_dn: String,
|
||||||
|
backend_dn: String,
|
||||||
|
client: BasicLdapClient,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn client_process<W: AsyncWrite + Unpin, R: AsyncRead + Unpin>(
|
||||||
|
mut r: FramedRead<R, LdapCodec>,
|
||||||
|
mut w: FramedWrite<W, LdapCodec>,
|
||||||
|
config: Arc<Config>,
|
||||||
|
) {
|
||||||
|
info!("Received new connection");
|
||||||
|
|
||||||
|
// We always start unbound.
|
||||||
|
let mut state = ClientState::Unbound;
|
||||||
|
|
||||||
|
// Start to wait for incoming packets
|
||||||
|
while let Ok(Some(Ok(protomsg))) = timeout(LDAP_CLIENT_IO_TIMEOUT, r.next()).await {
|
||||||
|
let next_state = match (&mut state, protomsg) {
|
||||||
|
// Doesn't matter what state we are in, any bind will trigger this process.
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::BindRequest(lbr),
|
||||||
|
ctrl,
|
||||||
|
},
|
||||||
|
) => match op::bind::bind(&mut w, lbr, config.clone(), msgid, ctrl).await {
|
||||||
|
Ok(ns) => ns,
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
// Unbinds are always actioned.
|
||||||
|
(
|
||||||
|
_,
|
||||||
|
LdapMsg {
|
||||||
|
msgid: _,
|
||||||
|
op: LdapOp::UnbindRequest,
|
||||||
|
ctrl: _,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Unbound handler
|
||||||
|
(
|
||||||
|
ClientState::Unbound,
|
||||||
|
LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchRequest(sr),
|
||||||
|
ctrl,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
// We have to trigger a bind first in case we have a mapping.
|
||||||
|
let lbr = LdapBindRequest {
|
||||||
|
dn: "".to_string(),
|
||||||
|
cred: LdapBindCred::Simple("".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut next_state =
|
||||||
|
match op::bind::bind(&mut w, lbr, config.clone(), 0, Vec::default()).await {
|
||||||
|
Ok(ns) => ns,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
match &mut next_state {
|
||||||
|
Some(ClientState::Unbound) | None => {
|
||||||
|
error!("Invalid state, bind did not return an authenticated state!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some(ClientState::Authenticated {
|
||||||
|
client,
|
||||||
|
request_dn: _,
|
||||||
|
backend_dn: _,
|
||||||
|
}) => {
|
||||||
|
let search_req = op::search::SearchRequest {
|
||||||
|
sr,
|
||||||
|
msgid,
|
||||||
|
ctrl,
|
||||||
|
client,
|
||||||
|
};
|
||||||
|
match op::search::search(&mut w, search_req).await {
|
||||||
|
Ok(()) => {}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next_state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticated message handler.
|
||||||
|
// - Search
|
||||||
|
(
|
||||||
|
ClientState::Authenticated {
|
||||||
|
client,
|
||||||
|
backend_dn: _,
|
||||||
|
request_dn: _,
|
||||||
|
},
|
||||||
|
LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchRequest(sr),
|
||||||
|
ctrl,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let search_req = op::search::SearchRequest {
|
||||||
|
sr,
|
||||||
|
msgid,
|
||||||
|
ctrl,
|
||||||
|
client,
|
||||||
|
};
|
||||||
|
|
||||||
|
match op::search::search(&mut w, search_req).await {
|
||||||
|
Ok(()) => None,
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Extended Requests - Generally whoami.
|
||||||
|
(
|
||||||
|
ClientState::Authenticated {
|
||||||
|
request_dn: _,
|
||||||
|
backend_dn,
|
||||||
|
client: _,
|
||||||
|
},
|
||||||
|
LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::ExtendedRequest(ler),
|
||||||
|
ctrl: _,
|
||||||
|
},
|
||||||
|
) => match op::ext::extop(&mut w, ler, msgid, backend_dn).await {
|
||||||
|
Ok(ns) => ns,
|
||||||
|
Err(_) => break,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
log::debug!("unimplemented");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(next_state) = next_state {
|
||||||
|
// Update the client state, dropping any former state.
|
||||||
|
state = next_state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
if let Err(_) = std::env::var("RUST_LOG") {
|
||||||
|
unsafe { std::env::set_var("RUST_LOG", "info"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
pretty_env_logger::formatted_timed_builder()
|
||||||
|
.parse_default_env()
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let config = match Config::from_path(&cli.config).await {
|
||||||
|
Ok(config) => config,
|
||||||
|
Err(e) => {
|
||||||
|
error!(
|
||||||
|
"Loading configuration file {} failed!",
|
||||||
|
cli.config.display()
|
||||||
|
);
|
||||||
|
error!("{}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = Arc::new(config);
|
||||||
|
|
||||||
|
// Let the listening port ready.
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:3389").await.unwrap();
|
||||||
|
info!("Listening on {:?}", listener);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match listener.accept().await {
|
||||||
|
Ok((tcpstream, client_socket_addr)) => {
|
||||||
|
log::debug!("New connection from {client_socket_addr}");
|
||||||
|
let (r, w) = tokio::io::split(tcpstream);
|
||||||
|
let r = FramedRead::new(r, LdapCodec::new(None, None));
|
||||||
|
let w = FramedWrite::new(w, LdapCodec::new(None, None));
|
||||||
|
tokio::spawn(client_process(r, w, state.clone()));
|
||||||
|
}
|
||||||
|
Err(_e) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/op/bind.rs
Normal file
120
src/op/bind.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
use futures_util::SinkExt;
|
||||||
|
use ldap3_proto::LdapCodec;
|
||||||
|
use ldap3_proto::control::*;
|
||||||
|
use ldap3_proto::proto::*;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
use tokio_util::codec::FramedWrite;
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::{BasicLdapClient, ClientState, Config, Dn, LdapError};
|
||||||
|
|
||||||
|
pub fn bind_operror(msgid: i32, msg: &str) -> LdapMsg {
|
||||||
|
LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::BindResponse(LdapBindResponse {
|
||||||
|
res: LdapResult {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message: msg.to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
},
|
||||||
|
saslcreds: None,
|
||||||
|
}),
|
||||||
|
ctrl: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bind<W: AsyncWrite + Unpin>(
|
||||||
|
w: &mut FramedWrite<W, LdapCodec>,
|
||||||
|
mut lbr: LdapBindRequest,
|
||||||
|
config: Arc<Config>,
|
||||||
|
msgid: i32,
|
||||||
|
ctrl: Vec<LdapControl>,
|
||||||
|
) -> Result<Option<ClientState>, LdapError> {
|
||||||
|
trace!("{:?}", lbr);
|
||||||
|
|
||||||
|
let request_dn = lbr.dn.clone();
|
||||||
|
|
||||||
|
debug!("Received bind request on DN: {}", lbr.dn);
|
||||||
|
let mut dn = Dn::from_dn_str(&lbr.dn)?;
|
||||||
|
|
||||||
|
let Some(requested_domain) = dn.get_hostname() else {
|
||||||
|
debug!("No domain name CN found in DN: {}", lbr.dn);
|
||||||
|
return Err(LdapError::InvalidQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lowercase the domain systematically to allow matches
|
||||||
|
let requested_domain = requested_domain.to_lowercase();
|
||||||
|
|
||||||
|
let Some(mapping) = config
|
||||||
|
.mapping
|
||||||
|
.iter()
|
||||||
|
.find(|x| x.from.to_lowercase() == requested_domain)
|
||||||
|
else {
|
||||||
|
debug!("No mapping found for domain {requested_domain}");
|
||||||
|
return Err(LdapError::InvalidQuery);
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"Redirecting {} to {} with domain {}",
|
||||||
|
requested_domain, mapping.backend, mapping.to
|
||||||
|
);
|
||||||
|
dn.set_hostname(&mapping.to, false);
|
||||||
|
lbr.dn = dn.to_dn_string();
|
||||||
|
let dn = lbr.dn.clone();
|
||||||
|
|
||||||
|
// We need the client to connect *and* bind to proceed here!
|
||||||
|
let mut client = match BasicLdapClient::build(&mapping.backend).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
error!("A client build error has occurred: {e:?}");
|
||||||
|
let resp_msg = bind_operror(msgid, "unable to bind");
|
||||||
|
w.send(resp_msg).await.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
// Always bail.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid = match client.bind(lbr, ctrl).await {
|
||||||
|
Ok((bind_resp, ctrl)) => {
|
||||||
|
// Almost there, lets check the bind result.
|
||||||
|
let valid = bind_resp.res.code == LdapResultCode::Success;
|
||||||
|
|
||||||
|
let resp_msg = LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::BindResponse(bind_resp),
|
||||||
|
ctrl,
|
||||||
|
};
|
||||||
|
w.send(resp_msg).await.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("A client bind error has occurred: {e:?}");
|
||||||
|
let resp_msg = bind_operror(msgid, "unable to bind");
|
||||||
|
w.send(resp_msg).await.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
// Always bail.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if valid {
|
||||||
|
info!("Successful bind for {}", dn);
|
||||||
|
Ok(Some(ClientState::Authenticated {
|
||||||
|
request_dn,
|
||||||
|
backend_dn: dn,
|
||||||
|
client,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/op/ext.rs
Normal file
51
src/op/ext.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use futures_util::SinkExt;
|
||||||
|
use ldap3_proto::LdapCodec;
|
||||||
|
use ldap3_proto::proto::*;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
use tokio_util::codec::FramedWrite;
|
||||||
|
|
||||||
|
use crate::{ClientState, LdapError};
|
||||||
|
|
||||||
|
pub async fn extop<W: AsyncWrite + Unpin>(
|
||||||
|
w: &mut FramedWrite<W, LdapCodec>,
|
||||||
|
ler: LdapExtendedRequest,
|
||||||
|
msgid: i32,
|
||||||
|
|
||||||
|
display_dn: &str,
|
||||||
|
) -> Result<Option<ClientState>, LdapError> {
|
||||||
|
let op = match ler.name.as_str() {
|
||||||
|
"1.3.6.1.4.1.4203.1.11.3" => LdapOp::ExtendedResponse(LdapExtendedResponse {
|
||||||
|
res: LdapResult {
|
||||||
|
code: LdapResultCode::Success,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
},
|
||||||
|
name: None,
|
||||||
|
value: Some(Vec::from(display_dn)),
|
||||||
|
}),
|
||||||
|
_ => LdapOp::ExtendedResponse(LdapExtendedResponse {
|
||||||
|
res: LdapResult {
|
||||||
|
code: LdapResultCode::OperationsError,
|
||||||
|
matcheddn: "".to_string(),
|
||||||
|
message: "".to_string(),
|
||||||
|
referral: vec![],
|
||||||
|
},
|
||||||
|
name: None,
|
||||||
|
value: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
w.send(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op,
|
||||||
|
ctrl: vec![],
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
3
src/op/mod.rs
Normal file
3
src/op/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod bind;
|
||||||
|
pub mod ext;
|
||||||
|
pub mod search;
|
||||||
70
src/op/search.rs
Normal file
70
src/op/search.rs
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
use futures_util::SinkExt;
|
||||||
|
use ldap3_proto::LdapCodec;
|
||||||
|
use ldap3_proto::control::*;
|
||||||
|
use ldap3_proto::proto::*;
|
||||||
|
use tokio::io::AsyncWrite;
|
||||||
|
use tokio_util::codec::FramedWrite;
|
||||||
|
|
||||||
|
use crate::op::bind::bind_operror;
|
||||||
|
use crate::{BasicLdapClient, LdapError};
|
||||||
|
|
||||||
|
pub struct SearchRequest<'a> {
|
||||||
|
pub sr: LdapSearchRequest,
|
||||||
|
pub msgid: i32,
|
||||||
|
pub ctrl: Vec<LdapControl>,
|
||||||
|
pub client: &'a mut BasicLdapClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search<W: AsyncWrite + Unpin>(
|
||||||
|
w: &mut FramedWrite<W, LdapCodec>,
|
||||||
|
search_request: SearchRequest<'_>,
|
||||||
|
) -> Result<(), LdapError> {
|
||||||
|
let SearchRequest {
|
||||||
|
sr,
|
||||||
|
msgid,
|
||||||
|
ctrl,
|
||||||
|
client,
|
||||||
|
} = search_request;
|
||||||
|
|
||||||
|
let (entries, result, ctrl) = match client.search(sr, ctrl).await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(e) => {
|
||||||
|
error!("A client search error has occurred: {e:?}");
|
||||||
|
let resp_msg = bind_operror(msgid, "unable to search");
|
||||||
|
w.send(resp_msg).await.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Error sent, return with no state change.
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (entry, ctrl) in entries {
|
||||||
|
w.send(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchResultEntry(entry),
|
||||||
|
ctrl,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.send(LdapMsg {
|
||||||
|
msgid,
|
||||||
|
op: LdapOp::SearchResultDone(result),
|
||||||
|
ctrl,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
error!("Unable to send response: {err}");
|
||||||
|
LdapError::Transport
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// No state change
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue