Compare commits

...

49 Commits

Author SHA1 Message Date
Aravinth Manivannan
c1fe45d409 fix: difficulty factor for "broke my site" should be greater than "peak sustainable traffic"
fixes: #151
2024-03-23 13:31:49 +05:30
Aravinth Manivannan
59e339f287 Merge pull request #150 from mCaptcha/fix-144
feat: add curl to the final image to aid in healthchecks
2024-03-15 18:01:12 +05:30
Aravinth Manivannan
ddcde9cf18 Merge pull request #146 from 15aura35/master
Update .env.docker-compose to use port 7000 instead of 7001
2024-03-15 18:00:17 +05:30
Aravinth Manivannan
65c92ee96e feat: add curl to the final image to aid in healthchecks
closes: https://github.com/mCaptcha/mCaptcha/issues/144
2024-03-14 20:52:37 +05:30
Aravinth Manivannan
40766ff44f Merge pull request #147 from mitallast/master
Fix: ensuring worker is ready
2024-03-12 19:41:49 +05:30
mitallast
ddc3008009 await worker is ready 2024-03-05 13:42:10 +03:00
mitallast
cba056aba6 fix wasm bigint progress handler 2024-03-05 13:13:47 +03:00
15aura35
16c975d2ec Update .env.docker-compose 2024-03-01 21:53:32 +00:00
Aravinth Manivannan
f67fdf917e Merge pull request #142 from mCaptcha/release-ci
feat: publish tagged docker images and bins
2024-02-23 15:51:56 +05:30
Aravinth Manivannan
e1746223c8 feat: publish tagged docker images and bins 2024-02-23 15:40:45 +05:30
Aravinth Manivannan
ae08c09702 fix: tmp disable bin publication 2024-02-23 15:40:27 +05:30
Aravinth Manivannan
1c9e242d7e Merge pull request #140 from mCaptcha/fix-134
fix: typecast BigInt to number in progress computation
2024-02-22 19:21:03 +05:30
Aravinth Manivannan
3cb0ca38ec fix: typecast BigInt to number in progress computation
closes: https://github.com/mCaptcha/mCaptcha/issues/134
2024-02-22 18:40:38 +05:30
Aravinth Manivannan
3cd38511fa Merge pull request #135 from SebastianGode/widget-dark
Added automatic dark mode to the widget
2024-02-19 20:13:58 +05:30
Sebastian Gode
d765bd7491 Added dark mode to widget 2024-02-13 13:30:14 +00:00
Sebastian Gode
8e33e75659 Added dark mode to widget 2024-02-13 12:59:20 +00:00
Aravinth Manivannan
c00857dd28 Merge pull request #133 from mCaptcha/aria-labels
feat: add aria labels to widget progress bar and checkbox
2024-02-04 01:09:09 +05:30
Aravinth Manivannan
9cf0eb596a feat: add aria labels to widget progress bar and checkbox 2024-02-03 19:33:45 +05:30
Aravinth Manivannan
d010a1cbd4 Merge pull request #131 from mCaptcha/fix-upload-config-file
fix: publish config file in tarball
2024-01-08 00:21:52 +05:30
Aravinth Manivannan
453be36201 fix: publish config file in tarball 2024-01-08 00:14:14 +05:30
Aravinth Manivannan
d4967626ee Merge pull request #130 from mCaptcha/document-configuration-parameters
feat: list all env vars and load in docker-compose
2024-01-07 23:52:49 +05:30
Aravinth Manivannan
2ee0a0ae5f feat: list all env vars and load in docker-compose 2024-01-07 23:35:31 +05:30
Aravinth Manivannan
5722a5327c Merge pull request #128 from mCaptcha/feat-auto-captcha
Use time (in seconds) instead of difficulty factor to describe PoW
2024-01-05 01:25:19 +05:30
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
1b2096d955 Merge pull request #127 from mCaptcha/feat-auto-captcha
feat: new dashboard page to show percentile scores on PoW performance analysis records
2024-01-04 18:47:44 +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
Aravinth Manivannan
4426057fbc feat: link to mCaptcha net blog post from the captcha creation form 2023-11-02 04:33:32 +05:30
Aravinth Manivannan
1f23999c10 fix: re-enable bin publishing with 73DAC973A9ADBB9ADCB5CDC4595A08135BA9FF73 GPG key 2023-10-30 09:29:48 +05:30
Aravinth Manivannan
0a3d93453e Merge pull request #119 from mCaptcha/fix-progress-bar
fix: create max_recorded nonce for existing captcha configs
2023-10-29 13:03:57 +00:00
Aravinth Manivannan
939fb5f8b9 fix: create max_recorded nonce for existing captcha configs 2023-10-29 18:11:06 +05:30
Aravinth Manivannan
3a787a6592 Merge pull request #118 from mCaptcha/feat-progress-bar
Feat progress bar
2023-10-29 01:20:04 +00:00
Aravinth Manivannan
9dfb0713ad feat: progress bar and incremental PoW generation 2023-10-29 06:28:21 +05:30
Aravinth Manivannan
ad4582cc16 feat: record and fetch max recorded nonces 2023-10-29 06:27:58 +05:30
Aravinth Manivannan
77e4a9c473 feat: use node@v20 2023-10-29 06:27:15 +05:30
Aravinth Manivannan
b6497882d7 feat: track maximum recorded nonce for captcha levels to render progress bar 2023-10-29 06:18:01 +05:30
62 changed files with 2660 additions and 418 deletions

43
.env.docker-compose Normal file
View File

@@ -0,0 +1,43 @@
MCAPTCHA_debug=false
MCAPTCHA_commercial=false
MCAPTCHA_source_code=https://github.com/mCaptcha/mCaptcha
MCAPTCHA_allow_registration=false
MCAPTCHA_allow_demo=false
# database
DATABASE_URL=postgres://postgres:password@mcaptcha_postgres:5432/postgres
MCAPTCHA_database_POOL=4
# redis
MCAPTCHA_redis_URL=redis://mcaptcha_redis
MCAPTCHA_redis_POOL=4
# server
PORT=7000
MCAPTCHA_server_DOMAIN=localhost
MCAPTCHA__server_COOKIE_SECRET=pleasereplacethiswithrandomstring # PLEASE SET RANDOM STRING. MIN LENGTH=32
MCAPTCHA__server_IP= 0.0.0.0
# captcha
MCAPTCHA_captcha_SALT=pleasereplacethiswithrandomstring # PLEASE SET RANDOM STRING. MIN LENGTH=32
MCAPTCHA_captcha_GC=30
MCAPTCHA_captcha_RUNNERS=4
MCAPTCHA_captcha_QUEUE_LENGTH=2000
MCAPTCHA_captcha_ENABLE_STATS=true
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_difficulty=50000 # almost instant solution
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_difficulty=3000000 # greater than 3.5s
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_difficulty=5000000 # roughly 1.5s
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_duration=30 # cooldown period in seconds
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_avg_traffic_time=1 # almost instant solution
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_peak_sustainable_traffic_time=3
MCAPTCHA_captcha_DEFAULT_DIFFICULTY_STRATEGY_broke_my_site_traffic_time=5
# SMTP
#MCAPTCHA_smtp_FROM=
#MCAPTCHA_smtp_REPLY=
#MCAPTCHA_smtp_URL=
#MCAPTCHA_smtp_USERNAME=
#MCAPTCHA_smtp_PASSWORD=
#MCAPTCHA_smtp_PORT=

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

