Compare commits

...

16 Commits

Author SHA1 Message Date
Aravinth Manivannan
239e0bfd47 feat: easy captcha update job runner 2024-01-05 01:03:38 +05:30
Aravinth Manivannan
790fd8f393 feat: create runner method for updating easy captchas 2024-01-05 01:03:13 +05:30
Aravinth Manivannan
c70a30e640 feat: fetch username of owner and description in easy captcha method 2024-01-05 01:02:27 +05:30
Aravinth Manivannan
3b8051159d feat: use time for easy captcha when option is configured by admin 2024-01-04 23:29:20 +05:30
Aravinth Manivannan
91c235b3f4 feat: add database method to get all easy captcha configurations with pagination 2024-01-04 23:28:50 +05:30
Aravinth Manivannan
9bcf6af3ab feat: add options to use time for easy captcha configuration 2024-01-04 23:28:04 +05:30
Aravinth Manivannan
e0d6188853 fix: terminate demo user job cleanly 2024-01-04 23:24:36 +05:30
Aravinth Manivannan
13c3066b86 fix: unused import 2024-01-04 17:22:10 +05:30
Aravinth Manivannan
da934f5ba7 feat: new dashboard page to show percentile scores on PoW performance analysis records 2024-01-04 01:58:19 +05:30
Aravinth Manivannan
26ad05d284 Merge pull request #125 from mCaptcha/fix-embedded-cache-health
fix: health endpoint crashing with embedded cache usage
2023-12-09 01:32:21 +05:30
Aravinth Manivannan
b6326603d1 fix: health endpoint crashing with embedded cache usage 2023-12-09 01:15:25 +05:30
Aravinth Manivannan
8bed3cb352 Merge pull request #121 from mCaptcha/feat-percentile
compute percentile on analytics records
2023-11-04 20:32:28 +00:00
Aravinth Manivannan
8e03290fda feat: expose percentile scores for all analyis records through API
endpoint
2023-11-05 01:20:49 +05:30
Aravinth Manivannan
321fd2e89b feat: create individual databases for each test 2023-11-05 01:17:42 +05:30
Aravinth Manivannan
36600e2f13 feat: database methods to compute percentiles on analysis records 2023-11-05 00:48:26 +05:30
Aravinth Manivannan
606d22cc9d Merge pull request #120 from mCaptcha/feat-help-text-in-publishing-data
feat: link to mCaptcha net blog post from the captcha creation form
2023-11-02 10:42:11 +00:00
29 changed files with 1644 additions and 108 deletions

View File

@@ -1,2 +1,2 @@
export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"
export MARIA_DATABASE_URL="mysql://maria:password@localhost:3306/maria" export MARIA_DATABASE_URL="mysql://root:password@localhost:3306/maria"

View File

@@ -34,8 +34,11 @@ enable_stats = true
[captcha.default_difficulty_strategy] [captcha.default_difficulty_strategy]
avg_traffic_difficulty = 50000 # almost instant solution avg_traffic_difficulty = 50000 # almost instant solution
#avg_traffic_time = 1 # almost instant solution
peak_sustainable_traffic_difficulty = 3000000 # roughly 1.5s peak_sustainable_traffic_difficulty = 3000000 # roughly 1.5s
#peak_sustainable_traffic_time = 3
broke_my_site_traffic_difficulty = 5000000 # greater than 3.5s broke_my_site_traffic_difficulty = 5000000 # greater than 3.5s
#broke_my_site_traffic_time = 5
duration = 30 # cooldown period in seconds duration = 30 # cooldown period in seconds
[database] [database]

View File

@@ -202,6 +202,13 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
captcha_key: &str, captcha_key: &str,
) -> DBResult<TrafficPattern>; ) -> DBResult<TrafficPattern>;
/// Get all easy captcha configurations on instance
async fn get_all_easy_captchas(
&self,
limit: usize,
offset: usize,
) -> DBResult<Vec<EasyCaptcha>>;
/// Delete traffic configuration /// Delete traffic configuration
async fn delete_traffic_pattern( async fn delete_traffic_pattern(
&self, &self,
@@ -307,6 +314,17 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
captcha_key: &str, captcha_key: &str,
difficulty_factor: u32, difficulty_factor: u32,
) -> DBResult<u32>; ) -> DBResult<u32>;
/// Get number of analytics entries that are under a certain duration
async fn stats_get_num_logs_under_time(&self, duration: u32) -> DBResult<usize>;
/// Get the entry at a location in the list of analytics entires under a certain time limit
/// and sorted in ascending order
async fn stats_get_entry_at_location_for_time_limit_asc(
&self,
duration: u32,
location: u32,
) -> DBResult<Option<usize>>;
} }
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)] #[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
@@ -372,6 +390,19 @@ pub struct AddNotification<'a> {
pub message: &'a str, pub message: &'a str,
} }
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
/// Represents Easy captcha configuration
pub struct EasyCaptcha {
/// traffic pattern of easy captcha
pub traffic_pattern: TrafficPattern,
/// captcha key/sitekey
pub key: String,
/// captcha description
pub description: String,
/// Owner of the captcha configuration
pub username: String,
}
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)] #[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
/// User's traffic pattern; used in generating a captcha configuration /// User's traffic pattern; used in generating a captcha configuration
pub struct TrafficPattern { pub struct TrafficPattern {

View File

@@ -7,6 +7,29 @@
use crate::errors::*; use crate::errors::*;
use crate::prelude::*; use crate::prelude::*;
/// easy traffic pattern
pub const TRAFFIC_PATTERN: TrafficPattern = TrafficPattern {
avg_traffic: 500,
peak_sustainable_traffic: 5_000,
broke_my_site_traffic: Some(10_000),
};
/// levels for complex captcha config
pub const LEVELS: [Level; 3] = [
Level {
difficulty_factor: 1,
visitor_threshold: 1,
},
Level {
difficulty_factor: 2,
visitor_threshold: 2,
},
Level {
difficulty_factor: 3,
visitor_threshold: 3,
},
];
/// test all database functions /// test all database functions
pub async fn database_works<'a, T: MCDatabase>( pub async fn database_works<'a, T: MCDatabase>(
db: &T, db: &T,
@@ -200,6 +223,11 @@ pub async fn database_works<'a, T: MCDatabase>(
tp tp
); );
// get all traffic patterns
let patterns = db.get_all_easy_captchas(10, 0).await.unwrap();
assert_eq!(patterns.get(0).as_ref().unwrap().key, c.key);
assert_eq!(&patterns.get(0).unwrap().traffic_pattern, tp);
// delete traffic pattern // delete traffic pattern
db.delete_traffic_pattern(p.username, c.key).await.unwrap(); db.delete_traffic_pattern(p.username, c.key).await.unwrap();
assert!( assert!(
@@ -250,7 +278,6 @@ pub async fn database_works<'a, T: MCDatabase>(
db.record_confirm(c.key).await.unwrap(); db.record_confirm(c.key).await.unwrap();
// analytics start // analytics start
db.analytics_create_psuedo_id_if_not_exists(c.key) db.analytics_create_psuedo_id_if_not_exists(c.key)
.await .await
.unwrap(); .unwrap();
@@ -282,11 +309,31 @@ pub async fn database_works<'a, T: MCDatabase>(
); );
let analytics = CreatePerformanceAnalytics { let analytics = CreatePerformanceAnalytics {
time: 0, time: 1,
difficulty_factor: 0, difficulty_factor: 1,
worker_type: "wasm".into(), worker_type: "wasm".into(),
}; };
assert_eq!(
db.stats_get_num_logs_under_time(analytics.time)
.await
.unwrap(),
0
);
db.analysis_save(c.key, &analytics).await.unwrap(); db.analysis_save(c.key, &analytics).await.unwrap();
assert_eq!(
db.stats_get_num_logs_under_time(analytics.time)
.await
.unwrap(),
1
);
assert_eq!(
db.stats_get_num_logs_under_time(analytics.time - 1)
.await
.unwrap(),
0
);
let limit = 50; let limit = 50;
let mut offset = 0; let mut offset = 0;
let a = db.analytics_fetch(c.key, limit, offset).await.unwrap(); let a = db.analytics_fetch(c.key, limit, offset).await.unwrap();
@@ -305,6 +352,50 @@ pub async fn database_works<'a, T: MCDatabase>(
.unwrap(); .unwrap();
assert_eq!(db.analytics_fetch(c.key, 1000, 0).await.unwrap().len(), 0); assert_eq!(db.analytics_fetch(c.key, 1000, 0).await.unwrap().len(), 0);
assert!(!db.analytics_captcha_is_published(c.key).await.unwrap()); assert!(!db.analytics_captcha_is_published(c.key).await.unwrap());
let rest_analytics = [
CreatePerformanceAnalytics {
time: 2,
difficulty_factor: 2,
worker_type: "wasm".into(),
},
CreatePerformanceAnalytics {
time: 3,
difficulty_factor: 3,
worker_type: "wasm".into(),
},
CreatePerformanceAnalytics {
time: 4,
difficulty_factor: 4,
worker_type: "wasm".into(),
},
CreatePerformanceAnalytics {
time: 5,
difficulty_factor: 5,
worker_type: "wasm".into(),
},
];
for a in rest_analytics.iter() {
db.analysis_save(c.key, &a).await.unwrap();
}
assert!(db
.stats_get_entry_at_location_for_time_limit_asc(1, 2)
.await
.unwrap()
.is_none());
assert_eq!(
db.stats_get_entry_at_location_for_time_limit_asc(2, 1)
.await
.unwrap(),
Some(2)
);
assert_eq!(
db.stats_get_entry_at_location_for_time_limit_asc(3, 2)
.await
.unwrap(),
Some(3)
);
db.analytics_delete_all_records_for_campaign(c.key) db.analytics_delete_all_records_for_campaign(c.key)
.await .await
.unwrap(); .unwrap();

View File

@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "SELECT\n COUNT(difficulty_factor) AS count\n FROM\n mcaptcha_pow_analytics\n WHERE time <= ?;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": {
"type": "LongLong",
"flags": "NOT_NULL | BINARY",
"char_set": 63,
"max_size": 21
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "9bae79667a8cc631541879321e72a40f20cf812584aaf44418089bc7a51e07c4"
}

View File

@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "SELECT\n difficulty_factor\n FROM\n mcaptcha_pow_analytics\n WHERE\n time <= ?\n ORDER BY difficulty_factor ASC LIMIT 1 OFFSET ?;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "difficulty_factor",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 11
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "c4d6ad934e38218931e74ae1c31c6712cbadb40f31bb12e160c9d333c7e3835c"
}

