Compare commits
2 Commits
7ee40b7dbe
...
9e3d4c5fe3
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e3d4c5fe3 | |||
| 857b9171af |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
# Experiments with certificates
|
# Experiments with certificates
|
||||||
/*.pem
|
certs
|
||||||
1070
Cargo.lock
generated
1070
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
|
"bonknet_broker",
|
||||||
"bonknet_client",
|
"bonknet_client",
|
||||||
"bonknet_server",
|
"bonknet_server",
|
||||||
"libbonknet",
|
"libbonknet",
|
||||||
|
|||||||
27
bonknet_broker/Cargo.toml
Normal file
27
bonknet_broker/Cargo.toml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "bonknet_broker"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
default-run = "bonknet_broker"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libbonknet = { path = "../libbonknet" }
|
||||||
|
actix = "0.13.3"
|
||||||
|
actix-rt = "2.9.0"
|
||||||
|
actix-server = "2.3.0"
|
||||||
|
actix-service = "2.0.2"
|
||||||
|
actix-tls = { version = "3.3.0", features = ["rustls-0_22"] }
|
||||||
|
rustls = "0.22.2"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
||||||
|
futures = "0.3"
|
||||||
|
thiserror = "1.0.56"
|
||||||
|
tokio-util = { version = "0.7.10", features = ["codec"] }
|
||||||
|
rmp-serde = "1.1.2"
|
||||||
|
rcgen = { version = "0.12.1", features = ["x509-parser"] }
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "init_certs"
|
||||||
|
path = "src/bin/init_certs.rs"
|
||||||
@@ -14,13 +14,35 @@ fn server_root_cert() -> Certificate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn server_cert() -> Certificate {
|
fn server_cert() -> Certificate {
|
||||||
let mut params = CertificateParams::new(vec!["entity.other.host".into(), "localhost".into()]);
|
let mut params = CertificateParams::new(vec!["entity.other.host".into(), "bonk.server.1".into()]);
|
||||||
params.distinguished_name.push(DnType::CommonName, "localhost");
|
params.distinguished_name.push(DnType::CommonName, "Server 1");
|
||||||
params.use_authority_key_identifier_extension = true;
|
params.use_authority_key_identifier_extension = true;
|
||||||
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
||||||
params
|
params
|
||||||
.extended_key_usages
|
.extended_key_usages
|
||||||
.push(rcgen::ExtendedKeyUsagePurpose::ServerAuth);
|
.push(rcgen::ExtendedKeyUsagePurpose::ClientAuth);
|
||||||
|
Certificate::from_params(params).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guestserver_root_cert() -> Certificate {
|
||||||
|
let subject_alt_names = vec!["hello.world.example".into()];
|
||||||
|
let mut certparams = CertificateParams::new(subject_alt_names);
|
||||||
|
certparams.is_ca = rcgen::IsCa::Ca(BasicConstraints::Unconstrained);
|
||||||
|
let mut distname = rcgen::DistinguishedName::new();
|
||||||
|
distname.push(DnType::OrganizationName, "Eister Corporation");
|
||||||
|
distname.push(DnType::CommonName, "Bonknet Guest Server Root Cert CA");
|
||||||
|
certparams.distinguished_name = distname;
|
||||||
|
Certificate::from_params(certparams).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn guestserver_cert() -> Certificate {
|
||||||
|
let mut params = CertificateParams::new(vec!["entity.other.host".into(), "bonk.guestserver.1".into()]);
|
||||||
|
params.distinguished_name.push(DnType::CommonName, "Guest Server 1");
|
||||||
|
params.use_authority_key_identifier_extension = true;
|
||||||
|
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
||||||
|
params
|
||||||
|
.extended_key_usages
|
||||||
|
.push(rcgen::ExtendedKeyUsagePurpose::ClientAuth);
|
||||||
Certificate::from_params(params).unwrap()
|
Certificate::from_params(params).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,25 +68,25 @@ fn client_cert() -> Certificate {
|
|||||||
Certificate::from_params(params).unwrap()
|
Certificate::from_params(params).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requester_root_cert() -> Certificate {
|
fn broker_root_cert() -> Certificate {
|
||||||
let subject_alt_names = vec!["hello.world.example".into()];
|
let subject_alt_names = vec!["hello.world.example".into()];
|
||||||
let mut certparams = CertificateParams::new(subject_alt_names);
|
let mut certparams = CertificateParams::new(subject_alt_names);
|
||||||
certparams.is_ca = rcgen::IsCa::Ca(BasicConstraints::Unconstrained);
|
certparams.is_ca = rcgen::IsCa::Ca(BasicConstraints::Unconstrained);
|
||||||
let mut distname = rcgen::DistinguishedName::new();
|
let mut distname = rcgen::DistinguishedName::new();
|
||||||
distname.push(DnType::OrganizationName, "Eister Corporation");
|
distname.push(DnType::OrganizationName, "Eister Corporation");
|
||||||
distname.push(DnType::CommonName, "Bonknet Requester Root Cert CA");
|
distname.push(DnType::CommonName, "Bonknet Broker Root Cert CA");
|
||||||
certparams.distinguished_name = distname;
|
certparams.distinguished_name = distname;
|
||||||
Certificate::from_params(certparams).unwrap()
|
Certificate::from_params(certparams).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn requester_cert() -> Certificate {
|
fn broker_cert() -> Certificate {
|
||||||
let mut params = CertificateParams::new(vec!["entity.other.host".into(), "bonk.client.1".into()]);
|
let mut params = CertificateParams::new(vec!["entity.other.host".into(), "localhost".into()]);
|
||||||
params.distinguished_name.push(DnType::CommonName, "Requester 1");
|
params.distinguished_name.push(DnType::CommonName, "localhost");
|
||||||
params.use_authority_key_identifier_extension = true;
|
params.use_authority_key_identifier_extension = true;
|
||||||
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
||||||
params
|
params
|
||||||
.extended_key_usages
|
.extended_key_usages
|
||||||
.push(rcgen::ExtendedKeyUsagePurpose::ClientAuth);
|
.push(rcgen::ExtendedKeyUsagePurpose::ServerAuth);
|
||||||
Certificate::from_params(params).unwrap()
|
Certificate::from_params(params).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,51 +95,68 @@ fn main() -> std::io::Result<()> {
|
|||||||
let server_root_cert = server_root_cert();
|
let server_root_cert = server_root_cert();
|
||||||
// The certificate is now valid for localhost and the domain "hello.world.example"
|
// The certificate is now valid for localhost and the domain "hello.world.example"
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("server_root_cert.pem")?;
|
let mut certfile = File::create("certs/server_root_cert.pem")?;
|
||||||
certfile.write_all(server_root_cert.serialize_pem().unwrap().as_bytes())?;
|
certfile.write_all(server_root_cert.serialize_pem().unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("server_root_key.pem")?;
|
let mut privkfile = File::create("certs/server_root_key.pem")?;
|
||||||
privkfile.write_all(server_root_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(server_root_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
// Now create the leaf
|
// Now create the leaf
|
||||||
let server_leaf_cert = server_cert();
|
let server_leaf_cert = server_cert();
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("server_cert.pem")?;
|
let mut certfile = File::create("certs/server_cert.pem")?;
|
||||||
certfile.write_all(server_leaf_cert.serialize_pem_with_signer(&server_root_cert).unwrap().as_bytes())?;
|
certfile.write_all(server_leaf_cert.serialize_pem_with_signer(&server_root_cert).unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("server_key.pem")?;
|
let mut privkfile = File::create("certs/server_key.pem")?;
|
||||||
privkfile.write_all(server_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(server_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
|
// GUESTSERVER
|
||||||
|
let guestserver_root_cert = guestserver_root_cert();
|
||||||
|
// The certificate is now valid for localhost and the domain "hello.world.example"
|
||||||
|
{
|
||||||
|
let mut certfile = File::create("certs/guestserver_root_cert.pem")?;
|
||||||
|
certfile.write_all(guestserver_root_cert.serialize_pem().unwrap().as_bytes())?;
|
||||||
|
let mut privkfile = File::create("certs/guestserver_root_key.pem")?;
|
||||||
|
privkfile.write_all(guestserver_root_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
|
}
|
||||||
|
// Now create the leaf
|
||||||
|
let guestserver_leaf_cert = guestserver_cert();
|
||||||
|
{
|
||||||
|
let mut certfile = File::create("certs/guestserver_cert.pem")?;
|
||||||
|
certfile.write_all(guestserver_leaf_cert.serialize_pem_with_signer(&guestserver_root_cert).unwrap().as_bytes())?;
|
||||||
|
let mut privkfile = File::create("certs/guestserver_key.pem")?;
|
||||||
|
privkfile.write_all(guestserver_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
|
}
|
||||||
// CLIENT
|
// CLIENT
|
||||||
let client_root_cert = client_root_cert();
|
let client_root_cert = client_root_cert();
|
||||||
// The certificate is now valid for localhost and the domain "hello.world.example"
|
// The certificate is now valid for localhost and the domain "hello.world.example"
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("client_root_cert.pem")?;
|
let mut certfile = File::create("certs/client_root_cert.pem")?;
|
||||||
certfile.write_all(client_root_cert.serialize_pem().unwrap().as_bytes())?;
|
certfile.write_all(client_root_cert.serialize_pem().unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("client_root_key.pem")?;
|
let mut privkfile = File::create("certs/client_root_key.pem")?;
|
||||||
privkfile.write_all(client_root_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(client_root_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
// Now create the leaf
|
// Now create the leaf
|
||||||
let client_leaf_cert = client_cert();
|
let client_leaf_cert = client_cert();
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("client_cert.pem")?;
|
let mut certfile = File::create("certs/client_cert.pem")?;
|
||||||
certfile.write_all(client_leaf_cert.serialize_pem_with_signer(&client_root_cert).unwrap().as_bytes())?;
|
certfile.write_all(client_leaf_cert.serialize_pem_with_signer(&client_root_cert).unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("client_key.pem")?;
|
let mut privkfile = File::create("certs/client_key.pem")?;
|
||||||
privkfile.write_all(client_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(client_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
// CLIENT
|
// BROKER
|
||||||
let requester_root_cert = requester_root_cert();
|
let requester_root_cert = broker_root_cert();
|
||||||
// The certificate is now valid for localhost and the domain "hello.world.example"
|
// The certificate is now valid for localhost and the domain "hello.world.example"
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("requester_root_cert.pem")?;
|
let mut certfile = File::create("certs/broker_root_cert.pem")?;
|
||||||
certfile.write_all(requester_root_cert.serialize_pem().unwrap().as_bytes())?;
|
certfile.write_all(requester_root_cert.serialize_pem().unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("requester_root_key.pem")?;
|
let mut privkfile = File::create("certs/broker_root_key.pem")?;
|
||||||
privkfile.write_all(requester_root_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(requester_root_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
// Now create the leaf
|
// Now create the leaf
|
||||||
let requester_leaf_cert = requester_cert();
|
let requester_leaf_cert = broker_cert();
|
||||||
{
|
{
|
||||||
let mut certfile = File::create("requester_cert.pem")?;
|
let mut certfile = File::create("certs/broker_cert.pem")?;
|
||||||
certfile.write_all(requester_leaf_cert.serialize_pem_with_signer(&requester_root_cert).unwrap().as_bytes())?;
|
certfile.write_all(requester_leaf_cert.serialize_pem_with_signer(&requester_root_cert).unwrap().as_bytes())?;
|
||||||
let mut privkfile = File::create("requester_key.pem")?;
|
let mut privkfile = File::create("certs/broker_key.pem")?;
|
||||||
privkfile.write_all(requester_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
privkfile.write_all(requester_leaf_cert.serialize_private_key_pem().as_bytes())?;
|
||||||
}
|
}
|
||||||
println!("Certificates created");
|
println!("Certificates created");
|
||||||
272
bonknet_broker/src/main.rs
Normal file
272
bonknet_broker/src/main.rs
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
use actix::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use libbonknet::{load_cert, load_prkey, FromServerMessage, RequiredReplyValues, FromGuestServerMessage, ToGuestServerMessage};
|
||||||
|
use rustls::{RootCertStore, ServerConfig};
|
||||||
|
use rustls::server::WebPkiClientVerifier;
|
||||||
|
use actix_tls::accept::rustls_0_22::{Acceptor as RustlsAcceptor, TlsStream};
|
||||||
|
use actix_server::Server;
|
||||||
|
use actix_rt::net::TcpStream;
|
||||||
|
use actix_service::{ServiceFactoryExt as _};
|
||||||
|
use futures::{StreamExt, SinkExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||||
|
use tracing::{info, error};
|
||||||
|
use rcgen::{Certificate, CertificateParams, DnType, KeyPair};
|
||||||
|
|
||||||
|
struct ServerCert {
|
||||||
|
cert: Vec<u8>,
|
||||||
|
prkey: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_server_cert(root_cert: &Certificate, name: &str) -> ServerCert {
|
||||||
|
let mut params = CertificateParams::new(vec!["entity.other.host".into(), format!("bonk.server.{name}")]);
|
||||||
|
params.distinguished_name.push(DnType::CommonName, format!("{name}"));
|
||||||
|
params.use_authority_key_identifier_extension = true;
|
||||||
|
params.key_usages.push(rcgen::KeyUsagePurpose::DigitalSignature);
|
||||||
|
params
|
||||||
|
.extended_key_usages
|
||||||
|
.push(rcgen::ExtendedKeyUsagePurpose::ClientAuth);
|
||||||
|
let certificate = Certificate::from_params(params).unwrap();
|
||||||
|
let cert = certificate.serialize_der_with_signer(root_cert).unwrap();
|
||||||
|
let prkey = certificate.serialize_private_key_der();
|
||||||
|
ServerCert { cert, prkey }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
enum DBError {
|
||||||
|
#[error("Certificate is already registered with name {0}")]
|
||||||
|
CertAlreadyRegistered(String),
|
||||||
|
// #[error("Generic Failure")]
|
||||||
|
// GenericFailure,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "bool")]
|
||||||
|
struct IsNameRegistered {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Message)]
|
||||||
|
#[rtype(result = "Result<(), DBError>")]
|
||||||
|
struct RegisterServer {
|
||||||
|
cert: Vec<u8>,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move into Sqlite DB with unique check on col1 and col2!!!! Right now name is not unique
|
||||||
|
struct ServerCertDB {
|
||||||
|
db: HashMap<Vec<u8>, String>, // Cert to Name
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for ServerCertDB {
|
||||||
|
type Context = Context<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<RegisterServer> for ServerCertDB {
|
||||||
|
type Result = Result<(), DBError>;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: RegisterServer, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
match self.db.get(&msg.cert) {
|
||||||
|
None => {
|
||||||
|
self.db.insert(msg.cert, msg.name);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some(name) => {
|
||||||
|
Err(DBError::CertAlreadyRegistered(name.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Handler<IsNameRegistered> for ServerCertDB {
|
||||||
|
type Result = bool;
|
||||||
|
|
||||||
|
fn handle(&mut self, msg: IsNameRegistered, _ctx: &mut Self::Context) -> Self::Result {
|
||||||
|
self.db.values().any(|x| *x == msg.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GuestServerConnection {
|
||||||
|
stream: TlsStream<TcpStream>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Actor for GuestServerConnection {
|
||||||
|
type Context = Context<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ServerConnection<T: 'static> {
|
||||||
|
stream: Framed<TlsStream<TcpStream>, T>,
|
||||||
|
name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ServerConnection<T> {
|
||||||
|
fn new(stream: TlsStream<TcpStream>, codec: T) -> Self {
|
||||||
|
let stream = Framed::new(stream, codec);
|
||||||
|
ServerConnection {
|
||||||
|
stream,
|
||||||
|
name: "Polnareffland1".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Actor for ServerConnection<T> {
|
||||||
|
type Context = Context<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::main]
|
||||||
|
async fn main() {
|
||||||
|
// Tracing Subscriber
|
||||||
|
let subscriber = tracing_subscriber::FmtSubscriber::new();
|
||||||
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||||
|
// BROKER CERTS
|
||||||
|
let broker_root_cert_der = load_cert("certs/broker_root_cert.pem").unwrap();
|
||||||
|
let broker_cert_der = load_cert("certs/broker_cert.pem").unwrap();
|
||||||
|
let broker_prkey_der = load_prkey("certs/broker_key.pem").unwrap();
|
||||||
|
// SERVER ROOT
|
||||||
|
let server_root_cert_der = load_cert("certs/server_root_cert.pem").unwrap();
|
||||||
|
let server_root_prkey_der = load_prkey("certs/server_root_key.pem").unwrap();
|
||||||
|
// GUESTSERVER ROOT
|
||||||
|
let guestserver_root_cert_der = load_cert("certs/guestserver_root_cert.pem").unwrap();
|
||||||
|
// CLIENT ROOT
|
||||||
|
let client_root_cert_der = load_cert("certs/client_root_cert.pem").unwrap();
|
||||||
|
// Client Verifier
|
||||||
|
let mut broker_root_store = RootCertStore::empty();
|
||||||
|
broker_root_store.add(server_root_cert_der.clone()).unwrap();
|
||||||
|
broker_root_store.add(client_root_cert_der.clone()).unwrap();
|
||||||
|
broker_root_store.add(guestserver_root_cert_der.clone()).unwrap();
|
||||||
|
let server_verifier = WebPkiClientVerifier::builder(Arc::new(broker_root_store)).build().unwrap();
|
||||||
|
// Configure TLS
|
||||||
|
let server_tlsconfig = ServerConfig::builder()
|
||||||
|
// .with_no_client_auth()
|
||||||
|
.with_client_cert_verifier(server_verifier)
|
||||||
|
.with_single_cert(vec![broker_cert_der.clone(), broker_root_cert_der.clone()], broker_prkey_der.into())
|
||||||
|
.unwrap();
|
||||||
|
let server_acceptor = RustlsAcceptor::new(server_tlsconfig);
|
||||||
|
|
||||||
|
let server_root_cert_der = Arc::new(server_root_cert_der);
|
||||||
|
let server_root_prkey = KeyPair::from_der(server_root_prkey_der.secret_pkcs8_der()).unwrap();
|
||||||
|
let client_root_cert_der = Arc::new(client_root_cert_der);
|
||||||
|
let guestserver_root_cert_der = Arc::new(guestserver_root_cert_der);
|
||||||
|
let server_root_cert = Arc::new(Certificate::from_params(CertificateParams::from_ca_cert_der(
|
||||||
|
&*server_root_cert_der,
|
||||||
|
server_root_prkey
|
||||||
|
).unwrap()).unwrap());
|
||||||
|
|
||||||
|
let server_db_addr = ServerCertDB {
|
||||||
|
db: HashMap::new(),
|
||||||
|
}.start();
|
||||||
|
|
||||||
|
Server::build()
|
||||||
|
.bind("server-command", ("localhost", 2541), move || {
|
||||||
|
let server_root_cert_der = Arc::clone(&server_root_cert_der);
|
||||||
|
let client_root_cert_der = Arc::clone(&client_root_cert_der);
|
||||||
|
let guestserver_root_cert_der = Arc::clone(&guestserver_root_cert_der);
|
||||||
|
let server_root_cert = Arc::clone(&server_root_cert);
|
||||||
|
let server_db_addr = server_db_addr.clone();
|
||||||
|
|
||||||
|
// Set up TLS service factory
|
||||||
|
server_acceptor
|
||||||
|
.clone()
|
||||||
|
.map_err(|err| println!("Rustls error: {:?}", err))
|
||||||
|
.and_then(move |stream: TlsStream<TcpStream>| {
|
||||||
|
let server_root_cert_der = Arc::clone(&server_root_cert_der);
|
||||||
|
let client_root_cert_der = Arc::clone(&client_root_cert_der);
|
||||||
|
let guestserver_root_cert_der = Arc::clone(&guestserver_root_cert_der);
|
||||||
|
let server_root_cert = Arc::clone(&server_root_cert);
|
||||||
|
let server_db_addr = server_db_addr.clone();
|
||||||
|
async move {
|
||||||
|
let peer_cert_der = stream.get_ref().1.peer_certificates().unwrap().last().unwrap().clone();
|
||||||
|
if peer_cert_der == *server_root_cert_der {
|
||||||
|
info!("Server connection");
|
||||||
|
let framed = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
|
framed.for_each(|item| async move {
|
||||||
|
match item {
|
||||||
|
Ok(buf) => {
|
||||||
|
use FromServerMessage::*;
|
||||||
|
let msg: FromServerMessage = rmp_serde::from_slice(&buf).unwrap();
|
||||||
|
info!("{:?}", msg);
|
||||||
|
match msg {
|
||||||
|
RequiredReply(v) => match v {
|
||||||
|
RequiredReplyValues::Ok => {
|
||||||
|
info!("Required Reply OK")
|
||||||
|
}
|
||||||
|
RequiredReplyValues::GenericFailure { .. } => {
|
||||||
|
info!("Required Reply Generic Failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChangeName { name } => {
|
||||||
|
info!("Requested Change Name to Name {}", name);
|
||||||
|
}
|
||||||
|
WhoAmI => {
|
||||||
|
info!("Requested WhoAmI");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
info!("Disconnection: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).await;
|
||||||
|
info!("Disconnection!");
|
||||||
|
} else if peer_cert_der == *guestserver_root_cert_der {
|
||||||
|
info!("GuestServer connection");
|
||||||
|
let server_root_cert = Arc::clone(&server_root_cert);
|
||||||
|
let codec = LengthDelimitedCodec::new();
|
||||||
|
let mut transport = Framed::new(stream, codec);
|
||||||
|
loop {
|
||||||
|
match transport.next().await {
|
||||||
|
None => {
|
||||||
|
info!("Transport returned None");
|
||||||
|
}
|
||||||
|
Some(item) => {
|
||||||
|
match item {
|
||||||
|
Ok(buf) => {
|
||||||
|
use FromGuestServerMessage::*;
|
||||||
|
let msg: FromGuestServerMessage = rmp_serde::from_slice(&buf).unwrap();
|
||||||
|
info!("{:?}", msg);
|
||||||
|
match msg {
|
||||||
|
Announce { name } => {
|
||||||
|
info!("Announced with name {}", name);
|
||||||
|
if server_db_addr.send(IsNameRegistered { name: name.clone() }).await.unwrap() {
|
||||||
|
info!("Name {} already registered!", name);
|
||||||
|
let reply = ToGuestServerMessage::FailedNameAlreadyOccupied;
|
||||||
|
transport.send(rmp_serde::to_vec(&reply).unwrap().into()).await.unwrap();
|
||||||
|
break; // Stop reading
|
||||||
|
} else {
|
||||||
|
let cert = generate_server_cert(&server_root_cert, name.as_str());
|
||||||
|
server_db_addr.send(RegisterServer {
|
||||||
|
cert: cert.cert.clone(),
|
||||||
|
name,
|
||||||
|
}).await.unwrap().unwrap();
|
||||||
|
let reply = ToGuestServerMessage::OkAnnounce {
|
||||||
|
server_cert: cert.cert,
|
||||||
|
server_prkey: cert.prkey
|
||||||
|
};
|
||||||
|
transport.send(rmp_serde::to_vec(&reply).unwrap().into()).await.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
info!("Disconnection: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if peer_cert_der == *client_root_cert_der {
|
||||||
|
info!("Client connection");
|
||||||
|
} else {
|
||||||
|
error!("Unknown Root Certificate");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).unwrap()
|
||||||
|
.workers(1)
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ enum ClientMessage {
|
|||||||
NotRequired { id: String },
|
NotRequired { id: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: This is an old examples
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
let client_name = "Polnareffland1";
|
let client_name = "Polnareffland1";
|
||||||
@@ -36,7 +37,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
let connector = TlsConnector::from(Arc::new(tlsconfig));
|
let connector = TlsConnector::from(Arc::new(tlsconfig));
|
||||||
let dnsname = ServerName::try_from("localhost").unwrap();
|
let dnsname = ServerName::try_from("localhost").unwrap();
|
||||||
|
|
||||||
let stream = TcpStream::connect("localhost:6379").await?;
|
let stream = TcpStream::connect("localhost:2541").await?;
|
||||||
let stream = connector.connect(dnsname, stream).await?;
|
let stream = connector.connect(dnsname, stream).await?;
|
||||||
|
|
||||||
let mut transport = Framed::new(stream, LengthDelimitedCodec::new());
|
let mut transport = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
#[tokio::main]
|
|
||||||
async fn main() {}
|
|
||||||
@@ -13,7 +13,6 @@ futures = "0.3"
|
|||||||
rcgen = "0.12.0"
|
rcgen = "0.12.0"
|
||||||
tokio-rustls = "0.25.0"
|
tokio-rustls = "0.25.0"
|
||||||
rustls-pemfile = "2.0.0"
|
rustls-pemfile = "2.0.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
rmp-serde = "1.1.2"
|
rmp-serde = "1.1.2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
console-subscriber = "0.2.0"
|
tracing-subscriber = "0.3"
|
||||||
@@ -1,137 +1,109 @@
|
|||||||
use std::collections::HashMap;
|
use std::sync::Arc;
|
||||||
use tokio::net::{TcpListener, TcpStream};
|
use futures::{StreamExt, SinkExt};
|
||||||
use std::net::{SocketAddr};
|
use tokio::net::TcpStream;
|
||||||
use std::sync::{Arc};
|
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
|
||||||
use std::time::Instant;
|
use tokio_rustls::rustls::pki_types::{ServerName, CertificateDer, PrivatePkcs8KeyDer};
|
||||||
use futures::stream::{Stream, StreamExt};
|
use tokio_rustls::TlsConnector;
|
||||||
use tokio_rustls::{TlsAcceptor};
|
|
||||||
use tokio_rustls::rustls::{RootCertStore, ServerConfig};
|
|
||||||
use tokio_rustls::rustls::server::WebPkiClientVerifier;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio_rustls::server::TlsStream;
|
|
||||||
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
use tokio_util::codec::{Framed, LengthDelimitedCodec};
|
||||||
use libbonknet::{load_prkey, load_cert};
|
use libbonknet::*;
|
||||||
|
use tracing::{info, error};
|
||||||
|
|
||||||
type FramedStream = Framed<TlsStream<TcpStream>,LengthDelimitedCodec>;
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[tokio::main]
|
||||||
enum ClientMessage {
|
async fn main() -> std::io::Result<()> {
|
||||||
Response { status_code: u32, msg: Option<String> },
|
// Tracing Subscriber
|
||||||
Announce { name: String },
|
let subscriber = tracing_subscriber::FmtSubscriber::new();
|
||||||
Required { id: String },
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||||
NotRequired { id: String },
|
// Root certs to verify the server is the right one
|
||||||
}
|
let mut broker_root_cert_store = RootCertStore::empty();
|
||||||
|
let broker_root_cert_der = load_cert("certs/broker_root_cert.pem").unwrap();
|
||||||
|
broker_root_cert_store.add(broker_root_cert_der).unwrap();
|
||||||
|
// Public CA that will be used to generate the Server certificate
|
||||||
|
let root_server_cert = load_cert("certs/server_root_cert.pem").unwrap();
|
||||||
|
// Guest CA
|
||||||
|
let root_guestserver_cert = load_cert("certs/guestserver_root_cert.pem").unwrap();
|
||||||
|
// Certificate used to do the first authentication
|
||||||
|
let guestserver_cert = load_cert("certs/guestserver_cert.pem").unwrap();
|
||||||
|
let guestserver_prkey = load_prkey("certs/guestserver_key.pem").unwrap();
|
||||||
|
// Load TLS Config
|
||||||
|
let tlsconfig = ClientConfig::builder()
|
||||||
|
.with_root_certificates(broker_root_cert_store.clone())
|
||||||
|
// .with_no_client_auth();
|
||||||
|
.with_client_auth_cert(vec![guestserver_cert, root_guestserver_cert], guestserver_prkey.into())
|
||||||
|
.unwrap();
|
||||||
|
let connector = TlsConnector::from(Arc::new(tlsconfig));
|
||||||
|
let dnsname = ServerName::try_from("localhost").unwrap();
|
||||||
|
|
||||||
async fn process_client(stream: TlsStream<TcpStream>, peer_addr: SocketAddr) {
|
let stream = TcpStream::connect("localhost:2541").await?;
|
||||||
|
let stream = connector.connect(dnsname, stream).await?;
|
||||||
|
|
||||||
|
let mut transport = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
|
let msg = FromGuestServerMessage::Announce { name: "cicciopizza".into() };
|
||||||
|
transport.send(rmp_serde::to_vec(&msg).unwrap().into()).await.unwrap();
|
||||||
|
let mut myserver_cert: Option<CertificateDer> = None;
|
||||||
|
let mut myserver_prkey: Option<PrivatePkcs8KeyDer> = None;
|
||||||
|
match transport.next().await {
|
||||||
|
None => {
|
||||||
|
info!("None in the transport.next() ???");
|
||||||
|
}
|
||||||
|
Some(item) => match item {
|
||||||
|
Ok(buf) => {
|
||||||
|
use ToGuestServerMessage::*;
|
||||||
|
let msg: ToGuestServerMessage = rmp_serde::from_slice(&buf).unwrap();
|
||||||
|
info!("{:?}", msg);
|
||||||
|
match msg {
|
||||||
|
OkAnnounce { server_cert, server_prkey } => {
|
||||||
|
info!("Ok Announce");
|
||||||
|
let (server_cert, server_prkey) = okannounce_to_cert(server_cert, server_prkey);
|
||||||
|
myserver_cert = Some(server_cert);
|
||||||
|
myserver_prkey = Some(server_prkey);
|
||||||
|
}
|
||||||
|
FailedNameAlreadyOccupied => {
|
||||||
|
error!("Failed Announce");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Error: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let (Some(server_cert), Some(server_prkey)) = (myserver_cert, myserver_prkey) {
|
||||||
|
let tlsconfig = ClientConfig::builder()
|
||||||
|
.with_root_certificates(broker_root_cert_store)
|
||||||
|
.with_client_auth_cert(vec![server_cert, root_server_cert], server_prkey.into())
|
||||||
|
.unwrap();
|
||||||
|
let connector = TlsConnector::from(Arc::new(tlsconfig));
|
||||||
|
let dnsname = ServerName::try_from("localhost").unwrap();
|
||||||
|
|
||||||
|
let stream = TcpStream::connect("localhost:2541").await?;
|
||||||
|
let stream = connector.connect(dnsname, stream).await?;
|
||||||
let transport = Framed::new(stream, LengthDelimitedCodec::new());
|
let transport = Framed::new(stream, LengthDelimitedCodec::new());
|
||||||
transport.for_each(|item| async move {
|
transport.for_each(|item| async move {
|
||||||
match item {
|
match item {
|
||||||
Ok(frame) => {
|
Ok(buf) => {
|
||||||
let a: ClientMessage = rmp_serde::from_slice(&frame).unwrap();
|
use ToServerMessage::*;
|
||||||
println!("{:?}: {:?}", peer_addr, a);
|
let msg: ToServerMessage = rmp_serde::from_slice(&buf).unwrap();
|
||||||
},
|
match msg {
|
||||||
|
Required { id } => {
|
||||||
|
info!("I'm required with Connection ID {}", id);
|
||||||
|
}
|
||||||
|
YouAre(name) => match name {
|
||||||
|
YouAreValues::Registered { name } => {
|
||||||
|
info!("I am {}", name);
|
||||||
|
}
|
||||||
|
YouAreValues::NotRegistered => {
|
||||||
|
info!("I'm not registered");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{:?}: ERROR: {}", peer_addr, e);
|
error!("Error: {:?}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).await;
|
}).await;
|
||||||
}
|
|
||||||
|
|
||||||
struct ClientState {
|
|
||||||
framedstream: FramedStream,
|
|
||||||
last_life_signal: Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ClientConnectionManager {
|
|
||||||
registered_clients: Arc<HashMap<String,ClientState>>,
|
|
||||||
unreg_clients: Arc<Vec<ClientState>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClientConnectionManager {
|
|
||||||
async fn new_and_initialize(port: u16, tlsconfig: ServerConfig) -> ClientConnectionManager {
|
|
||||||
let acceptor = TlsAcceptor::from(Arc::new(tlsconfig));
|
|
||||||
let listener = TcpListener::bind(format!("localhost:{}", port)).await.unwrap();
|
|
||||||
let registered_clients = Arc::new(HashMap::new());
|
|
||||||
let unreg_clients = Arc::new(Vec::new());
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let task_acceptor = acceptor;
|
|
||||||
loop {
|
|
||||||
let (stream, peer_addr) = listener.accept().await.unwrap();
|
|
||||||
let acceptor = task_acceptor.clone();
|
|
||||||
let stream = acceptor.accept(stream).await.unwrap();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
//let transport = Framed::new(stream, LengthDelimitedCodec::new());
|
|
||||||
process_client(stream, peer_addr).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let ccm = ClientConnectionManager { registered_clients, unreg_clients};
|
|
||||||
ccm
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn process_new_client(&mut self, transport: FramedStream, _peer_addr: SocketAddr) {
|
|
||||||
let state = ClientState{
|
|
||||||
framedstream: transport,
|
|
||||||
last_life_signal: Instant::now(),
|
|
||||||
};
|
|
||||||
self.unreg_clients.push(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let server_root_cert_der = load_cert("server_root_cert.pem").unwrap();
|
|
||||||
let server_cert_der = load_cert("server_cert.pem").unwrap();
|
|
||||||
let server_prkey_der = load_prkey("server_key.pem").unwrap();
|
|
||||||
// CLIENT ROOT
|
|
||||||
let client_root_cert_der = load_cert("client_root_cert.pem").unwrap();
|
|
||||||
// Client Verifier
|
|
||||||
let mut clientrootstore = RootCertStore::empty();
|
|
||||||
clientrootstore.add(client_root_cert_der).unwrap();
|
|
||||||
let client_verifier = WebPkiClientVerifier::builder(Arc::new(clientrootstore)).build().unwrap();
|
|
||||||
// Configure TLS
|
|
||||||
let tlsconfig = ServerConfig::builder()
|
|
||||||
// .with_no_client_auth()
|
|
||||||
.with_client_cert_verifier(client_verifier)
|
|
||||||
.with_single_cert(vec![server_cert_der.clone(), server_root_cert_der.clone()], server_prkey_der.into())
|
|
||||||
.unwrap();
|
|
||||||
let acceptor = TlsAcceptor::from(Arc::new(tlsconfig));
|
|
||||||
|
|
||||||
let listener = TcpListener::bind("localhost:6379").await.unwrap();
|
|
||||||
|
|
||||||
// Create Queue Binder
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (stream, peer_addr) = listener.accept().await.unwrap();
|
|
||||||
let acceptor = acceptor.clone();
|
|
||||||
let stream = acceptor.accept(stream).await.unwrap();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
//let transport = Framed::new(stream, LengthDelimitedCodec::new());
|
|
||||||
process_client(stream, peer_addr).await;
|
|
||||||
});
|
|
||||||
|
|
||||||
// let msg1 = ClientMessage::Required { id: "Testo".into() };
|
|
||||||
// let msg2 = ClientMessage::NotRequired { id: "Testo2".into() };
|
|
||||||
// transport.send(rmp_serde::to_vec(&msg1).unwrap().into()).await.unwrap();
|
|
||||||
// transport.send(rmp_serde::to_vec(&msg2).unwrap().into()).await.unwrap();
|
|
||||||
|
|
||||||
// let fut = async move {
|
|
||||||
// let stream = acceptor.accept(stream).await?;
|
|
||||||
// let (mut reader, mut writer) = split(stream);
|
|
||||||
// let n = copy(&mut reader, &mut writer).await?;
|
|
||||||
// writer.flush().await?;
|
|
||||||
// println!("Echo: {} - {}", peer_addr, n);
|
|
||||||
//
|
|
||||||
// Ok(()) as std::io::Result<()>
|
|
||||||
// };
|
|
||||||
//
|
|
||||||
// tokio::spawn(async move {
|
|
||||||
// if let Err(err) = fut.await {
|
|
||||||
// eprintln!("{:?}", err);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -8,3 +8,4 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tokio-rustls = "0.25.0"
|
tokio-rustls = "0.25.0"
|
||||||
rustls-pemfile = "2.0.0"
|
rustls-pemfile = "2.0.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::io::{BufReader, Error, ErrorKind};
|
use std::io::{BufReader, Error, ErrorKind};
|
||||||
use rustls_pemfile::{read_one, Item};
|
use rustls_pemfile::{read_one, Item};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer};
|
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivatePkcs8KeyDer};
|
||||||
|
|
||||||
pub fn load_cert(filename: &str) -> std::io::Result<CertificateDer> {
|
pub fn load_cert(filename: &str) -> std::io::Result<CertificateDer> {
|
||||||
@@ -23,3 +24,54 @@ pub fn load_prkey(filename: &str) -> std::io::Result<PrivatePkcs8KeyDer> {
|
|||||||
Err(Error::new(ErrorKind::InvalidInput, "no pkcs8key"))
|
Err(Error::new(ErrorKind::InvalidInput, "no pkcs8key"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum RequiredReplyValues {
|
||||||
|
Ok,
|
||||||
|
GenericFailure { status_code: u32, msg: Option<String> },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum FromServerMessage {
|
||||||
|
RequiredReply(RequiredReplyValues),
|
||||||
|
ChangeName { name: String },
|
||||||
|
WhoAmI,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum YouAreValues {
|
||||||
|
Registered { name: String },
|
||||||
|
NotRegistered,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ToServerMessage {
|
||||||
|
Required { id: String },
|
||||||
|
YouAre(YouAreValues),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum FromGuestServerMessage {
|
||||||
|
Announce { name: String }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ToGuestServerMessage {
|
||||||
|
OkAnnounce {server_cert: Vec<u8>, server_prkey: Vec<u8>},
|
||||||
|
FailedNameAlreadyOccupied,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn okannounce_to_cert<'a>(server_cert: Vec<u8>, server_prkey: Vec<u8>) -> (CertificateDer<'a>, PrivatePkcs8KeyDer<'a>) {
|
||||||
|
let server_cert = CertificateDer::from(server_cert);
|
||||||
|
let server_prkey = PrivatePkcs8KeyDer::from(server_prkey);
|
||||||
|
(server_cert, server_prkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToGuestServerMessage {
|
||||||
|
pub fn make_okannounce(server_cert: CertificateDer, server_prkey: PrivatePkcs8KeyDer) -> Self {
|
||||||
|
ToGuestServerMessage::OkAnnounce{
|
||||||
|
server_cert: server_cert.to_vec(),
|
||||||
|
server_prkey: server_prkey.secret_pkcs8_der().to_vec()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user