added API keys basic support
This commit is contained in:
parent
9a81e7e7df
commit
c8d7c41f33
10
Cargo.toml
10
Cargo.toml
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
todo!();
|
||||
}
|
|
@ -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 = []
|
||||
|
||||
|
||||
|
|
123
src/api/mod.rs
123
src/api/mod.rs
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use rand::prelude::*;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::types::Date;
|
||||
|
|
|
@ -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!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue