added API keys basic support

This commit is contained in:
Raphael Jacobs 2024-03-20 20:12:09 +01:00
parent 9a81e7e7df
commit c8d7c41f33
Signed by: raphy
GPG Key ID: 2987BB662DB3120B
10 changed files with 185 additions and 36 deletions

View File

@ -3,7 +3,9 @@ name = "dasher"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "tcp_bench"
path = "bench_tools/tcp_bench.rs"
[dependencies]
axum = "0.7.4"
@ -12,10 +14,12 @@ serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.113"
tokio = { version = "1.36.0", features = ["net"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"]}
sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "sqlite" ] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "sqlite"] }
thiserror = "1.0.57"
toml = "0.8.10"
rfc7239 = "0.1.0"
rand = "0.8.5"
[profile.dev.package.sqlx-macros]
opt-level = 3

View File

@ -1,5 +1,6 @@
`dasher`
# `dasher`
This is a tool I made to monitor my own infrastructure. It was fun to build and tailored to my needs. Think twice before using in production.
# Installation

3
bench_tools/tcp_bench.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() {
todo!();
}

View File

@ -1,6 +1,7 @@
tcp_ip4 = "0.0.0.0:58008"
http_ip4 = "0.0.0.0:3000"
db = "sqlite:memory:"
db = "sqlite:file:db?cache=shared"
whitelist = ["127.0.0.1"]
public_sets = []

View File