@@ -129,12 +129,12 @@ jobs:
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha' if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha'
run: make docker-publish run: make docker-publish
# - name: publish bins - name: publish bins
# if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha' if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha'
# run: ./scripts/publish.sh publish master latest $DUMBSERVE_PASSWORD run: ./scripts/publish.sh publish master latest $DUMBSERVE_PASSWORD
# env: env:
# DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }} DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
# GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }} GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
- name: generate documentation - name: generate documentation
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha') if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha')

125
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,125 @@
name: Publish release
on:
release:
type: [published]
jobs:
build_and_test:
strategy:
fail-fast: false
name: x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mcaptcha-redis:
image: mcaptcha/cache
ports:
- 6379:6379
mcaptcha-smtp:
image: maildev/maildev
env:
MAILDEV_WEB_PORT: "1080"
MAILDEV_INCOMING_USER: "admin"
MAILDEV_INCOMING_PASS: "password"
ports:
- 1080:1080
- 10025:1025
maria:
image: mariadb:10
env:
MARIADB_USER: "maria"
MARIADB_PASSWORD: "password"
MARIADB_ROOT_PASSWORD: "password"
MARIADB_DATABASE: "maria"
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=10
ports:
- 3306:3306
steps:
- uses: actions/checkout@v4
- name: configure GPG key
run: echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
env:
RELEASE_BOT_GPG_SIGNING_KEY: ${{ secrets.RELEASE_BOT_GPG_SIGNING_KEY }}
- name: Set release version
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: load env
run: |
source .env_sample \
&& echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV \
&& echo "MARIA_DATABASE_URL=$MARIA_DATABASE_URL" >> $GITHUB_ENV
- uses: actions/setup-node@v2
with:
node-version: "20.0.0"
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: install nightwatch dep
run: sudo apt-get install xvfb
- name: Run migrations
run: make migrate
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
- name: build
run: make
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
- name: lint frontend
run: yarn lint
- name: run tests
run: make test
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
- name: run integration tests
run: make test.integration
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: mcaptcha
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: build docker images
run: docker build -t mcaptcha/mcaptcha:${RELEASE_VERSION} .
- name: publish docker images
run: docker push mcaptcha/mcaptcha:${RELEASE_VERSION}
- name: publish bins
run: ./scripts/publish.sh publish $RELEASE_VERSION latest $DUMBSERVE_PASSWORD
env:
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}

View File

@@ -1,32 +1,32 @@
name: Create binary for release #name: Create binary for release
#
# Only on tags that start with a "v" ## Only on tags that start with a "v"
on: #on:
push: # push:
tags: # tags:
- "v*" # - "v*"
#
jobs: #jobs:
build: # build:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
strategy: # strategy:
fail-fast: false # fail-fast: false
matrix: # matrix:
include: # include:
- target: x86_64-pc-windows-gnu # - target: x86_64-pc-windows-gnu
archive: zip # archive: zip
- target: x86_64-unknown-linux-musl # - target: x86_64-unknown-linux-musl
archive: tar.gz tar.xz # archive: tar.gz tar.xz
- target: x86_64-apple-darwin # - target: x86_64-apple-darwin
archive: zip # archive: zip
steps: # steps:
- name: Checkout # - name: Checkout
uses: actions/checkout@v3 # uses: actions/checkout@v3
#
- name: Compile and release # - name: Compile and release
uses: rust-build/rust-build.action@v1.3.2 # uses: rust-build/rust-build.action@v1.3.2
env: # env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: # with:
RUSTTARGET: ${{ matrix.target }} # RUSTTARGET: ${{ matrix.target }}
ARCHIVE_TYPES: ${{ matrix.archive }} # ARCHIVE_TYPES: ${{ matrix.archive }}

2
.nvmrc
View File

@@ -1 +1 @@
18 20

View File

@@ -31,6 +31,10 @@ RUN cargo build --release
FROM debian:bookworm as mCaptcha FROM debian:bookworm as mCaptcha
LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends curl
RUN useradd -ms /bin/bash -u 1001 mcaptcha RUN useradd -ms /bin/bash -u 1001 mcaptcha
WORKDIR /home/mcaptcha WORKDIR /home/mcaptcha
COPY --from=rust /src/target/release/mcaptcha /usr/local/bin/ COPY --from=rust /src/target/release/mcaptcha /usr/local/bin/

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,
@@ -292,6 +299,32 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
/// Get all psuedo IDs /// Get all psuedo IDs
async fn analytics_get_all_psuedo_ids(&self, page: usize) -> DBResult<Vec<String>>; async fn analytics_get_all_psuedo_ids(&self, page: usize) -> DBResult<Vec<String>>;
/// Track maximum nonce received against captcha levels
async fn update_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: u32,
latest_nonce: u32,
) -> DBResult<()>;
/// Get maximum nonce tracked so far for captcha levels
async fn get_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: 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)]
@@ -357,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,11 +352,82 @@ 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();
// analytics end // analytics end
// nonce tracking start
assert_eq!(
db.get_max_nonce_for_level(c.key, l[0].difficulty_factor)
.await
.unwrap(),
0
);
db.update_max_nonce_for_level(c.key, l[0].difficulty_factor, 1000)
.await
.unwrap();
assert_eq!(
db.get_max_nonce_for_level(c.key, l[0].difficulty_factor)
.await
.unwrap(),
1000
);
db.update_max_nonce_for_level(c.key, l[0].difficulty_factor, 10_000)
.await
.unwrap();
assert_eq!(
db.get_max_nonce_for_level(c.key, l[0].difficulty_factor)
.await
.unwrap(),
10_000
);
// nonce tracking end
assert_eq!(db.fetch_solve(p.username, c.key).await.unwrap().len(), 1); assert_eq!(db.fetch_solve(p.username, c.key).await.unwrap().len(), 1);
assert_eq!( assert_eq!(
db.fetch_config_fetched(p.username, c.key) db.fetch_config_fetched(p.username, c.key)

View File

@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO\n mcaptcha_track_nonce (level_id, nonce)\n VALUES ((\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)\n AND\n difficulty_factor = ?\n AND\n visitor_threshold = ?\n ), ?);",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "216478d53870d7785cd0be43f030883ab79eaafb558d9197d09aea3adbd7b0bc"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE mcaptcha_track_nonce SET nonce = ?\n WHERE level_id = (\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)\n AND\n difficulty_factor = ?\n )\n AND nonce <= ?;",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "349ba17ff197aca7ee9fbd43e227d181c27ae04702fd6bdb6ddc32aab3bcb1ea"
}

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,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO\n mcaptcha_track_nonce (level_id, nonce)\n VALUES ((\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key =?)\n AND\n difficulty_factor = ?\n ), ?);",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "9def82dcec9c8d477824182bb2f71044cc264cf2073ab4f60a0000b435ed0f0b"
}