View File

@@ -0,0 +1,80 @@
{
"db_name": "MySQL",
"query": "SELECT \n mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,\n mcaptcha_config.name,\n mcaptcha_users.name as username,\n mcaptcha_config.captcha_key\n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n INNER JOIN\n mcaptcha_config\n ON\n mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id\n INNER JOIN\n mcaptcha_users\n ON\n mcaptcha_config.user_id = mcaptcha_users.ID\n ORDER BY mcaptcha_config.config_id\n LIMIT ? OFFSET ?",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "avg_traffic",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 1,
"name": "peak_sustainable_traffic",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 2,
"name": "broke_my_site_traffic",
"type_info": {
"type": "Long",
"flags": "",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 3,
"name": "name",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 400
}
},
{
"ordinal": 4,
"name": "username",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 400
}
},
{
"ordinal": 5,
"name": "captcha_key",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 400
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
true,
false,
false,
false
]
},
"hash": "d587844217f202c23d29c3cb4c819551bc204dd459c956c41024fa74aadbba64"
}

View File

@@ -43,7 +43,6 @@ pub mod dev {
pub use super::errors::*; pub use super::errors::*;
pub use super::Database; pub use super::Database;
pub use db_core::dev::*; pub use db_core::dev::*;
pub use prelude::*;
pub use sqlx::Error; pub use sqlx::Error;
} }
@@ -1219,6 +1218,121 @@ impl MCDatabase for Database {
Ok(res.nonce as u32) Ok(res.nonce as u32)
} }
} }
/// Get number of analytics entries that are under a certain duration
async fn stats_get_num_logs_under_time(&self, duration: u32) -> DBResult<usize> {
struct Count {
count: Option<i64>,
}
//"SELECT COUNT(*) FROM (SELECT difficulty_factor FROM mcaptcha_pow_analytics WHERE time <= ?) as count",
let count = sqlx::query_as!(
Count,
"SELECT
COUNT(difficulty_factor) AS count
FROM
mcaptcha_pow_analytics
WHERE time <= ?;",
duration as i32,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(count.count.unwrap_or_else(|| 0) as usize)
}
/// Get the entry at a location in the list of analytics entires under a certain time limited
/// and sorted in ascending order
async fn stats_get_entry_at_location_for_time_limit_asc(
&self,
duration: u32,
location: u32,
) -> DBResult<Option<usize>> {
struct Difficulty {
difficulty_factor: Option<i32>,
}
match sqlx::query_as!(
Difficulty,
"SELECT
difficulty_factor
FROM
mcaptcha_pow_analytics
WHERE
time <= ?
ORDER BY difficulty_factor ASC LIMIT 1 OFFSET ?;",
duration as i32,
location as i64 - 1,
)
.fetch_one(&self.pool)
.await
{
Ok(res) => Ok(Some(res.difficulty_factor.unwrap() as usize)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(map_row_not_found_err(e, DBError::CaptchaNotFound)),
}
}
/// Get all easy captcha configurations on instance
async fn get_all_easy_captchas(
&self,
limit: usize,
offset: usize,
) -> DBResult<Vec<EasyCaptcha>> {
struct InnerEasyCaptcha {
captcha_key: String,
name: String,
username: String,
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
let mut inner_res = sqlx::query_as!(
InnerEasyCaptcha,
"SELECT
mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic,
mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic,
mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,
mcaptcha_config.name,
mcaptcha_users.name as username,
mcaptcha_config.captcha_key
FROM
mcaptcha_sitekey_user_provided_avg_traffic
INNER JOIN
mcaptcha_config
ON
mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id
INNER JOIN
mcaptcha_users
ON
mcaptcha_config.user_id = mcaptcha_users.ID
ORDER BY mcaptcha_config.config_id
LIMIT ? OFFSET ?",
limit as i64,
offset as i64
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
let mut res = Vec::with_capacity(inner_res.len());
inner_res.drain(0..).for_each(|v| {
res.push(EasyCaptcha {
key: v.captcha_key,
description: v.name,
username: v.username,
traffic_pattern: TrafficPattern {
broke_my_site_traffic: v
.broke_my_site_traffic
.as_ref()
.map(|v| *v as u32),
avg_traffic: v.avg_traffic as u32,
peak_sustainable_traffic: v.peak_sustainable_traffic as u32,
},
})
});
Ok(res)
}
} }
#[derive(Clone)] #[derive(Clone)]

View File

@@ -5,9 +5,11 @@
#![cfg(test)] #![cfg(test)]
use sqlx::mysql::MySqlPoolOptions;
use std::env; use std::env;
use sqlx::{migrate::MigrateDatabase, mysql::MySqlPoolOptions};
use url::Url;
use crate::*; use crate::*;
use db_core::tests::*; use db_core::tests::*;
@@ -26,28 +28,6 @@ async fn everyting_works() {
const HEADING: &str = "testing notifications get db mariadb"; const HEADING: &str = "testing notifications get db mariadb";
const MESSAGE: &str = "testing notifications get message db mariadb"; const MESSAGE: &str = "testing notifications get message db mariadb";
// easy traffic pattern
const TRAFFIC_PATTERN: TrafficPattern = TrafficPattern {
avg_traffic: 500,
peak_sustainable_traffic: 5_000,
broke_my_site_traffic: Some(10_000),
};
const LEVELS: [Level; 3] = [
Level {
difficulty_factor: 1,
visitor_threshold: 1,
},
Level {
difficulty_factor: 2,
visitor_threshold: 2,
},
Level {
difficulty_factor: 3,
visitor_threshold: 3,
},
];
const ADD_NOTIFICATION: AddNotification = AddNotification { const ADD_NOTIFICATION: AddNotification = AddNotification {
from: NAME, from: NAME,
to: NAME, to: NAME,
@@ -56,10 +36,20 @@ async fn everyting_works() {
}; };
let url = env::var("MARIA_DATABASE_URL").unwrap(); let url = env::var("MARIA_DATABASE_URL").unwrap();
let mut parsed = Url::parse(&url).unwrap();
parsed.set_path("db_maria_test");
let url = parsed.to_string();
if sqlx::MySql::database_exists(&url).await.unwrap() {
sqlx::MySql::drop_database(&url).await.unwrap();
}
sqlx::MySql::create_database(&url).await.unwrap();
let pool_options = MySqlPoolOptions::new().max_connections(2); let pool_options = MySqlPoolOptions::new().max_connections(2);
let connection_options = ConnectionOptions::Fresh(Fresh { let connection_options = ConnectionOptions::Fresh(Fresh {
pool_options, pool_options,
url, url: url.clone(),
disable_logging: false, disable_logging: false,
}); });
let db = connection_options.connect().await.unwrap(); let db = connection_options.connect().await.unwrap();
@@ -78,4 +68,6 @@ async fn everyting_works() {
description: CAPTCHA_DESCRIPTION, description: CAPTCHA_DESCRIPTION,
}; };
database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await; database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await;
drop(db);
sqlx::MySql::drop_database(&url).await.unwrap();
} }

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT COUNT(difficulty_factor) FROM mcaptcha_pow_analytics WHERE time <= $1;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
null
]
},
"hash": "c08c1dd4bfcb6cbd0359c79cc3be79526a012b006ce9deb80bceb4e1a04c835d"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n difficulty_factor\n FROM\n mcaptcha_pow_analytics\n WHERE\n time <= $1\n ORDER BY difficulty_factor ASC LIMIT 1 OFFSET $2;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "difficulty_factor",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Int4",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "c67aec0c3d5786fb495b6ed60fa106437d8e5034d3a40bf8face2ca7c12f2694"
}

