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