View File

@@ -0,0 +1,25 @@
{
"db_name": "MySQL",
"query": "SELECT nonce FROM mcaptcha_track_nonce\n WHERE level_id = (\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)\n AND\n difficulty_factor = ?\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "nonce",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "b739ec4cfab1ec60947106c8112e931510c3a50a1606facdde0c0ebb540d5beb"
}

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

@@ -0,0 +1,12 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS mcaptcha_track_nonce (
level_id INTEGER NOT NULL,
nonce INTEGER NOT NULL DEFAULT 0,
ID INT auto_increment,
PRIMARY KEY(ID),
CONSTRAINT `fk_mcaptcha_track_nonce_level_id`
FOREIGN KEY (level_id)
REFERENCES mcaptcha_levels (level_id)
ON DELETE CASCADE
ON UPDATE CASCADE
);

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;
} }
@@ -433,6 +432,39 @@ impl MCDatabase for Database {
futs.push(fut); futs.push(fut);
} }
try_join_all(futs)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
let mut futs = Vec::with_capacity(levels.len());
for level in levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO
mcaptcha_track_nonce (level_id, nonce)
VALUES ((
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)
AND
difficulty_factor = ?
AND
visitor_threshold = ?
), ?);",
&captcha_key,
difficulty_factor,
visitor_threshold,
0,
)
.execute(&self.pool);
futs.push(fut);
}
try_join_all(futs) try_join_all(futs)
.await .await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?; .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
@@ -1087,6 +1119,220 @@ impl MCDatabase for Database {
Ok(res.drain(0..).map(|r| r.psuedo_id).collect()) Ok(res.drain(0..).map(|r| r.psuedo_id).collect())
} }
/// Track maximum nonce received against captcha levels
async fn update_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: u32,
latest_nonce: u32,
) -> DBResult<()> {
let latest_nonce = latest_nonce as i64;
sqlx::query!(
"UPDATE mcaptcha_track_nonce SET nonce = ?
WHERE level_id = (
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)
AND
difficulty_factor = ?
)
AND nonce <= ?;",
latest_nonce,
&captcha_key,
difficulty_factor as i64,
latest_nonce
)
.execute(&self.pool).await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Get maximum nonce tracked so far for captcha levels
async fn get_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: u32,
) -> DBResult<u32> {
struct X {
nonce: i32,
}
async fn inner_get_max_nonce(
pool: &MySqlPool,
captcha_key: &str,
difficulty_factor: u32,
) -> DBResult<X> {
sqlx::query_as!(
X,
"SELECT nonce FROM mcaptcha_track_nonce
WHERE level_id = (
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key = ?)
AND
difficulty_factor = ?
);",
&captcha_key,
difficulty_factor as i32,
)
.fetch_one(pool).await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))
}
let res = inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await;
if let Err(DBError::CaptchaNotFound) = res {
sqlx::query!(
"INSERT INTO
mcaptcha_track_nonce (level_id, nonce)
VALUES ((
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE captcha_key =?)
AND
difficulty_factor = ?
), ?);",
&captcha_key,
difficulty_factor as i32,
0,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
let res =
inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await?;
Ok(res.nonce as u32)
} else {
let res = res?;
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,17 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO\n mcaptcha_track_nonce (level_id, nonce)\n VALUES ((\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))\n AND\n difficulty_factor = $2\n AND\n visitor_threshold = $3\n ), $4);",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "133ee23ab5ac7c664a86b6edfaa8da79281b6d1f5ba33c642a6ea1b0682fe0b0"
}

View File

@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "SELECT nonce FROM mcaptcha_track_nonce\n WHERE level_id = (\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))\n AND\n difficulty_factor = $2\n );",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "nonce",
"type_info": "Int4"
}
],
"parameters": {
"Left": [
"Text",
"Int4"
]
},
"nullable": [
false
]
},
"hash": "96f1f1e45144d5add6c4ba4cd2df8eda6043bc8cd6952787f92a687fef778a6e"
}

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,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO\n mcaptcha_track_nonce (level_id, nonce)\n VALUES ((\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))\n AND\n difficulty_factor = $2\n ), $3);",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "e0088cf77c1c3a0184f35d1899a6168023fba021adf281cf1c8f9e8ccfe3a03e"
}

View File

@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE mcaptcha_track_nonce SET nonce = $3\n WHERE level_id = (\n SELECT\n level_id\n FROM\n mcaptcha_levels\n WHERE\n config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))\n AND\n difficulty_factor = $2\n )\n AND nonce <= $3;",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "e33ee14cf76cd09d9a157b8784a3fe25b89eaca105aa30e479d31b756cd5c88b"
}

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

@@ -0,0 +1,6 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS mcaptcha_track_nonce (
nonce INTEGER NOT NULL DEFAULT 0,
level_id INTEGER references mcaptcha_levels(level_id) ON DELETE CASCADE,
ID SERIAL PRIMARY KEY NOT NULL
);

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;
} }
@@ -445,6 +444,38 @@ impl MCDatabase for Database {
futs.push(fut); futs.push(fut);
} }
try_join_all(futs)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
let mut futs = Vec::with_capacity(levels.len());
for level in levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO
mcaptcha_track_nonce (level_id, nonce)
VALUES ((
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
AND
difficulty_factor = $2
AND
visitor_threshold = $3
), $4);",
&captcha_key,
difficulty_factor,
visitor_threshold,
0,
)
.execute(&self.pool);
futs.push(fut);
}
try_join_all(futs) try_join_all(futs)
.await .await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?; .map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
@@ -638,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,
@@ -675,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
@@ -1097,6 +1179,154 @@ impl MCDatabase for Database {
Ok(res.drain(0..).map(|r| r.psuedo_id).collect()) Ok(res.drain(0..).map(|r| r.psuedo_id).collect())
} }
/// Track maximum nonce received against captcha levels
async fn update_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: u32,
latest_nonce: u32,
) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_track_nonce SET nonce = $3
WHERE level_id = (
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
AND
difficulty_factor = $2
)
AND nonce <= $3;",
&captcha_key,
difficulty_factor as i32,
latest_nonce as i32,
)
.execute(&self.pool).await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Get maximum nonce tracked so far for captcha levels
async fn get_max_nonce_for_level(
&self,
captcha_key: &str,
difficulty_factor: u32,
) -> DBResult<u32> {
struct X {
nonce: i32,
}
async fn inner_get_max_nonce(
pool: &PgPool,
captcha_key: &str,
difficulty_factor: u32,
) -> DBResult<X> {
sqlx::query_as!(
X,
"SELECT nonce FROM mcaptcha_track_nonce
WHERE level_id = (
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
AND
difficulty_factor = $2
);",
&captcha_key,
difficulty_factor as i32,
)
.fetch_one(pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))
}
let res = inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await;
if let Err(DBError::CaptchaNotFound) = res {
sqlx::query!(
"INSERT INTO
mcaptcha_track_nonce (level_id, nonce)
VALUES ((
SELECT
level_id
FROM
mcaptcha_levels
WHERE
config_id = (SELECT config_id FROM mcaptcha_config WHERE key = ($1))
AND
difficulty_factor = $2
), $3);",
&captcha_key,
difficulty_factor as i32,
0,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
let res =
inner_get_max_nonce(&self.pool, captcha_key, difficulty_factor).await?;
Ok(res.nonce as u32)
} else {
let res = res?;
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)]
@@ -1166,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