View File

@@ -0,0 +1,53 @@
{
"db_name": "PostgreSQL",
"query": "SELECT \n mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic, \n mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,\n mcaptcha_config.name,\n mcaptcha_users.name as username,\n mcaptcha_config.key\n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n INNER JOIN\n mcaptcha_config\n ON\n mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id\n INNER JOIN\n mcaptcha_users\n ON\n mcaptcha_config.user_id = mcaptcha_users.ID\n ORDER BY mcaptcha_config.config_id\n OFFSET $1 LIMIT $2; ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "avg_traffic",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "peak_sustainable_traffic",
"type_info": "Int4"
},
{
"ordinal": 2,
"name": "broke_my_site_traffic",
"type_info": "Int4"
},
{
"ordinal": 3,
"name": "name",
"type_info": "Varchar"
},
{
"ordinal": 4,
"name": "username",
"type_info": "Varchar"
},
{
"ordinal": 5,
"name": "key",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
false
]
},
"hash": "f01a9c09c8722bc195f477a8c3ce6466d415e7c74665fa882eff4a8566e70577"
}

View File

@@ -43,7 +43,6 @@ pub mod dev {
pub use super::errors::*; pub use super::errors::*;
pub use super::Database; pub use super::Database;
pub use db_core::dev::*; pub use db_core::dev::*;
pub use prelude::*;
pub use sqlx::Error; pub use sqlx::Error;
} }
@@ -670,13 +669,8 @@ impl MCDatabase for Database {
username: &str, username: &str,
captcha_key: &str, captcha_key: &str,
) -> DBResult<TrafficPattern> { ) -> DBResult<TrafficPattern> {
struct Traffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
let res = sqlx::query_as!( let res = sqlx::query_as!(
Traffic, InnerTraffic,
"SELECT "SELECT
avg_traffic, avg_traffic,
peak_sustainable_traffic, peak_sustainable_traffic,
@@ -707,11 +701,67 @@ impl MCDatabase for Database {
.fetch_one(&self.pool) .fetch_one(&self.pool)
.await .await
.map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?; .map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
Ok(TrafficPattern { Ok(res.into())
broke_my_site_traffic: res.broke_my_site_traffic.as_ref().map(|v| *v as u32), }
avg_traffic: res.avg_traffic as u32,
peak_sustainable_traffic: res.peak_sustainable_traffic as u32, /// Get all easy captcha configurations on instance
}) async fn get_all_easy_captchas(
&self,
limit: usize,
offset: usize,
) -> DBResult<Vec<EasyCaptcha>> {
struct InnerEasyCaptcha {
key: String,
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
name: String,
username: String,
}
let mut inner_res = sqlx::query_as!(
InnerEasyCaptcha,
"SELECT
mcaptcha_sitekey_user_provided_avg_traffic.avg_traffic,
mcaptcha_sitekey_user_provided_avg_traffic.peak_sustainable_traffic,
mcaptcha_sitekey_user_provided_avg_traffic.broke_my_site_traffic,
mcaptcha_config.name,
mcaptcha_users.name as username,
mcaptcha_config.key
FROM
mcaptcha_sitekey_user_provided_avg_traffic
INNER JOIN
mcaptcha_config
ON
mcaptcha_config.config_id = mcaptcha_sitekey_user_provided_avg_traffic.config_id
INNER JOIN
mcaptcha_users
ON
mcaptcha_config.user_id = mcaptcha_users.ID
ORDER BY mcaptcha_config.config_id
OFFSET $1 LIMIT $2; ",
offset as i32,
limit as i32
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
let mut res = Vec::with_capacity(inner_res.len());
inner_res.drain(0..).for_each(|v| {
res.push(EasyCaptcha {
key: v.key,
description: v.name,
username: v.username,
traffic_pattern: TrafficPattern {
broke_my_site_traffic: v
.broke_my_site_traffic
.as_ref()
.map(|v| *v as u32),
avg_traffic: v.avg_traffic as u32,
peak_sustainable_traffic: v.peak_sustainable_traffic as u32,
},
})
});
Ok(res)
} }
/// Delete traffic configuration /// Delete traffic configuration
@@ -1227,6 +1277,56 @@ impl MCDatabase for Database {
Ok(res.nonce as u32) Ok(res.nonce as u32)
} }
} }
/// Get number of analytics entries that are under a certain duration
async fn stats_get_num_logs_under_time(&self, duration: u32) -> DBResult<usize> {
struct Count {
count: Option<i64>,
}
let count = sqlx::query_as!(
Count,
"SELECT COUNT(difficulty_factor) FROM mcaptcha_pow_analytics WHERE time <= $1;",
duration as i32,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(count.count.unwrap_or_else(|| 0) as usize)
}
/// Get the entry at a location in the list of analytics entires under a certain time limit
/// and sorted in ascending order
async fn stats_get_entry_at_location_for_time_limit_asc(
&self,
duration: u32,
location: u32,
) -> DBResult<Option<usize>> {
struct Difficulty {
difficulty_factor: Option<i32>,
}
match sqlx::query_as!(
Difficulty,
"SELECT
difficulty_factor
FROM
mcaptcha_pow_analytics
WHERE
time <= $1
ORDER BY difficulty_factor ASC LIMIT 1 OFFSET $2;",
duration as i32,
location as i64 - 1,
)
.fetch_one(&self.pool)
.await
{
Ok(res) => Ok(Some(res.difficulty_factor.unwrap() as usize)),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(map_row_not_found_err(e, DBError::CaptchaNotFound)),
}
}
} }
#[derive(Clone)] #[derive(Clone)]
@@ -1296,3 +1396,19 @@ impl From<InternaleCaptchaConfig> for Captcha {
} }
} }
} }
struct InnerTraffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
impl From<InnerTraffic> for TrafficPattern {
fn from(v: InnerTraffic) -> Self {
TrafficPattern {
broke_my_site_traffic: v.broke_my_site_traffic.as_ref().map(|v| *v as u32),
avg_traffic: v.avg_traffic as u32,
peak_sustainable_traffic: v.peak_sustainable_traffic as u32,
}
}
}

View File