@ -1,8 +1,11 @@
use core::panic;
use std::net::{Ipv4Addr, SocketAddr};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use axum::extract::ConnectInfo;
use axum::http::header::FORWARDED;
use axum::http::HeaderMap;
use axum::{extract::State, http::StatusCode, routing::get, Json, Router};
use rand::random;
use thiserror::Error;
use tracing::{debug, info, instrument};
@ -21,6 +24,8 @@ use crate::db::DB;
use crate::types::*;
use rfc7239::parse;
#[derive(Clone)]
pub struct AppState {
db: DB,
@ -53,57 +58,97 @@ async fn run_web_api_failable(db: DB, conf: Config) -> Result<(), HttpServError>
Ok(())
}
// checks if ip is in whitelist.
fn allowed(conf: Config, ip: &Ipv4Addr) -> bool {
conf.whitelist.contains(ip)
/// Checks for an API key, and then checks if an ip is allowed.
async fn allowed(conf: &Config, db: &DB, headers: &HeaderMap, ip: &IpAddr) -> bool {
if let Some(key) = headers.get("x-api-key") {
if let Ok(key_str) = key.to_str() {
match db.get_key(key_str).await {
Ok(_) => return true,
Err(e) => debug!("AuthError: {e:?}"),
}
}
}
match ip {
std::net::IpAddr::V4(ip) => {
let trueip = get_true_ip(conf, headers, ip);
if !conf.whitelist.contains(&trueip) {
return false;
}
}
std::net::IpAddr::V6(_) => todo!("IPV6 still to do!!"),
}
true
}
fn get_true_ip(conf: &Config, headers: &HeaderMap, ip: &Ipv4Addr) -> Ipv4Addr {
if conf.trusted_proxies.contains(ip) {
if let Some(forwarded_for) = headers.get(FORWARDED) {
if let Ok(forwarded_for_str) = forwarded_for.to_str() {
debug!("forwarded_for_headers:{forwarded_for:?}");
if let Some(addr) = parse_forwarded_for(forwarded_for_str) {
return addr;
};
};
}
}
*ip
}
/// Wrapping our forwarded_for library
fn parse_forwarded_for(header_val: &str) -> Option<Ipv4Addr> {
for node_result in parse(header_val) {
let node = node_result.ok()?;
if let Some(forwarded_for) = node.forwarded_for {
debug!("Forwarded by {}", forwarded_for)
// TODO: boh?
}
}
None
}
#[instrument(skip(state))]
async fn query_last_n(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
State(state): State<AppState>,
Json(payload): Json<QueryN>,
) -> (StatusCode, Json<Answer>) {
match addr.ip() {
std::net::IpAddr::V4(ip) => {
if !allowed(state.conf, &ip) {
debug!("{addr} unauthorized.");
return (StatusCode::NETWORK_AUTHENTICATION_REQUIRED, EMPTY_ANSWER);
}
}
std::net::IpAddr::V6(_) => todo!("IPV6 still to do!!"),
let n = payload.n.unwrap_or(1);
debug!("Headers: {headers:?} Request: {payload:?} from {addr}");
if !state.conf.public_sets.contains(&payload.name)
&& !allowed(&state.conf, &state.db, &headers, &addr.ip()).await
{
return (StatusCode::NETWORK_AUTHENTICATION_REQUIRED, EMPTY_ANSWER);
};
let n = payload.n.unwrap_or(1);
debug!("Request: {payload:?} from {addr}");
if let Ok(answers) = state.db.get_last_n(&payload.name, n).await {
(StatusCode::OK, Json((answers.len() as u64, answers)))
} else {
(StatusCode::NOT_FOUND, EMPTY_ANSWER)
match state.db.get_last_n(&payload.name, n).await {
Ok(answers) => return (StatusCode::OK, Json((answers.len() as u64, answers))),
Err(e) => {
debug!("{e:?}");
return (StatusCode::NOT_FOUND, EMPTY_ANSWER);
}
}
}
#[instrument(skip(state))]
async fn query_all(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
State(state): State<AppState>,
Json(payload): Json<QueryAll>,
) -> (StatusCode, Json<Answer>) {
debug!("Request: {payload:?} from {addr}");
debug!("Headers: {headers:?} Request: {payload:?} from {addr}");
// If the queried datased is NOT public, check for
// ip whitelisting.
if !state.conf.public_sets.contains(&payload.name) {
match addr.ip() {
std::net::IpAddr::V4(ip) => {
if !allowed(state.conf, &ip) {
debug!("{addr} unauthorized.");
return (StatusCode::NETWORK_AUTHENTICATION_REQUIRED, EMPTY_ANSWER);
}
}
std::net::IpAddr::V6(_) => todo!("IPV6 still to do!!"),
}
if !state.conf.public_sets.contains(&payload.name)
&& !allowed(&state.conf, &state.db, &headers, &addr.ip()).await
{
return (StatusCode::NETWORK_AUTHENTICATION_REQUIRED, EMPTY_ANSWER);
};
match (payload.from, payload.to) {
@ -151,3 +196,21 @@ async fn query_all(
}
}
}
pub async fn generate_api_key() -> String {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\
abcdefghijklmnopqrstuvwxyz\
0123456789";
const API_KEY_LEN: usize = 30;
let mut rng = rand::thread_rng();
let api_key: String = (0..API_KEY_LEN)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
api_key
}

View File

@ -1,3 +1,4 @@
use rand::prelude::*;
use serde::Deserialize;
use crate::types::Date;

View File

@ -3,11 +3,12 @@ use std::{
process::exit,
};
use crate::api::run_web_api;
use crate::api::{self, run_web_api};
use crate::tcp;
use tcp::listen_api;
use tokio::task;
use tracing::{debug, info};
use crate::cli::*;
use crate::conf::Config;
@ -62,6 +63,25 @@ pub async fn dispatch(args: Args, conf: &Config) {
}
Command::ShowData => todo!(),
Command::Info => todo!(),
Command::Api { subcommand } => match subcommand {
ApiCommand::Generate { comment } => {
let key = api::generate_api_key().await;
let db = DB::new(&conf.db).await.unwrap();
let res = comment.unwrap_or(String::new());
match db.add_key(&key, &res).await {
Ok(_) => {
info!("Added api key to database. Copy your key:");
println!("{key}");
}
Err(e) => {
debug!("{e:?}");
println!("Error generating/adding key.")
}
};
}
ApiCommand::List => todo!(),
ApiCommand::Delete => todo!(),
},
}
}

View File

@ -33,6 +33,11 @@ pub struct Args {
pub enum Command {
/// Run webserver and collector
Run,
/// Manage API keys
Api {
#[command(subcommand)]
subcommand: ApiCommand,
},
/// Print config file
ShowConf,
/// Delete dataset <name>.
@ -52,3 +57,16 @@ pub enum Command {
/// Show information and statistics
Info,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ApiCommand {
/// Generates and prins a new api key
Generate {
/// Optional comment
comment: Option<String>,
},
/// Shows all API keys
List,
/// Remove/revoke an API key.
Delete,
}

View File

@ -67,6 +67,17 @@ impl DB {
.execute(&(self.0))
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS api_keys (
key_id INTEGER PRIMARY KEY NOT NULL,
key TEXT NOT NULL,
comment TEXT,
unique(key)
)",
)
.execute(&(self.0))
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS data (
data_id INTEGER NOT NULL,
@ -80,6 +91,27 @@ impl DB {
Ok(())
}
/// Gets API key.
pub async fn get_key(&self, api_key: &str) -> Result<ApiKey, DBError> {
let key = sqlx::query_as::<_, ApiKey>(&format!(
"SELECT key, comment FROM api_keys WHERE key=?1;"
))
.bind(api_key)
.fetch_one(&(self.0))
.await?;
Ok(key)
}
/// Adds API key.
pub async fn add_key(&self, api_key: &str, comment: &str) -> Result<(), DBError> {
let added_key = sqlx::query("INSERT INTO api_keys ( key, comment ) VALUES ( ?1, ?2 )")
.bind(api_key)
.bind(comment)
.execute(&(self.0))
.await?;
Ok(())
}
/// Adds a certain value to the dataset <name>. This function performs lookup
/// of datasets, eventul creation of dataset and then finally insertion.
/// When repeatedly sending the same data, it is best to create an

View File

@ -12,3 +12,9 @@ pub struct Entry {
pub value: String,
pub date: i64,
}
#[derive(Default, Serialize, Debug, FromRow)]
pub struct ApiKey {
pub key: String,
pub comment: String,
}