@@ -9,11 +9,8 @@ services:
image: mcaptcha/mcaptcha:latest image: mcaptcha/mcaptcha:latest
ports: ports:
- 7000:7000 - 7000:7000
environment: env_file:
DATABASE_URL: postgres://postgres:password@mcaptcha_postgres:5432/postgres # set password at placeholder - .env.docker-compose
MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
RUST_LOG: "debug"
PORT: 7000
depends_on: depends_on:
- mcaptcha_postgres - mcaptcha_postgres
- mcaptcha_redis - mcaptcha_redis

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

@@ -37,8 +37,8 @@
"webpack-dev-server": "^4.15.1" "webpack-dev-server": "^4.15.1"
}, },
"dependencies": { "dependencies": {
"@mcaptcha/pow-wasm": "^0.1.0-rc1", "@mcaptcha/pow_sha256-polyfill": "^0.1.0-rc2",
"@mcaptcha/pow_sha256-polyfill": "^0.1.0-rc1", "@mcaptcha/vanilla-glue": "^0.1.0-rc1",
"@mcaptcha/vanilla-glue": "^0.1.0-rc1" "@mcaptcha/pow-wasm": "^0.1.0-rc2"
} }
} }

View File

@@ -17,7 +17,7 @@ DUMBSERVE_PASSWORD=$4
DUMBSERVE_HOST="https://$DUMBSERVE_USERNAME:$DUMBSERVE_PASSWORD@dl.mcaptcha.org" DUMBSERVE_HOST="https://$DUMBSERVE_USERNAME:$DUMBSERVE_PASSWORD@dl.mcaptcha.org"
NAME=mcaptcha NAME=mcaptcha
KEY=0CBABF3084E84E867A76709750BE39D10ECE01FB KEY=73DAC973A9ADBB9ADCB5CDC4595A08135BA9FF73
TMP_DIR=$(mktemp -d) TMP_DIR=$(mktemp -d)
FILENAME="$NAME-$2-linux-amd64" FILENAME="$NAME-$2-linux-amd64"
@@ -44,6 +44,7 @@ copy() {
mkdir $TARGET_DIR/docs mkdir $TARGET_DIR/docs
cp docs/DEPLOYMENT.md $TARGET_DIR/docs cp docs/DEPLOYMENT.md $TARGET_DIR/docs
cp docs/CONFIGURATION.md $TARGET_DIR/docs cp docs/CONFIGURATION.md $TARGET_DIR/docs
cp config/default.toml $TARGET_DIR/config.toml
get_bin get_bin
} }

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

@@ -5,6 +5,7 @@
//use actix::prelude::*; //use actix::prelude::*;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::pow::PoWConfig;
use libmcaptcha::{ use libmcaptcha::{
defense::LevelBuilder, master::messages::AddSiteBuilder, DefenseBuilder, defense::LevelBuilder, master::messages::AddSiteBuilder, DefenseBuilder,
MCaptchaBuilder, MCaptchaBuilder,
@@ -21,7 +22,13 @@ pub struct GetConfigPayload {
pub key: String, pub key: String,
} }
// API keys are mcaptcha actor names #[derive(Clone, Serialize, Deserialize, Debug)]
pub struct ApiPoWConfig {
pub string: String,
pub difficulty_factor: u32,
pub salt: String,
pub max_recorded_nonce: u32,
}
/// get PoW configuration for an mcaptcha key /// get PoW configuration for an mcaptcha key
#[my_codegen::post(path = "V1_API_ROUTES.pow.get_config()")] #[my_codegen::post(path = "V1_API_ROUTES.pow.get_config()")]
@@ -35,52 +42,34 @@ pub async fn get_config(
} }
let payload = payload.into_inner(); let payload = payload.into_inner();
match data.captcha.get_pow(payload.key.clone()).await { let config: ServiceResult<PoWConfig> =
Ok(Some(config)) => { match data.captcha.get_pow(payload.key.clone()).await {
data.stats.record_fetch(&data, &payload.key).await?; Ok(Some(config)) => Ok(config),
Ok(HttpResponse::Ok().json(config)) Ok(None) => {
} init_mcaptcha(&data, &payload.key).await?;
Ok(None) => { let config = data
init_mcaptcha(&data, &payload.key).await?; .captcha
let config = data .get_pow(payload.key.clone())
.captcha .await
.get_pow(payload.key.clone()) .expect("mcaptcha should be initialized and ready to go");
.await Ok(config.unwrap())
.expect("mcaptcha should be initialized and ready to go"); }
// background it. would require data::Data to be static Err(e) => Err(e.into()),
// to satidfy lifetime };
data.stats.record_fetch(&data, &payload.key).await?; let config = config?;
Ok(HttpResponse::Ok().json(config)) let max_nonce = data
} .db
Err(e) => Err(e.into()), .get_max_nonce_for_level(&payload.key, config.difficulty_factor)
} .await?;
data.stats.record_fetch(&data, &payload.key).await?;
// match res.exists { let config = ApiPoWConfig {
// Some(true) => { string: config.string,
// match data.captcha.get_pow(payload.key.clone()).await { difficulty_factor: config.difficulty_factor,
// Ok(Some(config)) => { salt: config.salt,
// record_fetch(&payload.key, &data.db).await; max_recorded_nonce: max_nonce,
// Ok(HttpResponse::Ok().json(config)) };
// } Ok(HttpResponse::Ok().json(config))
// Ok(None) => {
// init_mcaptcha(&data, &payload.key).await?;
// let config = data
// .captcha
// .get_pow(payload.key.clone())
// .await
// .expect("mcaptcha should be initialized and ready to go");
// // background it. would require data::Data to be static
// // to satidfy lifetime
// record_fetch(&payload.key, &data.db).await;
// Ok(HttpResponse::Ok().json(config))
// }
// Err(e) => Err(e.into()),
// }
// }
//
// Some(false) => Err(ServiceError::TokenNotFound),
// None => Err(ServiceError::TokenNotFound),
// }
} }
/// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master. /// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master.
/// ///