@@ -5,9 +5,12 @@
#![cfg(test)] #![cfg(test)]
use sqlx::postgres::PgPoolOptions;
use std::env; use std::env;
use sqlx::migrate::MigrateDatabase;
use sqlx::postgres::PgPoolOptions;
use url::Url;
use crate::*; use crate::*;
use db_core::tests::*; use db_core::tests::*;
@@ -26,28 +29,6 @@ async fn everyting_works() {
const HEADING: &str = "testing notifications get db postgres"; const HEADING: &str = "testing notifications get db postgres";
const MESSAGE: &str = "testing notifications get message db postgres"; const MESSAGE: &str = "testing notifications get message db postgres";
// easy traffic pattern
const TRAFFIC_PATTERN: TrafficPattern = TrafficPattern {
avg_traffic: 500,
peak_sustainable_traffic: 5_000,
broke_my_site_traffic: Some(10_000),
};
const LEVELS: [Level; 3] = [
Level {
difficulty_factor: 1,
visitor_threshold: 1,
},
Level {
difficulty_factor: 2,
visitor_threshold: 2,
},
Level {
difficulty_factor: 3,
visitor_threshold: 3,
},
];
const ADD_NOTIFICATION: AddNotification = AddNotification { const ADD_NOTIFICATION: AddNotification = AddNotification {
from: NAME, from: NAME,
to: NAME, to: NAME,
@@ -56,10 +37,20 @@ async fn everyting_works() {
}; };
let url = env::var("POSTGRES_DATABASE_URL").unwrap(); let url = env::var("POSTGRES_DATABASE_URL").unwrap();
let mut parsed = Url::parse(&url).unwrap();
parsed.set_path("db_postgres_test");
let url = parsed.to_string();
if sqlx::Postgres::database_exists(&url).await.unwrap() {
sqlx::Postgres::drop_database(&url).await.unwrap();
}
sqlx::Postgres::create_database(&url).await.unwrap();
let pool_options = PgPoolOptions::new().max_connections(2); let pool_options = PgPoolOptions::new().max_connections(2);
let connection_options = ConnectionOptions::Fresh(Fresh { let connection_options = ConnectionOptions::Fresh(Fresh {
pool_options, pool_options,
url, url: url.clone(),
disable_logging: false, disable_logging: false,
}); });
let db = connection_options.connect().await.unwrap(); let db = connection_options.connect().await.unwrap();
@@ -78,4 +69,6 @@ async fn everyting_works() {
description: CAPTCHA_DESCRIPTION, description: CAPTCHA_DESCRIPTION,
}; };
database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await; database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await;
drop(db);
sqlx::Postgres::drop_database(&url).await.unwrap();
} }

View File