View File

@@ -65,6 +65,7 @@ pub async fn verify_pow(
let payload = payload.into_inner(); let payload = payload.into_inner();
let worker_type = payload.worker_type.clone(); let worker_type = payload.worker_type.clone();
let time = payload.time; let time = payload.time;
let nonce = payload.nonce;
let (res, difficulty_factor) = data.captcha.verify_pow(payload.into(), ip).await?; let (res, difficulty_factor) = data.captcha.verify_pow(payload.into(), ip).await?;
data.stats.record_solve(&data, &key).await?; data.stats.record_solve(&data, &key).await?;
if let (Some(time), Some(worker_type)) = (time, worker_type) { if let (Some(time), Some(worker_type)) = (time, worker_type) {
@@ -75,6 +76,9 @@ pub async fn verify_pow(
}; };
data.db.analysis_save(&key, &analytics).await?; data.db.analysis_save(&key, &analytics).await?;
} }
data.db
.update_max_nonce_for_level(&key, difficulty_factor, nonce as u32)
.await?;
let payload = ValidationToken { token: res }; let payload = ValidationToken { token: res };
Ok(HttpResponse::Ok().json(payload)) Ok(HttpResponse::Ok().json(payload))
} }

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

@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<. } .> <. } .>
<label class="sitekey-form__label" for="publish_benchmarks"> <label class="sitekey-form__label" for="publish_benchmarks">
Anonymously publish CAPTCHA performance statistics to help other webmasters Anonymously publish CAPTCHA performance statistics to help other webmasters. <a href="https://mcaptcha.org/blog/introducing-mcaptcha-net">Please see here for more info</a>.
<input <input
class="sitekey-form__input" class="sitekey-form__input"
type="checkbox" type="checkbox"

View File

@@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<label class="sitekey-form__label" for="publish_benchmarks"> <label class="sitekey-form__label" for="publish_benchmarks">
Anonymously publish CAPTCHA performance statistics to help other webmasters Anonymously publish CAPTCHA performance statistics to help other webmasters. <a href="https://mcaptcha.org/blog/introducing-mcaptcha-net">Please see here for more info</a>.
<input <input
class="sitekey-form__input" class="sitekey-form__input"
type="checkbox" type="checkbox"

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"); .>

View File

@@ -56,53 +56,41 @@ type messageTextReturn = {
error: () => void; error: () => void;
}; };
export const messageText = (): messageTextReturn => { export const BEFORE = "I'm not a robot";
const beforeID = "widget__verification-text--before"; export const DURING = "Processing...";
const duringID = "widget__verification-text--during"; export const AFTER = "Verified!";
const errorID = "widget__verification-text--error"; export const ERROR = "Something went wrong";
const afterID = "widget__verification-text--after";
const before = new LazyElement(beforeID); export const messageText = (): messageTextReturn => {
const after = new LazyElement(afterID); const conatinerID = "widget__verification-text";
const during = new LazyElement(duringID);
const error = new LazyElement(errorID); const container = new LazyElement(conatinerID);
/** runner fn to display HTMLElement **/ /** runner fn to display HTMLElement **/
const showMsg = (e: HTMLElement) => (e.style.display = "block"); const showMsg = (value: string) => {
/** runner fn to hide HTMLElement **/ container.get().innerText = value;
const hideMsg = (e: HTMLElement) => (e.style.display = "none"); btn().ariaValueText = value;
};
return { return {
/** display "before" message **/ /** display "before" message **/
before: () => { before: () => {
showMsg(before.get()); showMsg(BEFORE);
hideMsg(after.get());
hideMsg(during.get());
hideMsg(error.get());
}, },
/** display "after" message **/ /** display "after" message **/
after: () => { after: () => {
hideMsg(before.get()); showMsg(AFTER);
showMsg(after.get());
hideMsg(during.get());
hideMsg(error.get());
}, },
/** display "during" message **/ /** display "during" message **/
during: () => { during: () => {
hideMsg(before.get()); showMsg(DURING);
hideMsg(after.get());
showMsg(during.get());
hideMsg(error.get());
}, },
/** display "error" message **/ /** display "error" message **/
error: () => { error: () => {
hideMsg(before.get()); showMsg(ERROR);
hideMsg(after.get());
hideMsg(during.get());
showMsg(error.get());
}, },
}; };
}; };

View File

@@ -6,51 +6,57 @@ SPDX-License-Identifier: MIT OR Apache-2.0
<. include!("../components/headers/widget-headers.html"); .> <. include!("../components/headers/widget-headers.html"); .>
<body> <body>
<form class="widget__contaienr"> <main class="widget__container">
<noscript> <form class="widget__inner-container">
<div class="widget__noscript-container"> <noscript>
<span class="widget__noscript-warning"> <div class="widget__noscript-container">
Please enable JavaScript to receive mCaptcha challenge <span class="widget__noscript-warning">
</span> Please enable JavaScript to receive mCaptcha challenge
<a class="widget__source-code" href="https://github.com/mCaptcha"> </span>
Read our source code <a class="widget__source-code" href="https://github.com/mCaptcha">
</a> Read our source code
</div> </a>
</noscript> </div>
<label class="widget__verification-container" for="widget__verification-checkbox"> </noscript>
<input <label class="widget__verification-container" for="widget__verification-checkbox">
id="widget__verification-checkbox" <span id="widget__verification-text"
class="widget__verification-checkbox" >I'm not a robot</span>
type="checkbox" /> <input disabled
<span id="widget__verification-text--before">I'm not a robot</span> id="widget__verification-checkbox"
<span id="widget__verification-text--during">Processing...</span> aria-valuenow="I'm not a robot"
<span id="widget__verification-text--after">Verified!</span> aria-checked="false"
<span id="widget__verification-text--error">Something went wrong</span> role="checkbox"
</label> class="widget__verification-checkbox"
<div class="widget__mcaptcha-details"> type="checkbox" />
<a href="<.= crate::PKG_HOMEPAGE .>" </label>
class="widget__mcaptcha-logo-container" <div class="widget__mcaptcha-details">
target="_blank" <a href="<.= crate::PKG_HOMEPAGE .>"
> class="widget__mcaptcha-logo-container"
<img target="_blank"
class="widget__mcaptcha-logo" >
src="<.= crate::FILES.get("./static/cache/img/icon-trans.png").unwrap().>" <img
alt="mCaptcha logo" class="widget__mcaptcha-logo"
/> src="<.= crate::FILES.get("./static/cache/img/icon-trans.png").unwrap().>"
<p class="widget__mcaptcha-brand-name">mCaptcha</p> alt="mCaptcha logo"
</a> />
<div class="widget__mcaptcha-info-container"> <p class="widget__mcaptcha-brand-name">mCaptcha</p>
<a class="widget__mcaptcha-info-link"
target="_blank"
href="<.= crate::PKG_HOMEPAGE .><.= crate::PAGES.privacy .>">
Privacy
</a>
<a class="widget__mcaptcha-info-link"
target="_blank"
href="<.= crate::PKG_HOMEPAGE .><.= crate::PAGES.security .>">
Terms
</a> </a>
<div class="widget__mcaptcha-info-container">
<a class="widget__mcaptcha-info-link"
target="_blank"
href="<.= crate::PKG_HOMEPAGE .><.= crate::PAGES.privacy .>">
Privacy
</a>
<a class="widget__mcaptcha-info-link"
target="_blank"
href="<.= crate::PKG_HOMEPAGE .><.= crate::PAGES.security .>">
Terms
</a>
</div>
</div> </div>
</div> </form>
</form> <div class="progress__bar"><div
aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"
role="progressbar" class="progress__fill"></div></div>
</main>
<.include!("./footer.html"); .> <.include!("./footer.html"); .>