@@ -56,23 +56,22 @@ you will be overriding the values set in the configuration files.
### Captcha ### Captcha
| Name | Value | | Name | Value |
| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --- | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `MCAPTCHA_captcha_SALT` | Salt has to be long and random | | `MCAPTCHA_captcha_SALT` | Salt has to be long and random |
| `MCAPTCHA_captcha_GC` | Garbage collection duration in seconds, requires tuning but 30 is a good starting point | | `MCAPTCHA_captcha_GC` | Garbage collection duration in seconds, requires tuning but 30 is a good starting point |
| `MCAPTCHA_captcha_RUNNERS` | [Performance] Number of runners to use for PoW validation. Defaults to number of CPUs available | | `MCAPTCHA_captcha_RUNNERS` | [Performance] Number of runners to use for PoW validation. Defaults to number of CPUs available |
| `MCAPTCHA_captcha_QUEUE_LENGTH` | [Performance] PoW Validation queue length, controls how many pending validation jobs can be held in queue | | `MCAPTCHA_captcha_QUEUE_LENGTH` | [Performance] PoW Validation queue length, controls how many pending validation jobs can be held in queue |
| `MCAPTCHA_captcha_ENABLE_STATS` | Record for CAPTCHA events like configuration fetch, solves and authentication of validation token. Useful for commercial deployments. | | | `MCAPTCHA_captcha_ENABLE_STATS` | Record for CAPTCHA events like configuration fetch, solves and authentication of validation token. Useful for commercial deployments. |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for average traffic metric | | `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for average traffic metric |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for peak traffic metric | | `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time` | This difficulty factor is used in to use in easy mode CAPTCHA configuration estimation for average traffic metric |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_difficulty`% | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for traffic that took the website down | | `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for peak traffic metric |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration`% | Default duration to use in CAPTCHA configuration in easy mode | | `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time` | This difficulty factor is used in to use in easy mode CAPTCHA configuration estimation for peak traffic metric |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_difficulty` | Default difficulty factor to use in easy mode CAPTCHA configuration estimation for traffic that took the website down |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time` | Default time (in seconds) to use to compute difficulty factor using stored PoW performance records. |
| `MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration` | Default duration to use in CAPTCHA configuration in easy mode |
\% See commits See commits [`54b14291ec140e`](https://github.com/mCaptcha/mCaptcha/commit/54b14291ec140ea4cbbf73462d3d6fc2d39f2d2c) and [`42544ec421e0`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065) for more info.
[`54b14291ec140e`](https://github.com/mCaptcha/mCaptcha/commit/54b14291ec140ea4cbbf73462d3d6fc2d39f2d2c)
and
[`42544ec421e0`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065)
for more info.
### SMTP ### SMTP

View File

@@ -101,6 +101,79 @@ pub fn calculate(
Ok(levels) Ok(levels)
} }
async fn calculate_with_percentile(
data: &AppData,
tp: &TrafficPattern,
) -> ServiceResult<Option<Vec<Level>>> {
use crate::api::v1::stats::{percentile_bench_runner, PercentileReq};
let strategy = &data.settings.captcha.default_difficulty_strategy;
if strategy.avg_traffic_time.is_none()
&& strategy.peak_sustainable_traffic_time.is_none()
&& strategy.broke_my_site_traffic_time.is_none()
{
return Ok(None);
}
let mut req = PercentileReq {
time: strategy.avg_traffic_time.unwrap(),
percentile: 90.00,
};
let resp = percentile_bench_runner(data, &req).await?;
if resp.difficulty_factor.is_none() {
return Ok(None);
}
let avg_traffic_difficulty = resp.difficulty_factor.unwrap();
req.time = strategy.peak_sustainable_traffic_time.unwrap();
let resp = percentile_bench_runner(data, &req).await?;
if resp.difficulty_factor.is_none() {
return Ok(None);
}
let peak_sustainable_traffic_difficulty = resp.difficulty_factor.unwrap();
req.time = strategy.broke_my_site_traffic_time.unwrap();
let resp = percentile_bench_runner(data, &req).await?;
let broke_my_site_traffic_difficulty = if resp.difficulty_factor.is_none() {
resp.difficulty_factor.unwrap()
} else {
peak_sustainable_traffic_difficulty * 2
};
let mut levels = vec![
LevelBuilder::default()
.difficulty_factor(avg_traffic_difficulty)?
.visitor_threshold(tp.avg_traffic)
.build()?,
LevelBuilder::default()
.difficulty_factor(peak_sustainable_traffic_difficulty)?
.visitor_threshold(tp.peak_sustainable_traffic)
.build()?,
];
let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(broke_my_site_traffic_difficulty)?;
match tp.broke_my_site_traffic {
Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic)
}
None => match tp
.peak_sustainable_traffic
.checked_add(tp.peak_sustainable_traffic / 2)
{
Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast
// into i32. Should choose bigger number or casts properly
None => highest_level.visitor_threshold(u32::MAX),
},
};
levels.push(highest_level.build()?);
Ok(Some(levels))
}
#[my_codegen::post( #[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.create", path = "crate::V1_API_ROUTES.captcha.easy.create",
wrap = "crate::api::v1::get_middleware()" wrap = "crate::api::v1::get_middleware()"
@@ -113,8 +186,12 @@ async fn create(
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let payload = payload.into_inner(); let payload = payload.into_inner();
let pattern = (&payload).into(); let pattern = (&payload).into();
let levels = let levels = if let Some(levels) = calculate_with_percentile(&data, &pattern).await?
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?; {
levels
} else {
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?
};
let msg = CreateCaptcha { let msg = CreateCaptcha {
levels, levels,
duration: data.settings.captcha.default_difficulty_strategy.duration, duration: data.settings.captcha.default_difficulty_strategy.duration,
@@ -147,6 +224,15 @@ async fn update(
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let payload = payload.into_inner(); let payload = payload.into_inner();
update_runner(&data, payload, username).await?;
Ok(HttpResponse::Ok())
}
pub async fn update_runner(
data: &AppData,
payload: UpdateTrafficPattern,
username: String,
) -> ServiceResult<()> {
let pattern = (&payload.pattern).into(); let pattern = (&payload.pattern).into();
let levels = let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?; calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
@@ -167,7 +253,7 @@ async fn update(
.add_traffic_pattern(&username, &msg.key, &pattern) .add_traffic_pattern(&username, &msg.key, &pattern)
.await?; .await?;
Ok(HttpResponse::Ok()) Ok(())
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -62,6 +62,7 @@ impl Health {
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")] #[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
async fn health(data: AppData) -> impl Responder { async fn health(data: AppData) -> impl Responder {
let mut resp_builder = HealthBuilder::default(); let mut resp_builder = HealthBuilder::default();
resp_builder.redis(None);
resp_builder.db(data.db.ping().await); resp_builder.db(data.db.ping().await);

View File

@@ -14,6 +14,7 @@ pub mod meta;
pub mod notifications; pub mod notifications;
pub mod pow; pub mod pow;
mod routes; mod routes;
pub mod stats;
pub mod survey; pub mod survey;
pub use routes::ROUTES; pub use routes::ROUTES;
@@ -26,6 +27,7 @@ pub fn services(cfg: &mut ServiceConfig) {
mcaptcha::services(cfg); mcaptcha::services(cfg);
notifications::services(cfg); notifications::services(cfg);
survey::services(cfg); survey::services(cfg);
stats::services(cfg);
} }
#[derive(Deserialize)] #[derive(Deserialize)]

View File

@@ -11,6 +11,7 @@ use super::mcaptcha::routes::Captcha;
use super::meta::routes::Meta; use super::meta::routes::Meta;
use super::notifications::routes::Notifications; use super::notifications::routes::Notifications;
use super::pow::routes::PoW; use super::pow::routes::PoW;
use super::stats::routes::Stats;
use super::survey::routes::Survey; use super::survey::routes::Survey;
pub const ROUTES: Routes = Routes::new(); pub const ROUTES: Routes = Routes::new();
@@ -23,6 +24,7 @@ pub struct Routes {
pub pow: PoW, pub pow: PoW,
pub survey: Survey, pub survey: Survey,
pub notifications: Notifications, pub notifications: Notifications,
pub stats: Stats,
} }
impl Routes { impl Routes {
@@ -35,6 +37,7 @@ impl Routes {
pow: PoW::new(), pow: PoW::new(),
notifications: Notifications::new(), notifications: Notifications::new(),
survey: Survey::new(), survey: Survey::new(),
stats: Stats::new(),
} }
} }
} }

259
src/api/v1/stats.rs Normal file
View File

@@ -0,0 +1,259 @@
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::{web, HttpResponse, Responder};
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::AppData;
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
pub struct BuildDetails {
pub version: &'static str,
pub git_commit_hash: &'static str,
}
pub mod routes {
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Stats {
pub percentile_benches: &'static str,
}
impl Stats {
pub const fn new() -> Self {
Self {
percentile_benches: "/api/v1/stats/analytics/percentile",
}
}
}
}
pub async fn percentile_bench_runner(
data: &AppData,
req: &PercentileReq,
) -> ServiceResult<PercentileResp> {
let count = data.db.stats_get_num_logs_under_time(req.time).await?;
if count == 0 {
return Ok(PercentileResp {
difficulty_factor: None,
});
}
if count < 2 {
return Ok(PercentileResp {
difficulty_factor: None,
});
}
let location = ((count - 1) as f64 * (req.percentile / 100.00)) + 1.00;
let fraction = location - location.floor();
if fraction > 0.00 {
if let (Some(base), Some(ceiling)) = (
data.db
.stats_get_entry_at_location_for_time_limit_asc(
req.time,
location.floor() as u32,
)
.await?,
data.db
.stats_get_entry_at_location_for_time_limit_asc(
req.time,
location.floor() as u32 + 1,
)
.await?,
) {
let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32;
return Ok(PercentileResp {
difficulty_factor: Some(res),
});
}
} else {
if let Some(base) = data
.db
.stats_get_entry_at_location_for_time_limit_asc(
req.time,
location.floor() as u32,
)
.await?
{
let res = base as u32;
return Ok(PercentileResp {
difficulty_factor: Some(res),
});
}
};
Ok(PercentileResp {
difficulty_factor: None,
})
}
/// Get difficulty factor with max time limit for percentile of stats
#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")]
async fn percentile_benches(
data: AppData,
payload: web::Json<PercentileReq>,
) -> ServiceResult<impl Responder> {
Ok(HttpResponse::Ok().json(percentile_bench_runner(&data, &payload).await?))
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileReq {
pub time: u32,
pub percentile: f64,
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileResp {
pub difficulty_factor: Option<u32>,
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(percentile_benches);
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, App};
use super::*;
use crate::api::v1::services;
use crate::*;
#[actix_rt::test]
async fn stats_bench_work_pg() {
let data = crate::tests::pg::get_data().await;
stats_bench_work(data).await;
}
#[actix_rt::test]
async fn stats_bench_work_maria() {
let data = crate::tests::maria::get_data().await;
stats_bench_work(data).await;
}
async fn stats_bench_work(data: ArcData) {
use crate::tests::*;
const NAME: &str = "benchstatsuesr";
const EMAIL: &str = "benchstatsuesr@testadminuser.com";
const PASSWORD: &str = "longpassword2";
const DEVICE_USER_PROVIDED: &str = "foo";
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let data = &data;
{
delete_user(&data, NAME).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
// create captcha
let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await;
let app = get_app!(data).await;
let page = 1;
let tmp_id = uuid::Uuid::new_v4();
let download_rotue = V1_API_ROUTES
.survey
.get_download_route(&tmp_id.to_string(), page);
let download_req = test::call_service(
&app,
test::TestRequest::get().uri(&download_rotue).to_request(),
)
.await;
assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
data.db
.analytics_create_psuedo_id_if_not_exists(&key.key)
.await
.unwrap();
let psuedo_id = data
.db
.analytics_get_psuedo_id_from_capmaign_id(&key.key)
.await
.unwrap();
for i in 1..6 {
println!("[{i}] Saving analytics");
let analytics = db_core::CreatePerformanceAnalytics {
time: i,
difficulty_factor: i,
worker_type: "wasm".into(),
};
data.db.analysis_save(&key.key, &analytics).await.unwrap();
}
let msg = PercentileReq {
time: 1,
percentile: 99.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert!(resp.difficulty_factor.is_none());
let msg = PercentileReq {
time: 1,
percentile: 100.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert!(resp.difficulty_factor.is_none());
let msg = PercentileReq {
time: 2,
percentile: 100.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert_eq!(resp.difficulty_factor.unwrap(), 2);
let msg = PercentileReq {
time: 5,
percentile: 90.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert_eq!(resp.difficulty_factor.unwrap(), 4);
delete_user(&data, NAME).await;
}
}

View File

@@ -8,6 +8,7 @@ use std::time::Duration;
use actix::clock::sleep; use actix::clock::sleep;
use actix::spawn; use actix::spawn;
use tokio::sync::oneshot::{channel, error::TryRecvError, Receiver, Sender};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use crate::api::v1::account::delete::runners::delete_user; use crate::api::v1::account::delete::runners::delete_user;
@@ -23,20 +24,24 @@ pub const DEMO_USER: &str = "aaronsw";
pub const DEMO_PASSWORD: &str = "password"; pub const DEMO_PASSWORD: &str = "password";
pub struct DemoUser { pub struct DemoUser {
handle: JoinHandle<()>, tx: Sender<()>,
} }
impl DemoUser { impl DemoUser {
pub async fn spawn(data: AppData, duration: Duration) -> ServiceResult<Self> { pub async fn spawn(
let handle = Self::run(data, duration).await?; data: AppData,
let d = Self { handle }; duration: u32,
) -> ServiceResult<(Self, JoinHandle<()>)> {
let (tx, rx) = channel();
let handle = Self::run(data, duration, rx).await?;
let d = Self { tx };
Ok(d) Ok((d, handle))
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn abort(&self) { pub fn abort(mut self) {
self.handle.abort(); self.tx.send(());
} }
/// register demo user runner /// register demo user runner
@@ -71,16 +76,38 @@ impl DemoUser {
pub async fn run( pub async fn run(
data: AppData, data: AppData,
duration: Duration, duration: u32,
mut rx: Receiver<()>,
) -> ServiceResult<JoinHandle<()>> { ) -> ServiceResult<JoinHandle<()>> {
Self::register_demo_user(&data).await?; Self::register_demo_user(&data).await?;
fn can_run(rx: &mut Receiver<()>) -> bool {
match rx.try_recv() {
Err(TryRecvError::Empty) => true,
_ => false,
}
}
let mut exit = false;
let fut = async move { let fut = async move {
loop { loop {
sleep(duration).await; if exit {
break;
}
for _ in 0..duration {
if can_run(&mut rx) {
sleep(Duration::new(1, 0)).await;
continue;
} else {
exit = true;
break;
}
}
if let Err(e) = Self::delete_demo_user(&data).await { if let Err(e) = Self::delete_demo_user(&data).await {
log::error!("Error while deleting demo user: {:?}", e); log::error!("Error while deleting demo user: {:?}", e);
} }
if let Err(e) = Self::register_demo_user(&data).await { if let Err(e) = Self::register_demo_user(&data).await {
log::error!("Error while registering demo user: {:?}", e); log::error!("Error while registering demo user: {:?}", e);
} }
@@ -133,7 +160,7 @@ mod tests {
assert!(!username_exists(&payload, &data).await.unwrap().exists); assert!(!username_exists(&payload, &data).await.unwrap().exists);
// test the runner // test the runner
let user = DemoUser::spawn(data, duration).await.unwrap(); let user = DemoUser::spawn(data, DURATION as u32).await.unwrap();
let (_, signin_resp, token_key) = let (_, signin_resp, token_key) =
add_levels_util(data_inner, DEMO_USER, DEMO_PASSWORD).await; add_levels_util(data_inner, DEMO_USER, DEMO_PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
@@ -162,6 +189,7 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(resp).await; let res_levels: Vec<Level> = test::read_body_json(resp).await;
assert!(res_levels.is_empty()); assert!(res_levels.is_empty());
user.abort(); user.0.abort();
user.1.await.unwrap();
} }
} }

132
src/easy.rs Normal file
View File

@@ -0,0 +1,132 @@
// Copyright (C) 2024// Copyright (C) 2024 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::time::Duration;
//use std::sync::atomicBool
use actix::clock::sleep;
use actix::spawn;
use tokio::sync::oneshot::{channel, error::TryRecvError, Receiver, Sender};
use tokio::task::JoinHandle;
use crate::api::v1::mcaptcha::easy::{
update_runner, TrafficPatternRequest, UpdateTrafficPattern,
};
use crate::*;
use errors::*;
pub struct UpdateEasyCaptcha {
tx: Sender<()>,
}
impl UpdateEasyCaptcha {
pub async fn spawn(
data: AppData,
duration: u32,
) -> ServiceResult<(Self, JoinHandle<()>)> {
let (tx, rx) = channel();
let handle = Self::run(data, duration, rx).await?;
let d = Self { tx };
Ok((d, handle))
}
#[allow(dead_code)]
pub fn abort(mut self) {
self.tx.send(());
}
/// update configurations
async fn update_captcha_configurations(
data: &AppData,
rx: &mut Receiver<()>,
) -> ServiceResult<()> {
let limit = 10;
let mut offset = 0;
let mut page = 0;
loop {
offset = page * limit;
if !Self::can_run(rx) {
return Ok(());
}
let mut patterns = data.db.get_all_easy_captchas(limit, offset).await?;
for pattern in patterns.drain(0..) {
if !Self::can_run(rx) {
return Ok(());
}
let publish_benchmarks =
data.db.analytics_captcha_is_published(&pattern.key).await?;
let req = UpdateTrafficPattern {
pattern: TrafficPatternRequest {
avg_traffic: pattern.traffic_pattern.avg_traffic,
peak_sustainable_traffic: pattern
.traffic_pattern
.peak_sustainable_traffic,
broke_my_site_traffic: pattern
.traffic_pattern
.broke_my_site_traffic,
description: pattern.description,
publish_benchmarks,
},
key: pattern.key,
};
if !Self::can_run(rx) {
return Ok(());
}
update_runner(&data, req, pattern.username).await?;
}
page += 1;
}
}
fn can_run(rx: &mut Receiver<()>) -> bool {
match rx.try_recv() {
Err(TryRecvError::Empty) => true,
_ => false,
}
}
pub async fn run(
data: AppData,
duration: u32,
mut rx: Receiver<()>,
) -> ServiceResult<JoinHandle<()>> {
let mut exit = false;
let fut = async move {
loop {
if exit {
break;
}
for _ in 0..duration {
if Self::can_run(&mut rx) {
sleep(Duration::new(1, 0)).await;
continue;
} else {
exit = true;
break;
}
}
if let Some(err) = Self::update_captcha_configurations(&data, &mut rx)
.await
.err()
{
log::error!(
"Tried to update easy captcha configurations in background {:?}",
err
);
}
}
};
let handle = spawn(fut);
Ok(handle)
}
}

View File

@@ -14,6 +14,7 @@ use actix_web::{
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
use log::info; use log::info;
use tokio::task::JoinHandle;
mod api; mod api;
mod data; mod data;
@@ -21,6 +22,7 @@ mod date;
mod db; mod db;
mod demo; mod demo;
mod docs; mod docs;
mod easy;
mod email; mod email;
mod errors; mod errors;
#[macro_use] #[macro_use]
@@ -110,11 +112,22 @@ async fn main() -> std::io::Result<()> {
let data = Data::new(&settings, secrets.clone()).await; let data = Data::new(&settings, secrets.clone()).await;
let data = actix_web::web::Data::new(data); let data = actix_web::web::Data::new(data);
let mut demo_user: Option<DemoUser> = None; let mut demo_user: Option<(DemoUser, JoinHandle<()>)> = None;
if settings.allow_demo && settings.allow_registration { if settings.allow_demo && settings.allow_registration {
demo_user = Some( demo_user = Some(DemoUser::spawn(data.clone(), 60 * 30).await.unwrap());
DemoUser::spawn(data.clone(), Duration::from_secs(60 * 30)) }
let mut update_easy_captcha: Option<(easy::UpdateEasyCaptcha, JoinHandle<()>)> =
None;
if settings
.captcha
.default_difficulty_strategy
.avg_traffic_time
.is_some()
{
update_easy_captcha = Some(
easy::UpdateEasyCaptcha::spawn(data.clone(), 60 * 30)
.await .await
.unwrap(), .unwrap(),
); );
@@ -156,7 +169,13 @@ async fn main() -> std::io::Result<()> {
} }
if let Some(demo_user) = demo_user { if let Some(demo_user) = demo_user {
demo_user.abort(); demo_user.0.abort();
demo_user.1.await.unwrap();
}
if let Some(update_easy_captcha) = update_easy_captcha {
update_easy_captcha.0.abort();
update_easy_captcha.1.await.unwrap();
} }
if let Some(survey_upload_handle) = survey_upload_handle { if let Some(survey_upload_handle) = survey_upload_handle {

View File

@@ -10,6 +10,7 @@ use sailfish::TemplateOnce;
mod notifications; mod notifications;
mod settings; mod settings;
pub mod sitekey; pub mod sitekey;
mod utils;
use db_core::Captcha; use db_core::Captcha;
@@ -47,18 +48,21 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(panel); cfg.service(panel);
settings::services(cfg); settings::services(cfg);
sitekey::services(cfg); sitekey::services(cfg);
utils::services(cfg);
cfg.service(notifications::notifications); cfg.service(notifications::notifications);
} }
pub mod routes { pub mod routes {
use super::settings::routes::Settings; use super::settings::routes::Settings;
use super::sitekey::routes::Sitekey; use super::sitekey::routes::Sitekey;
use super::utils::routes::Utils;
pub struct Panel { pub struct Panel {
pub home: &'static str, pub home: &'static str,
pub sitekey: Sitekey, pub sitekey: Sitekey,
pub notifications: &'static str, pub notifications: &'static str,
pub settings: Settings, pub settings: Settings,
pub utils: Utils,
} }
impl Panel { impl Panel {
@@ -68,10 +72,11 @@ pub mod routes {
sitekey: Sitekey::new(), sitekey: Sitekey::new(),
notifications: "/notifications", notifications: "/notifications",
settings: Settings::new(), settings: Settings::new(),
utils: Utils::new(),
} }
} }
pub const fn get_sitemap() -> [&'static str; 5] { pub const fn get_sitemap() -> [&'static str; 6] {
const PANEL: Panel = Panel::new(); const PANEL: Panel = Panel::new();
const S: [&str; 2] = Sitekey::get_sitemap(); const S: [&str; 2] = Sitekey::get_sitemap();
@@ -81,6 +86,7 @@ pub mod routes {
S[0], S[0],
S[1], S[1],
Settings::get_sitemap()[0], Settings::get_sitemap()[0],
Utils::get_sitemap()[0],
] ]
} }
} }

280
src/pages/panel/utils.rs Normal file
View File

@@ -0,0 +1,280 @@
// Copyright (C) 2024 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use sailfish::TemplateOnce;
use crate::api::v1::stats::{percentile_bench_runner, PercentileReq, PercentileResp};
use crate::errors::PageResult;
use crate::pages::auth::sudo::SudoPage;
use crate::AppData;
pub mod routes {
pub struct Utils {
pub percentile: &'static str,
}
impl Utils {
pub const fn new() -> Self {
Utils {
percentile: "/utils/percentile",
}
}
pub const fn get_sitemap() -> [&'static str; 1] {
const S: Utils = Utils::new();
[S.percentile]
}
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_percentile);
cfg.service(post_percentile);
}
const PAGE: &str = "Difficulty factor statistics";
#[derive(TemplateOnce, Clone)]
#[template(path = "panel/utils/percentile/index.html")]
pub struct PercentilePage {
time: Option<u32>,
percentile: Option<f64>,
difficulty_factor: Option<u32>,
}
#[my_codegen::get(
path = "crate::PAGES.panel.utils.percentile",
wrap = "crate::pages::get_middleware()"
)]
async fn get_percentile(id: Identity) -> PageResult<impl Responder> {
let data = PercentilePage {
time: None,
percentile: None,
difficulty_factor: None,
};
let body = data.render_once().unwrap();
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(body))
}
#[my_codegen::post(
path = "crate::PAGES.panel.utils.percentile",
wrap = "crate::pages::get_middleware()"
)]
async fn post_percentile(
data: AppData,
id: Identity,
payload: web::Form<PercentileReq>,
) -> PageResult<impl Responder> {
let resp = percentile_bench_runner(&data, &payload).await?;
let page = PercentilePage {
time: Some(payload.time),
percentile: Some(payload.percentile),
difficulty_factor: resp.difficulty_factor,
};
let body = page.render_once().unwrap();
Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(body))
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, web::Bytes, App};
use super::*;
use crate::api::v1::services;
use crate::*;
#[actix_rt::test]
async fn page_stats_bench_work_pg() {
let data = crate::tests::pg::get_data().await;
page_stats_bench_work(data).await;
}
#[actix_rt::test]
async fn page_stats_bench_work_maria() {
let data = crate::tests::maria::get_data().await;
page_stats_bench_work(data).await;
}
async fn page_stats_bench_work(data: ArcData) {
use crate::tests::*;
const NAME: &str = "pagebenchstatsuesr";
const EMAIL: &str = "pagebenchstatsuesr@testadminuser.com";
const PASSWORD: &str = "longpassword2";
const DEVICE_USER_PROVIDED: &str = "foo";
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
const THREADS: i32 = 4;
let data = &data;
{
delete_user(&data, NAME).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
// create captcha
let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await;
let app = get_app!(data).await;
let cookies = get_cookie!(signin_resp);
let page = 1;
let tmp_id = uuid::Uuid::new_v4();
let download_rotue = V1_API_ROUTES
.survey
.get_download_route(&tmp_id.to_string(), page);
let download_req = test::call_service(
&app,
test::TestRequest::get().uri(&download_rotue).to_request(),
)
.await;
assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
data.db
.analytics_create_psuedo_id_if_not_exists(&key.key)
.await
.unwrap();
let psuedo_id = data
.db
.analytics_get_psuedo_id_from_capmaign_id(&key.key)
.await
.unwrap();
for i in 1..6 {
println!("[{i}] Saving analytics");
let analytics = db_core::CreatePerformanceAnalytics {
time: i,
difficulty_factor: i,
worker_type: "wasm".into(),
};
data.db.analysis_save(&key.key, &analytics).await.unwrap();
}
let msg = PercentileReq {
time: 1,
percentile: 99.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
assert!(resp.difficulty_factor.is_none());
let msg = PercentileReq {
time: 1,
percentile: 100.00,
};
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp: PercentileResp = test::read_body_json(resp).await;
// start
let percentile_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(&crate::PAGES.panel.utils.percentile)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(percentile_resp.status(), StatusCode::OK);
let body: Bytes = test::read_body(percentile_resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("Maximum time taken"));
let percentile_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(&crate::PAGES.panel.utils.percentile)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(percentile_resp.status(), StatusCode::OK);
let body: Bytes = test::read_body(percentile_resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("Maximum time taken"));
// end
// start post
let msg = PercentileReq {
time: 1,
percentile: 99.00,
};
let percentile_resp = test::call_service(
&app,
test::TestRequest::post()
.uri(&crate::PAGES.panel.utils.percentile)
.set_form(&msg)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(percentile_resp.status(), StatusCode::OK);
let body: Bytes = test::read_body(percentile_resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains(
"Not enough inputs to compute statistics. Please try again later"
));
assert!(body.contains(&1.to_string()));
assert!(body.contains(&99.00.to_string()));
// end post
// start post
let msg = PercentileReq {
time: 2,
percentile: 100.00,
};
let percentile_resp = test::call_service(
&app,
test::TestRequest::post()
.uri(&crate::PAGES.panel.utils.percentile)
.set_form(&msg)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(percentile_resp.status(), StatusCode::OK);
let body: Bytes = test::read_body(percentile_resp).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("Difficulty factor: 2"));
assert!(body.contains(&2.to_string()));
assert!(body.contains(&100.00.to_string()));
}
}

View File

@@ -37,8 +37,11 @@ pub struct Captcha {
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)] #[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct DefaultDifficultyStrategy { pub struct DefaultDifficultyStrategy {
pub avg_traffic_difficulty: u32, pub avg_traffic_difficulty: u32,
pub broke_my_site_traffic_difficulty: u32, pub avg_traffic_time: Option<u32>,
pub peak_sustainable_traffic_difficulty: u32, pub peak_sustainable_traffic_difficulty: u32,
pub peak_sustainable_traffic_time: Option<u32>,
pub broke_my_site_traffic_time: Option<u32>,
pub broke_my_site_traffic_difficulty: u32,
pub duration: u32, pub duration: u32,
} }
@@ -113,7 +116,7 @@ pub struct Settings {
pub smtp: Option<Smtp>, pub smtp: Option<Smtp>,
} }
const ENV_VAR_CONFIG: [(&str, &str); 29] = [ const ENV_VAR_CONFIG: [(&str, &str); 32] = [
/* top-level */ /* top-level */
("debug", "MCAPTCHA_debug"), ("debug", "MCAPTCHA_debug"),
("commercial", "MCAPTCHA_commercial"), ("commercial", "MCAPTCHA_commercial"),
@@ -150,6 +153,9 @@ const ENV_VAR_CONFIG: [(&str, &str); 29] = [
( "captcha.default_difficulty_strategy.duration", ( "captcha.default_difficulty_strategy.duration",
"MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration" "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration"
), ),
("captcha.default_difficulty_strategy.avg_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time"),
("captcha.default_difficulty_strategy.peak_sustainable_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time"),
("captcha.default_difficulty_strategy.broke_my_site_traffic_time", "MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time"),
/* SMTP */ /* SMTP */
@@ -251,6 +257,28 @@ impl Settings {
Ok(settings) Ok(settings)
} }
fn check_easy_captcha_config(&self) {
let s = &self.captcha.default_difficulty_strategy;
if s.avg_traffic_time.is_some() {
if s.broke_my_site_traffic_time.is_none()
|| s.peak_sustainable_traffic_time.is_none()
{
panic!("if captcha.default_difficulty_strategy.avg_traffic_time is set, then captcha.default_difficulty_strategy.broke_my_site_traffic_time and captcha.default_difficulty_strategy.peak_sustainable_traffic_time must also be set");
}
}
if s.peak_sustainable_traffic_time.is_some() {
if s.avg_traffic_time.is_none() || s.peak_sustainable_traffic_time.is_none()
{
panic!("if captcha.default_difficulty_strategy.peak_sustainable_traffic_time is set, then captcha.default_difficulty_strategy.broke_my_site_traffic_time and captcha.default_difficulty_strategy.avg_traffic_time must also be set");
}
}
if s.broke_my_site_traffic_time.is_some() {
if s.avg_traffic_time.is_none() || s.peak_sustainable_traffic_time.is_none()
{
panic!("if captcha.default_difficulty_strategy.broke_my_site_traffic_time is set, then captcha.default_difficulty_strategy.peak_sustainable_traffic_time and captcha.default_difficulty_strategy.avg_traffic_time must also be set");
}
}
}
fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> { fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
for (parameter, env_var_name) in DEPRECATED_ENV_VARS.iter() { for (parameter, env_var_name) in DEPRECATED_ENV_VARS.iter() {
@@ -538,6 +566,30 @@ mod tests {
999, 999,
captcha.default_difficulty_strategy.duration captcha.default_difficulty_strategy.duration
); );
helper!(
"MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time",
"10",
Some(10),
captcha.default_difficulty_strategy.avg_traffic_time
);
helper!(
"MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time",
"20",
Some(20),
captcha
.default_difficulty_strategy
.peak_sustainable_traffic_time
);
helper!(
"MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time",
"30",
Some(30),
captcha
.default_difficulty_strategy
.broke_my_site_traffic_time
);
/* SMTP */ /* SMTP */

View File

@@ -29,6 +29,9 @@ pub fn get_settings() -> Settings {
pub mod pg { pub mod pg {
use std::env; use std::env;
use sqlx::migrate::MigrateDatabase;
use crate::api::v1::mcaptcha::get_random;
use crate::data::Data; use crate::data::Data;
use crate::settings::*; use crate::settings::*;
use crate::survey::SecretsStore; use crate::survey::SecretsStore;
@@ -38,6 +41,16 @@ pub mod pg {
pub async fn get_data() -> ArcData { pub async fn get_data() -> ArcData {
let url = env::var("POSTGRES_DATABASE_URL").unwrap(); let url = env::var("POSTGRES_DATABASE_URL").unwrap();
let mut parsed = url::Url::parse(&url).unwrap();
parsed.set_path(&get_random(16));
let url = parsed.to_string();
if sqlx::Postgres::database_exists(&url).await.unwrap() {
sqlx::Postgres::drop_database(&url).await.unwrap();
}
sqlx::Postgres::create_database(&url).await.unwrap();
let mut settings = get_settings(); let mut settings = get_settings();
settings.captcha.runners = Some(1); settings.captcha.runners = Some(1);
settings.database.url = url.clone(); settings.database.url = url.clone();
@@ -50,6 +63,9 @@ pub mod pg {
pub mod maria { pub mod maria {
use std::env; use std::env;
use sqlx::migrate::MigrateDatabase;
use crate::api::v1::mcaptcha::get_random;
use crate::data::Data; use crate::data::Data;
use crate::settings::*; use crate::settings::*;
use crate::survey::SecretsStore; use crate::survey::SecretsStore;
@@ -59,6 +75,16 @@ pub mod maria {
pub async fn get_data() -> ArcData { pub async fn get_data() -> ArcData {
let url = env::var("MARIA_DATABASE_URL").unwrap(); let url = env::var("MARIA_DATABASE_URL").unwrap();
let mut parsed = url::Url::parse(&url).unwrap();
parsed.set_path(&get_random(16));
let url = parsed.to_string();
if sqlx::MySql::database_exists(&url).await.unwrap() {
sqlx::MySql::drop_database(&url).await.unwrap();
}
sqlx::MySql::create_database(&url).await.unwrap();
let mut settings = get_settings(); let mut settings = get_settings();
settings.captcha.runners = Some(1); settings.captcha.runners = Some(1);
settings.database.url = url.clone(); settings.database.url = url.clone();

View File

@@ -5,15 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-or-later
--> -->
<nav class="secondary-menu"> <nav class="secondary-menu">
<input type="checkbox" class="nav-toggle" id="nav-toggle" > <input type="checkbox" class="nav-toggle" id="nav-toggle" >
<div class="secondary-menu__heading"> <div class="secondary-menu__heading">
<a class="novisit" href="/"> <a class="novisit" href="/">
<img class="secondary-menu__logo" src="<.= crate::MCAPTCHA_TRANS_ICON.0 .>" alt="<.= crate::MCAPTCHA_TRANS_ICON.1 .>" /> <img class="secondary-menu__logo" src="<.= crate::MCAPTCHA_TRANS_ICON.0 .>" alt="<.= crate::MCAPTCHA_TRANS_ICON.1 .>" />
</a> </a>
<a href="/" class="secondary-menu__brand-name"> <a href="/" class="secondary-menu__brand-name">
mCaptcha mCaptcha
</a> </a>
<label class="nav__hamburger-menu"for="nav-toggle"> <label class="nav__hamburger-menu"for="nav-toggle">
<span></span> <span></span>
<span></span> <span></span>
<span></span> <span></span>
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</div> </div>
<ul class="secondary-menu__list"> <ul class="secondary-menu__list">
<li class="secondary-menu__item"> <li class="secondary-menu__item">
<a class="secondary-menu__item-link" href="<.= crate::PAGES.home .>"> <a class="secondary-menu__item-link" href="<.= crate::PAGES.home .>">
<img class="secondary-menu__icon" src="<.= crate::HOME.0 .>" alt="<.= crate::HOME.1 .>" /> <img class="secondary-menu__icon" src="<.= crate::HOME.0 .>" alt="<.= crate::HOME.1 .>" />
<div class="secondary-menu__item-name"> <div class="secondary-menu__item-name">
Overview Overview
@@ -29,13 +29,21 @@ SPDX-License-Identifier: AGPL-3.0-or-later
</a> </a>
</li> </li>
<li class="secondary-menu__item"> <li class="secondary-menu__item">
<a class="secondary-menu__item-link" href="<.= crate::PAGES.panel.sitekey.list .>"> <a class="secondary-menu__item-link" href="<.= crate::PAGES.panel.sitekey.list .>">
<img class="secondary-menu__icon" src="<.= crate::KEY.0 .>" alt="<.= crate::KEY.1 .>" /> <img class="secondary-menu__icon" src="<.= crate::KEY.0 .>" alt="<.= crate::KEY.1 .>" />
<div class="secondary-menu__item-name"> <div class="secondary-menu__item-name">
Site Keys Site Keys
</div> </div>
</a> </a>
</li> </li>
<li class="secondary-menu__item">
<a class="secondary-menu__item-link" href="<.= crate::PAGES.panel.utils.percentile .>">
<img class="secondary-menu__icon" src="<.= crate::BAR_CHART.0 .>" alt="<.= crate::BAR_CHART.1 .>" />
<div class="secondary-menu__item-name">
Statistics
</div>
</a>
</li>
<li class="secondary-menu__item"> <li class="secondary-menu__item">
<a class="secondary-menu__item-link" href="<.= crate::PAGES.panel.settings.home .>"> <a class="secondary-menu__item-link" href="<.= crate::PAGES.panel.settings.home .>">
<img class="secondary-menu__icon" src="<.= crate::SETTINGS_ICON.0 .>" alt="<.= crate::SETTINGS_ICON.1 .>" /> <img class="secondary-menu__icon" src="<.= crate::SETTINGS_ICON.0 .>" alt="<.= crate::SETTINGS_ICON.1 .>" />

View File

@@ -0,0 +1,67 @@
<!--
SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
SPDX-License-Identifier: AGPL-3.0-or-later
-->
<. include!("../../../components/headers/index.html"); .>
<. include!("../../navbar/index.html"); .>
<div class="tmp-layout">
<. include!("../../header/index.html"); .>
<main class="panel-main">
<. include!("../../help-banner/index.html"); .>
<!-- Main content container -->
<div class="inner-container">
<div class="sitekey-form" action="<.= crate::V1_API_ROUTES.captcha.create .>" method="post">
<h1 class="form__title">
<.= PAGE .>
</h1>
<form class="settings__form" id="utils_percentile-form"
action="<.= crate::PAGES.panel.utils.percentile .>" method="post">
<. if let Some(difficulty_factor) = difficulty_factor { .>
<legend class="sitekey__level-title">
<p>Difficulty factor: <.= difficulty_factor .></p>
</legend>
<. } else { .>
<. if time.is_some() && percentile.is_some() { .>
<legend class="sitekey__level-title">
<p>Not enough inputs to compute statistics. Please try again later</p>
</legend>
<. } .>
<. } .>
<label class="settings-form__label" for="time">
Maximum time taken to solve CAPTCHA (in seconds)
<input
class="settings-form__input"
type="number"
name="time"
required
id="time"
<. if let Some(time) = time { .>
value="<.= time .>"
<. } .>
/>
</label>
<label class="settings-form__label" for="percentile">
Percentile of requests coming under time limit
<input
class="settings-form__input"
type="number"
name="percentile"
required
id="percentile"
<. if let Some(percentile) = percentile { .>
value="<.= percentile .>"
<. } .>
/>
</label>
<button class="settings__submit-btn" type="submit">Search</button>
</form>
</div>
</div>
<!-- end of container -->
<. include!("../../../components/footers.html"); .>