View File

@@ -3,7 +3,7 @@
// //
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
import { Work, ServiceWorkerWork } from "./types"; import {Work, ServiceWorkerMessage} from "./types";
import fetchPoWConfig from "./fetchPoWConfig"; import fetchPoWConfig from "./fetchPoWConfig";
import sendWork from "./sendWork"; import sendWork from "./sendWork";
import sendToParent from "./sendToParent"; import sendToParent from "./sendToParent";
@@ -12,7 +12,17 @@ import * as CONST from "./const";
import "./main.scss"; import "./main.scss";
let LOCK = false; let LOCK = false;
const worker = new Worker("/bench.js");
const workerPromise = new Promise<Worker>((res) => {
const worker = new Worker("/bench.js");
worker.onmessage = (event: MessageEvent) => {
const message: ServiceWorkerMessage = event.data;
if(message.type === "ready") {
console.log("worker ready");
res(worker);
}
};
});
/** add mcaptcha widget element to DOM */ /** add mcaptcha widget element to DOM */
export const registerVerificationEventHandler = (): void => { export const registerVerificationEventHandler = (): void => {
@@ -20,10 +30,23 @@ export const registerVerificationEventHandler = (): void => {
document.querySelector(".widget__verification-container") document.querySelector(".widget__verification-container")
); );
verificationContainer.style.display = "flex"; verificationContainer.style.display = "flex";
CONST.btn().addEventListener("click", (e) => solveCaptchaRunner(e)); workerPromise.then((worker: Worker) => {
const btn = CONST.btn();
btn.disabled = false;
btn.addEventListener("click", (e) => solveCaptchaRunner(worker, e));
});
}; };
export const solveCaptchaRunner = async (e: Event): Promise<void> => { export const solveCaptchaRunner = async (worker: Worker, e: Event): Promise<void> => {
const PROGRESS_FILL = <HTMLElement>document.querySelector(".progress__fill");
const setWidth = (width: number) => {
PROGRESS_FILL.style.width = `${width}%`;
PROGRESS_FILL.ariaValueNow = <any>parseInt(<any>width);
};
let width = 0;
if (LOCK) { if (LOCK) {
e.preventDefault(); e.preventDefault();
return; return;
@@ -32,7 +55,10 @@ export const solveCaptchaRunner = async (e: Event): Promise<void> => {
try { try {
LOCK = true; LOCK = true;
if (CONST.btn().checked == false) { if (CONST.btn().checked == false) {
width = 0;
setWidth(width);
CONST.messageText().before(); CONST.messageText().before();
CONST.btn().ariaChecked = <any>false;
LOCK = false; LOCK = false;
return; return;
} }
@@ -43,32 +69,50 @@ export const solveCaptchaRunner = async (e: Event): Promise<void> => {
CONST.messageText().during(); CONST.messageText().during();
// 1. get config // 1. get config
const config = await fetchPoWConfig(); const config = await fetchPoWConfig();
const max_recorded_nonce = config.max_recorded_nonce;
// 2. prove work // 2. prove work
worker.postMessage(config); worker.postMessage(config);
worker.onmessage = async (event: MessageEvent) => { worker.onmessage = async (event: MessageEvent) => {
const resp: ServiceWorkerWork = event.data; const resp: ServiceWorkerMessage = event.data;
console.log(
`Proof generated. Difficuly: ${config.difficulty_factor} Duration: ${resp.work.time}`
);
const proof: Work = { if (resp.type === "work") {
key: CONST.sitekey(), width = 80;
string: config.string, setWidth(width);
nonce: resp.work.nonce, console.log(
result: resp.work.result, `Proof generated. Difficuly: ${config.difficulty_factor} Duration: ${resp.value.work.time}`
time: Math.trunc(resp.work.time), );
worker_type: resp.work.worker_type,
};
// 3. submit work const proof: Work = {
const token = await sendWork(proof); key: CONST.sitekey(),
// 4. send token string: config.string,
sendToParent(token); nonce: resp.value.work.nonce,
// 5. mark checkbox checked result: resp.value.work.result,
CONST.btn().checked = true; time: Math.trunc(resp.value.work.time),
CONST.messageText().after(); worker_type: resp.value.work.worker_type,
LOCK = false; };
width = 90;
setWidth(width);
// 3. submit work
const token = await sendWork(proof);
// 4. send token
sendToParent(token);
// 5. mark checkbox checked
CONST.btn().checked = true;
CONST.btn().ariaChecked = <any>true;
width = 100;
setWidth(width);
CONST.messageText().after();
LOCK = false;
}
if (resp.type === "progress") {
if (width < 80) {
width = resp.nonce / max_recorded_nonce * 100;
setWidth(width);
}
console.log(`received nonce ${resp.nonce}`);
}
}; };
} catch (e) { } catch (e) {
CONST.messageText().error(); CONST.messageText().error();

View File

@@ -7,106 +7,140 @@
@import "../reset"; @import "../reset";
.widget__contaienr { body {
align-items: center; display: flex;
box-sizing: border-box; flex-direction: column;
display: flex; width: 100%;
height: 100%; }
.widget__container {
align-items: center;
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
margin: auto 0;
}
.widget__inner-container {
align-items: center;
box-sizing: border-box;
display: flex;
height: 100%;
width: 100%;
background-color: #f6f6f6;
border: 2px solid #e5e5e5;
} }
.widget__noscript-container { .widget__noscript-container {
display: flex; display: flex;
font-size: 0.7rem; font-size: 0.7rem;
line-height: 20px; line-height: 20px;
flex-direction: column; flex-direction: column;
padding: 5px; padding: 5px;
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
margin: auto; margin: auto;
} }
.widget__noscript-warning { .widget__noscript-warning {
display: block; display: block;
margin: auto; margin: auto;
flex: 2; flex: 2;
width: 100%; width: 100%;
margin: auto; margin: auto;
} }
.widget__source-code { .widget__source-code {
display: block; display: block;
flex: 1; flex: 1;
} }
.widget__verification-container { .widget__verification-container {
align-items: center; align-items: center;
display: none; display: flex;
line-height: 30px; flex-direction: row-reverse;
font-size: 1rem; line-height: 30px;
font-size: 1rem;
} }
.widget__verification-checkbox { .widget__verification-checkbox {
width: 30px; width: 30px;
height: 30px; height: 30px;
margin: 0 10px; margin: 0 10px;
}
#widget__verification-text--during {
display: none;
}
#widget__verification-text--after {
display: none;
color: green;
}
#widget__verification-text--error {
display: none;
color: red;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--before {
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--during {
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--error {
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--after {
display: block;
} }
.widget__mcaptcha-details { .widget__mcaptcha-details {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin-left: auto; margin-left: auto;
margin-right: 10px; margin-right: 10px;
} }
.widget__mcaptcha-brand-name { .widget__mcaptcha-brand-name {
font-size: 0.7rem; font-size: 0.7rem;
font-weight: 600; font-weight: 600;
margin: auto; margin: auto;
text-align: center; text-align: center;
} }
.widget__mcaptcha-logo { .widget__mcaptcha-logo {
display: block; display: block;
width: 35px; width: 35px;
margin: auto; margin: auto;
} }
.widget__mcaptcha-info-container { .widget__mcaptcha-info-container {
display: flex; display: flex;
margin: auto; margin: auto;
} }
.widget__mcaptcha-info-link { .widget__mcaptcha-info-link {
font-size: 0.5rem; font-size: 0.5rem;
margin: 2px; margin: 2px;
}
@media (prefers-color-scheme: dark) {
.widget__container {
background-color: #1c1c1c;
}
.widget__inner-container {
background-color: #1c1c1c;
border: 2px solid #656569;
}
.widget__verification-container {
color: rgb(232, 230, 227);
}
.widget__mcaptcha-brand-name {
color: #7d94f9;
}
.widget__mcaptcha-info-link {
color: #7d94f9;
}
}
/* progress bar courtesy of https://codepen.io/Bizzy-Coding/pen/poOymVJ?editors=1111 */
.progress__bar {
position: relative;
height: 5px;
width: 100%;
background: #fff;
border-radius: 15px;
}
.progress__fill {
background: #65a2e0;
border-radius: 15px;
height: 100%;
width: 0%;
}
@media (prefers-color-scheme: dark) {
.progress__bar {
background: unset;
}
} }

View File

@@ -3,7 +3,7 @@
// //
// SPDX-License-Identifier: MIT OR Apache-2.0 // SPDX-License-Identifier: MIT OR Apache-2.0
import { gen_pow } from "@mcaptcha/pow-wasm"; import { stepped_gen_pow } from "@mcaptcha/pow-wasm";
import * as p from "@mcaptcha/pow_sha256-polyfill"; import * as p from "@mcaptcha/pow_sha256-polyfill";
import { WasmWork, PoWConfig, SubmitWork } from "./types"; import { WasmWork, PoWConfig, SubmitWork } from "./types";
@@ -12,19 +12,25 @@ import { WasmWork, PoWConfig, SubmitWork } from "./types";
* @param {PoWConfig} config - the proof-of-work configuration using which * @param {PoWConfig} config - the proof-of-work configuration using which
* work needs to be computed * work needs to be computed
* */ * */
const prove = async (config: PoWConfig): Promise<SubmitWork> => { const prove = async (
config: PoWConfig,
progress: (nonce: number) => void
): Promise<SubmitWork> => {
const WASM = "wasm"; const WASM = "wasm";
const JS = "js"; const JS = "js";
const STEPS = 5000;
if (WasmSupported) { if (WasmSupported) {
let proof: WasmWork = null; let proof: WasmWork = null;
let res: SubmitWork = null; let res: SubmitWork = null;
let time: number = null; let time: number = null;
const t0 = performance.now(); const t0 = performance.now();
const proofString = gen_pow( const proofString = stepped_gen_pow(
config.salt, config.salt,
config.string, config.string,
config.difficulty_factor config.difficulty_factor,
STEPS,
(nonce: BigInt | number) => progress(Number(nonce))
); );
const t1 = performance.now(); const t1 = performance.now();
time = t1 - t0; time = t1 - t0;
@@ -47,10 +53,12 @@ const prove = async (config: PoWConfig): Promise<SubmitWork> => {
const t0 = performance.now(); const t0 = performance.now();
proof = await p.generate_work( proof = await p.stepped_generate_work(
config.salt, config.salt,
config.string, config.string,
config.difficulty_factor config.difficulty_factor,
STEPS,
progress
); );
const t1 = performance.now(); const t1 = performance.now();
time = t1 - t0; time = t1 - t0;

View File

@@ -6,17 +6,37 @@
import log from "../logger"; import log from "../logger";
import prove from "./prove"; import prove from "./prove";
import { PoWConfig, ServiceWorkerWork } from "./types"; import { PoWConfig, ServiceWorkerMessage, ServiceWorkerWork } from "./types";
log.log("worker registered"); log.log("worker registered");
const ready: ServiceWorkerMessage = {
type: "ready",
};
postMessage(ready);
onmessage = async (e) => { onmessage = async (e) => {
console.debug("message received at worker"); console.debug("message received at worker");
const config: PoWConfig = e.data; const config: PoWConfig = e.data;
const work = await prove(config); const progressCallback = (nonce: number) => {
const res: ServiceWorkerWork = { const res: ServiceWorkerMessage = {
type: "progress",
nonce: nonce,
};
postMessage(res);
};
const work = await prove(config, progressCallback);
const w: ServiceWorkerWork = {
work, work,
}; };
const res: ServiceWorkerMessage = {
type: "work",
value: w,
};
postMessage(res); postMessage(res);
}; };

View File

@@ -5,7 +5,7 @@
import * as CONST from "../const"; import * as CONST from "../const";
import {getBaseHtml, sitekey, checkbox} from "./setupTests"; import { getBaseHtml, sitekey, checkbox } from "./setupTests";
import * as TESTElements from "./setupTests"; import * as TESTElements from "./setupTests";
it("const works", () => { it("const works", () => {
@@ -17,29 +17,17 @@ it("const works", () => {
// display after // display after
CONST.messageText().after(); CONST.messageText().after();
expect(TESTElements.afterMsg.style.display).toBe("block"); expect(TESTElements.Msg.innerText).toBe(CONST.AFTER);
expect(TESTElements.beforeMsg.style.display).toBe("none");
expect(TESTElements.duringMsg.style.display).toBe("none");
expect(TESTElements.errorMsg.style.display).toBe("none");
// display before // display before
CONST.messageText().before(); CONST.messageText().before();
expect(TESTElements.afterMsg.style.display).toBe("none"); expect(TESTElements.Msg.innerText).toBe(CONST.BEFORE);
expect(TESTElements.beforeMsg.style.display).toBe("block");
expect(TESTElements.duringMsg.style.display).toBe("none");
expect(TESTElements.errorMsg.style.display).toBe("none");
// display during // display during
CONST.messageText().during(); CONST.messageText().during();
expect(TESTElements.afterMsg.style.display).toBe("none"); expect(TESTElements.Msg.innerText).toBe(CONST.DURING);
expect(TESTElements.beforeMsg.style.display).toBe("none");
expect(TESTElements.duringMsg.style.display).toBe("block");
expect(TESTElements.errorMsg.style.display).toBe("none");
// display error // display error
CONST.messageText().error(); CONST.messageText().error();
expect(TESTElements.afterMsg.style.display).toBe("none"); expect(TESTElements.Msg.innerText).toBe(CONST.ERROR);
expect(TESTElements.beforeMsg.style.display).toBe("none");
expect(TESTElements.duringMsg.style.display).toBe("none");
expect(TESTElements.errorMsg.style.display).toBe("block");
}); });

View File

@@ -11,25 +11,19 @@ export const checkbox = <HTMLInputElement>document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.id = CONST.btnId; checkbox.id = CONST.btnId;
const getMessages = (state: string) => { const getMessages = () => {
const msg = <HTMLElement>document.createElement("span"); const msg = <HTMLElement>document.createElement("span");
msg.id = `widget__verification-text--${state}`; msg.id = "widget__verification-text";
msg.innerText = "I'm not a robot";
return msg; return msg;
}; };
export const beforeMsg = getMessages("before"); export const Msg = getMessages();
export const afterMsg = getMessages("after");
export const duringMsg = getMessages("during");
export const errorMsg = getMessages("error");
/** get base HTML with empty mCaptcha container */ /** get base HTML with empty mCaptcha container */
export const getBaseHtml = (): HTMLFormElement => { export const getBaseHtml = (): HTMLFormElement => {
const form = <HTMLFormElement>document.createElement("form"); const form = <HTMLFormElement>document.createElement("form");
form.appendChild(checkbox); form.appendChild(checkbox);
form.appendChild(beforeMsg); form.appendChild(Msg);
form.appendChild(duringMsg);
form.appendChild(afterMsg);
form.appendChild(errorMsg);
return form; return form;
}; };

View File

@@ -32,8 +32,14 @@ export type PoWConfig = {
string: string; string: string;
difficulty_factor: number; difficulty_factor: number;
salt: string; salt: string;
max_recorded_nonce: number;
}; };
export type Token = { export type Token = {
token: string; token: string;
}; };
export type ServiceWorkerMessage =
| { type: "ready" }
| { type: "work"; value: ServiceWorkerWork }
| { type: "progress"; nonce: number };

View File

@@ -631,15 +631,15 @@
resolved "https://registry.yarnpkg.com/@mcaptcha/core-glue/-/core-glue-0.1.0-rc1.tgz#76d665a3fc537062061e12e274f969ac3e053685" resolved "https://registry.yarnpkg.com/@mcaptcha/core-glue/-/core-glue-0.1.0-rc1.tgz#76d665a3fc537062061e12e274f969ac3e053685"
integrity sha512-P4SgUioJDR38QpnP9sPY72NyaYex8MXD6RbzrfKra+ngamT26XjqVZEHBiZU2RT7u0SsWhuko4N1ntNOghsgpg== integrity sha512-P4SgUioJDR38QpnP9sPY72NyaYex8MXD6RbzrfKra+ngamT26XjqVZEHBiZU2RT7u0SsWhuko4N1ntNOghsgpg==
"@mcaptcha/pow-wasm@^0.1.0-rc1": "@mcaptcha/pow-wasm@^0.1.0-rc2":
version "0.1.0-rc1" version "0.1.0-rc2"
resolved "https://registry.yarnpkg.com/@mcaptcha/pow-wasm/-/pow-wasm-0.1.0-rc1.tgz#eef8409e0c74e9c7261587bdebd80a8c4af92f9e" resolved "https://registry.yarnpkg.com/@mcaptcha/pow-wasm/-/pow-wasm-0.1.0-rc2.tgz#c7aaa678325600a178b11a702e2aeb9f8143e605"
integrity sha512-7+PGKoe1StFRsa9TEqztzK4/obbdY4OfavFX+geTk8b3K26D+eHPyimJ9BPlpI1VZl8ujR3CnbfbnQSRaqS7ZQ== integrity sha512-2G0nQ2GQWECRcE5kzfULDsQ032s6/PDzE1rncMdQAR1Mu2YQfFZHgnX4zLJmQnjKIhy9meIjXvatVSyIllrbtg==
"@mcaptcha/pow_sha256-polyfill@^0.1.0-rc1": "@mcaptcha/pow_sha256-polyfill@^0.1.0-rc2":
version "0.1.0-rc1" version "0.1.0-rc2"
resolved "https://registry.yarnpkg.com/@mcaptcha/pow_sha256-polyfill/-/pow_sha256-polyfill-0.1.0-rc1.tgz#dfeee88f5f6fd99aeae65dbcff6fbb09fe8a1696" resolved "https://registry.yarnpkg.com/@mcaptcha/pow_sha256-polyfill/-/pow_sha256-polyfill-0.1.0-rc2.tgz#253320e7a6666e395ef9dfb123d1102066d72b87"
integrity sha512-OFA4W3/vh8ORUnifbm8c/8eP22CbiXr4Un6/l4fMyqLj1aoQLMGAiuqab0trGqBnY0DU2bwTMyxflx26/cWgIw== integrity sha512-ERIbxIo+ZnQKtti/T4FLmcY0neuc5R05L97qYc62Hm++i+3dx/W6A8oC4V9U0XKCPYnHZFoZozAZlbsGXjrsVQ==
"@mcaptcha/vanilla-glue@^0.1.0-rc1": "@mcaptcha/vanilla-glue@^0.1.0-rc1":
version "0.1.0-rc1" version "0.1.0-rc1"