Compare commits

...
Sign in to create a new pull request.

42 commits

Author SHA1 Message Date
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
Aravinth Manivannan
49a8757ead
chore: CI: update base node version 2023-10-29 03:32:06 +05:30
Aravinth Manivannan
1107d3fc05
Merge pull request #116 from mCaptcha/update-deps-js
Update deps js
2023-10-28 13:21:12 +00:00
Aravinth Manivannan
072a997ff0
chore: update JS deps 2023-10-28 15:00:27 +05:30
Aravinth Manivannan
d30b53f4ee
chore: update base node version 2023-10-28 15:00:25 +05:30
Aravinth Manivannan
b3021c500e
chore: update redoc openapi spec compiler 2023-10-28 15:00:08 +05:30
Aravinth Manivannan
79006f1e64
chore: update swagger UI 2023-10-28 14:59:44 +05:30
Aravinth Manivannan
a45840d259
Merge pull request #92 from mCaptcha/upload-to-survey
Upload PoW performance to mCaptcha/survey
2023-10-20 02:20:04 +05:30
Aravinth Manivannan
960283324d
feat: schedule mCaptcha/survey registration and uploads 2023-10-20 01:48:59 +05:30
Aravinth Manivannan
74364c4e17
chore: lint 2023-10-20 01:47:24 +05:30
Aravinth Manivannan
3d02f55241
fix: create psuedo id and setup publishing for those tht have opted in 2023-10-20 01:39:19 +05:30
Aravinth Manivannan
eab146b121
gc: get public hostname as config parameter 2023-10-20 01:38:22 +05:30
Aravinth Manivannan
d4534c1c43
feat: define db method to get all psuedo IDs with pagination 2023-10-20 00:18:29 +05:30
Aravinth Manivannan
d5617c7ec7
feat: upload secret route 2023-10-19 09:59:30 +05:30
Aravinth Manivannan
f933a30e7e
feat: load survey keystore 2023-10-19 09:59:29 +05:30
Aravinth Manivannan
87785b38be
feat: bootstrap survey upload job runner 2023-10-19 09:59:29 +05:30
Aravinth Manivannan
52c2c6e598
feat: bootstrap survey uploader's endpoints 2023-10-19 09:59:29 +05:30
Aravinth Manivannan
b6a6705449
feat: read survey uploader's settings 2023-10-19 09:59:29 +05:30
Aravinth Manivannan
c56b04fa5a
feat: download published pow performance analytics 2023-10-19 09:59:29 +05:30
Aravinth Manivannan
ccb9278d67
Merge pull request #115 from mCaptcha/hotfix-env-vars
hotfix: read soon-to-be deprecated env vars to avoid breakages like #114
2023-10-18 17:52:51 +05:30
Aravinth Manivannan
eb69e9aedc
hotfix: read soon-to-be deprecated env vars to avoid breakages like #114 2023-10-18 17:38:42 +05:30
Aravinth Manivannan
1310c22bed
fix: update env var names in docker-compose with the latest names 2023-10-18 13:27:59 +05:30
Aravinth Manivannan
b300d2caac
fix: typo in env var names 2023-10-18 13:23:50 +05:30
Aravinth Manivannan
5d03682c45
fix: CI: disable docker container uploads for branch!=master 2023-10-18 13:22:17 +05:30
Aravinth Manivannan
61729c5fae
fix: set logging var, only if one is not provided 2023-10-18 13:21:33 +05:30
Aravinth Manivannan
8ec5122f87
hotfix: CI: disable tarpaulin run until it is fixed 2023-10-18 12:41:02 +05:30
Aravinth Manivannan
6bd66e6d00
Merge pull request #113 from mCaptcha/update-deps3
chore: use libmcaptcha and libcachebust from crates.io
2023-10-17 16:48:08 +05:30
Aravinth Manivannan
4739c697b7
Merge pull request #107 from jfly/patch-1
Change license
2023-10-17 14:06:18 +05:30
Jeremy Fleischman
ce73d29792
Change license
`AGPL3` isn't a valid SPDX identifier, but `AGPL-3.0-or-later` is. See https://spdx.org/licenses/
2023-09-27 23:31:21 -07:00
80 changed files with 5063 additions and 7473 deletions

View file

@ -1,2 +1,2 @@
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

@ -30,7 +30,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: "18.0.0"
node-version: "20.0.0"
- name: Build frontend
run: make frontend

View file

@ -1,119 +1,119 @@
name: Coverage
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
- db-abstract
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
#- 1.51.0
name: ${{ matrix.version }} - 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: 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: "18.0.0"
- uses: actions-rust-lang/setup-rust-toolchain@v1
- name: Build frontend
run: make frontend
- name: Run the frontend tests
run: make test.frontend
- name: Run migrations
run: make migrate
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
- name: build frontend
run: make frontend
- name: Generate coverage file
if: github.event_name == 'pull_request'
#if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1
with:
args: "-t 1200"
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
# GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# execution so this value is required for preventing meta tests from
# panicking
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61
CACHE_BUSTER_FILE_MAP: '{"map":{"./static/bundle/main.js":"./prod/bundle/main.1417115E59909BE0A01040A45A398ADB09D928DF89CCF038FA44B14850442096.js"},"base_dir":"./prod"}'
COMPILED_DATE: "2021-07-21"
- name: Upload to Codecov
if: github.event_name == 'pull_request'
uses: codecov/codecov-action@v2
#name: Coverage
#
#on:
# pull_request:
# types: [opened, synchronize, reopened]
# push:
# branches:
# - master
# - db-abstract
#
#jobs:
# build_and_test:
# strategy:
# fail-fast: false
# matrix:
# version:
# - stable
# #- 1.51.0
#
# name: ${{ matrix.version }} - 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: 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: "18.0.0"
#
# - uses: actions-rust-lang/setup-rust-toolchain@v1
#
# - name: Build frontend
# run: make frontend
#
# - name: Run the frontend tests
# run: make test.frontend
#
# - name: Run migrations
# run: make migrate
# env:
# POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
# MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
#
# - name: build frontend
# run: make frontend
#
# - name: Generate coverage file
# if: github.event_name == 'pull_request'
# #if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
# uses: actions-rs/tarpaulin@v0.1
# with:
# args: "-t 1200"
# env:
# POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
# MARIA_DATABASE_URL: "${{ env.MARIA_DATABASE_URL }}"
# # GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# # execution so this value is required for preventing meta tests from
# # panicking
# GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61
# CACHE_BUSTER_FILE_MAP: '{"map":{"./static/bundle/main.js":"./prod/bundle/main.1417115E59909BE0A01040A45A398ADB09D928DF89CCF038FA44B14850442096.js"},"base_dir":"./prod"}'
# COMPILED_DATE: "2021-07-21"
#
# - name: Upload to Codecov
# if: github.event_name == 'pull_request'
# uses: codecov/codecov-action@v2

View file

@ -84,7 +84,7 @@ jobs:
- uses: actions/setup-node@v2
with:
node-version: "18.0.0"
node-version: "20.0.0"
- uses: actions-rust-lang/setup-rust-toolchain@v1
@ -119,7 +119,7 @@ jobs:
run: make test.integration
- name: Login to DockerHub
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'
uses: docker/login-action@v1
with:
username: mcaptcha
@ -129,12 +129,12 @@ jobs:
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha'
run: make docker-publish
# - name: publish bins
# if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha'
# run: ./scripts/publish.sh publish master latest $DUMBSERVE_PASSWORD
# env:
# DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
# GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
- name: publish bins
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'mCaptcha/mCaptcha'
run: ./scripts/publish.sh publish master latest $DUMBSERVE_PASSWORD
env:
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
- name: generate documentation
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha')

2
.nvmrc
View file

@ -1 +1 @@
18
20

View file

@ -2,6 +2,9 @@
### Changed
- 2023-10-18: Environment variable names have changed, please see
[CONFIGURATION.md](docs/CONFIGURATION.md) for the names of environment
variables.
- ([`7d0e4c6`](https://github.com/mCaptcha/mCaptcha/commit/7d0e4c6be4b0769921cda7681858ebe16ec9a07b)) Add `secret` parameter to token verification request payload(`/api/v1/pow/siteverify`) to mitigate a security issue that @gusted found:
> ...A malicious user could grab the sitekey
> and use that sitekey with mcaptcha to use it for their own server.

172
Cargo.lock generated
View file

@ -439,6 +439,19 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "async-compression"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f658e2baef915ba0f26f1f7c42bfb8e12f532a01f449a090ded75ae7a07e9ba2"
dependencies = [
"flate2",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.53"
@ -1532,6 +1545,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
@ -1559,6 +1583,43 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1651,6 +1712,12 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "is-terminal"
version = "0.4.9"
@ -1938,6 +2005,7 @@ dependencies = [
"openssl",
"pretty_env_logger 0.4.0",
"rand",
"reqwest",
"rust-embed",
"sailfish",
"serde",
@ -2639,6 +2707,46 @@ version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
[[package]]
name = "reqwest"
version = "0.11.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
dependencies = [
"async-compression",
"base64 0.21.2",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-util",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -3369,6 +3477,27 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.6.0"
@ -3572,6 +3701,12 @@ dependencies = [
"winnow",
]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.37"
@ -3605,6 +3740,12 @@ dependencies = [
"once_cell",
]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "typenum"
version = "1.16.0"
@ -3779,6 +3920,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -3810,6 +3960,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.84"
@ -4027,6 +4189,16 @@ dependencies = [
"memchr",
]
[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]]
name = "yaml-rust"
version = "0.4.5"

View file

@ -78,6 +78,7 @@ lettre = { version = "0.10.0-rc.3", features = [
openssl = { version = "0.10.48", features = ["vendored"] }
uuid = { version = "1.4.0", features = ["v4", "serde"] }
reqwest = { version = "0.11.18", features = ["json", "gzip"] }
[dependencies.db-core]

View file

@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM node:18.0.0 as frontend
FROM node:20 as frontend
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive \

View file

@ -66,3 +66,8 @@ url = "127.0.0.1"
port = 10025
username = "admin"
password = "password"
#[survey]
#nodes = ["http://localhost:7001"]
#rate_limit = 10 # upload every hour
#instance_root_url = "http://localhost:7000"

View file

@ -289,6 +289,35 @@ pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
Err(e) => Err(e),
}
}
/// Get all psuedo IDs
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)]

View file

@ -7,6 +7,29 @@
use crate::errors::*;
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
pub async fn database_works<'a, T: MCDatabase>(
db: &T,
@ -250,7 +273,6 @@ pub async fn database_works<'a, T: MCDatabase>(
db.record_confirm(c.key).await.unwrap();
// analytics start
db.analytics_create_psuedo_id_if_not_exists(c.key)
.await
.unwrap();
@ -258,6 +280,12 @@ pub async fn database_works<'a, T: MCDatabase>(
.analytics_get_psuedo_id_from_capmaign_id(c.key)
.await
.unwrap();
assert_eq!(
vec![psuedo_id.clone()],
db.analytics_get_all_psuedo_ids(0).await.unwrap()
);
assert!(db.analytics_get_all_psuedo_ids(1).await.unwrap().is_empty());
db.analytics_create_psuedo_id_if_not_exists(c.key)
.await
.unwrap();
@ -267,6 +295,7 @@ pub async fn database_works<'a, T: MCDatabase>(
.await
.unwrap()
);
assert_eq!(
c.key,
db.analytics_get_capmaign_id_from_psuedo_id(&psuedo_id)
@ -275,11 +304,31 @@ pub async fn database_works<'a, T: MCDatabase>(
);
let analytics = CreatePerformanceAnalytics {
time: 0,
difficulty_factor: 0,
time: 1,
difficulty_factor: 1,
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();
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 mut offset = 0;
let a = db.analytics_fetch(c.key, limit, offset).await.unwrap();
@ -298,11 +347,82 @@ pub async fn database_works<'a, T: MCDatabase>(
.unwrap();
assert_eq!(db.analytics_fetch(c.key, 1000, 0).await.unwrap().len(), 0);
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)
.await
.unwrap();
// 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_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,25 @@
{
"db_name": "MySQL",
"query": "\n SELECT\n psuedo_id\n FROM\n mcaptcha_psuedo_campaign_id\n ORDER BY ID ASC LIMIT ? OFFSET ?;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "psuedo_id",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | UNIQUE_KEY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 400
}
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "e2c30dafa790b388a193ad8785c0a7d88d8e7a7558775e238fe009f478003e46"
}

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

@ -433,6 +433,39 @@ impl MCDatabase for Database {
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)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
@ -987,12 +1020,8 @@ impl MCDatabase for Database {
&self,
captcha_id: &str,
) -> DBResult<String> {
struct ID {
psuedo_id: String,
}
let res = sqlx::query_as!(
ID,
PsuedoID,
"SELECT psuedo_id FROM
mcaptcha_psuedo_campaign_id
WHERE
@ -1069,6 +1098,182 @@ impl MCDatabase for Database {
Ok(())
}
/// Get all psuedo IDs
async fn analytics_get_all_psuedo_ids(&self, page: usize) -> DBResult<Vec<String>> {
const LIMIT: usize = 50;
let offset = LIMIT * page;
let mut res = sqlx::query_as!(
PsuedoID,
"
SELECT
psuedo_id
FROM
mcaptcha_psuedo_campaign_id
ORDER BY ID ASC LIMIT ? OFFSET ?;",
LIMIT as i64,
offset as i64
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
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)),
}
}
}
#[derive(Clone)]
@ -1134,3 +1339,7 @@ impl From<InternaleCaptchaConfig> for Captcha {
}
}
}
struct PsuedoID {
psuedo_id: String,
}

View file

@ -5,9 +5,11 @@
#![cfg(test)]
use sqlx::mysql::MySqlPoolOptions;
use std::env;
use sqlx::{migrate::MigrateDatabase, mysql::MySqlPoolOptions};
use url::Url;
use crate::*;
use db_core::tests::*;
@ -26,28 +28,6 @@ async fn everyting_works() {
const HEADING: &str = "testing notifications get 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 {
from: NAME,
to: NAME,
@ -56,10 +36,20 @@ async fn everyting_works() {
};
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 connection_options = ConnectionOptions::Fresh(Fresh {
pool_options,
url,
url: url.clone(),
disable_logging: false,
});
let db = connection_options.connect().await.unwrap();
@ -78,4 +68,6 @@ async fn everyting_works() {
description: CAPTCHA_DESCRIPTION,
};
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,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n psuedo_id\n FROM\n mcaptcha_psuedo_campaign_id\n ORDER BY ID ASC LIMIT $1 OFFSET $2;",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "psuedo_id",
"type_info": "Varchar"
}
],
"parameters": {
"Left": [
"Int8",
"Int8"
]
},
"nullable": [
false
]
},
"hash": "d6b89b032e3a65bb5739dde8901a0d6363939bdd87739b4292dd1d88e03ce6f7"
}

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,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

@ -445,6 +445,38 @@ impl MCDatabase for Database {
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)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
@ -994,12 +1026,8 @@ impl MCDatabase for Database {
&self,
captcha_id: &str,
) -> DBResult<String> {
struct ID {
psuedo_id: String,
}
let res = sqlx::query_as!(
ID,
PsuedoID,
"SELECT psuedo_id FROM
mcaptcha_psuedo_campaign_id
WHERE
@ -1078,6 +1106,177 @@ impl MCDatabase for Database {
Ok(())
}
/// Get all psuedo IDs
async fn analytics_get_all_psuedo_ids(&self, page: usize) -> DBResult<Vec<String>> {
const LIMIT: usize = 50;
let offset = LIMIT * page;
let mut res = sqlx::query_as!(
PsuedoID,
"
SELECT
psuedo_id
FROM
mcaptcha_psuedo_campaign_id
ORDER BY ID ASC LIMIT $1 OFFSET $2;",
LIMIT as i64,
offset as i64
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
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)]
@ -1125,6 +1324,10 @@ impl From<InnerNotification> for Notification {
}
}
struct PsuedoID {
psuedo_id: String,
}
#[derive(Clone)]
struct InternaleCaptchaConfig {
config_id: i32,

View file

@ -5,9 +5,12 @@
#![cfg(test)]
use sqlx::postgres::PgPoolOptions;
use std::env;
use sqlx::migrate::MigrateDatabase;
use sqlx::postgres::PgPoolOptions;
use url::Url;
use crate::*;
use db_core::tests::*;
@ -26,28 +29,6 @@ async fn everyting_works() {
const HEADING: &str = "testing notifications get 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 {
from: NAME,
to: NAME,
@ -56,10 +37,20 @@ async fn everyting_works() {
};
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 connection_options = ConnectionOptions::Fresh(Fresh {
pool_options,
url,
url: url.clone(),
disable_logging: false,
});
let db = connection_options.connect().await.unwrap();
@ -78,4 +69,6 @@ async fn everyting_works() {
description: CAPTCHA_DESCRIPTION,
};
database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await;
drop(db);
sqlx::Postgres::drop_database(&url).await.unwrap();
}

View file

@ -11,8 +11,8 @@ services:
- 7000:7000
environment:
DATABASE_URL: postgres://postgres:password@mcaptcha_postgres:5432/postgres # set password at placeholder
MCAPTCHA_REDIS_URL: redis://mcaptcha_redis/
RUST_LOG: debug
MCAPTCHA_redis_URL: "redis://mcaptcha_redis/"
RUST_LOG: "debug"
PORT: 7000
depends_on:
- mcaptcha_postgres

File diff suppressed because it is too large Load diff

View file

@ -11,12 +11,12 @@
"type": "git",
"url": "git+https://github.com/mCaptcha/mCaptcha.git"
},
"license": "AGPL3",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/mCaptcha/mCaptcha/issues"
},
"homepage": "https://github.com/mCaptcha/mCaptcha#readme",
"devDependencies": {
"@redocly/cli": "^1.0.0-beta.129"
"@redocly/cli": "^1.4.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -10,35 +10,35 @@
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"@types/jsdom": "^21.1.1",
"@types/node": "^20.3.3",
"@types/sinon": "^10.0.15",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"@types/jest": "^29.5.6",
"@types/jsdom": "^21.1.4",
"@types/node": "^20.8.9",
"@types/sinon": "^10.0.20",
"@typescript-eslint/eslint-plugin": "^6.9.0",
"@typescript-eslint/parser": "^6.9.0",
"@wasm-tool/wasm-pack-plugin": "^1.7.0",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"eslint": "^8.44.0",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
"eslint": "^8.52.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jsdom": "^22.1.0",
"mini-css-extract-plugin": "^2.7.6",
"sass": "^1.63.6",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"sinon": "^15.2.0",
"sinon": "^17.0.0",
"ts-jest": "^29.1.1",
"ts-loader": "^9.4.4",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.6",
"webpack": "^5.88.1",
"typescript": "^5.2.2",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1"
},
"dependencies": {
"@mcaptcha/pow_sha256-polyfill": "^0.1.0-alpha-1",
"@mcaptcha/pow-wasm": "^0.1.0-alpha-1",
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-3"
"@mcaptcha/pow_sha256-polyfill": "^0.1.0-rc2",
"@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"
NAME=mcaptcha
KEY=0CBABF3084E84E867A76709750BE39D10ECE01FB
KEY=73DAC973A9ADBB9ADCB5CDC4595A08135BA9FF73
TMP_DIR=$(mktemp -d)
FILENAME="$NAME-$2-linux-amd64"

View file

@ -85,10 +85,18 @@ pub mod runner {
data.db
.add_captcha_levels(username, &key, &payload.levels)
.await?;
if payload.publish_benchmarks {
data.db
.analytics_create_psuedo_id_if_not_exists(&key)
.await?;
}
let mcaptcha_config = MCaptchaDetails {
name: payload.description.clone(),
key,
};
Ok(mcaptcha_config)
}
}

View file

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

View file

@ -5,6 +5,7 @@
//use actix::prelude::*;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::pow::PoWConfig;
use libmcaptcha::{
defense::LevelBuilder, master::messages::AddSiteBuilder, DefenseBuilder,
MCaptchaBuilder,
@ -21,7 +22,13 @@ pub struct GetConfigPayload {
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
#[my_codegen::post(path = "V1_API_ROUTES.pow.get_config()")]
@ -35,52 +42,34 @@ pub async fn get_config(
}
let payload = payload.into_inner();
match data.captcha.get_pow(payload.key.clone()).await {
Ok(Some(config)) => {
data.stats.record_fetch(&data, &payload.key).await?;
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
data.stats.record_fetch(&data, &payload.key).await?;
Ok(HttpResponse::Ok().json(config))
}
Err(e) => Err(e.into()),
}
let config: ServiceResult<PoWConfig> =
match data.captcha.get_pow(payload.key.clone()).await {
Ok(Some(config)) => Ok(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");
Ok(config.unwrap())
}
Err(e) => Err(e.into()),
};
let config = config?;
let max_nonce = data
.db
.get_max_nonce_for_level(&payload.key, config.difficulty_factor)
.await?;
data.stats.record_fetch(&data, &payload.key).await?;
// match res.exists {
// Some(true) => {
// match data.captcha.get_pow(payload.key.clone()).await {
// Ok(Some(config)) => {
// record_fetch(&payload.key, &data.db).await;
// 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),
// }
let config = ApiPoWConfig {
string: config.string,
difficulty_factor: config.difficulty_factor,
salt: config.salt,
max_recorded_nonce: max_nonce,
};
Ok(HttpResponse::Ok().json(config))
}
/// 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 worker_type = payload.worker_type.clone();
let time = payload.time;
let nonce = payload.nonce;
let (res, difficulty_factor) = data.captcha.verify_pow(payload.into(), ip).await?;
data.stats.record_solve(&data, &key).await?;
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
.update_max_nonce_for_level(&key, difficulty_factor, nonce as u32)
.await?;
let payload = ValidationToken { token: res };
Ok(HttpResponse::Ok().json(payload))
}

View file

@ -11,6 +11,8 @@ use super::mcaptcha::routes::Captcha;
use super::meta::routes::Meta;
use super::notifications::routes::Notifications;
use super::pow::routes::PoW;
use super::stats::routes::Stats;
use super::survey::routes::Survey;
pub const ROUTES: Routes = Routes::new();
@ -20,7 +22,9 @@ pub struct Routes {
pub captcha: Captcha,
pub meta: Meta,
pub pow: PoW,
pub survey: Survey,
pub notifications: Notifications,
pub stats: Stats,
}
impl Routes {
@ -32,6 +36,8 @@ impl Routes {
meta: Meta::new(),
pow: PoW::new(),
notifications: Notifications::new(),
survey: Survey::new(),
stats: Stats::new(),
}
}
}

252
src/api/v1/stats.rs Normal file
View file

@ -0,0 +1,252 @@
// 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",
}
}
}
}
/// 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> {
let count = data.db.stats_get_num_logs_under_time(payload.time).await?;
if count == 0 {
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}));
}
if count < 2 {
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}));
}
let location = ((count - 1) as f64 * (payload.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(
payload.time,
location.floor() as u32,
)
.await?,
data.db
.stats_get_entry_at_location_for_time_limit_asc(
payload.time,
location.floor() as u32 + 1,
)
.await?,
) {
let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32;
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: Some(res),
}));
}
} else {
if let Some(base) = data
.db
.stats_get_entry_at_location_for_time_limit_asc(
payload.time,
location.floor() as u32,
)
.await?
{
let res = base as u32;
return Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: Some(res),
}));
}
};
Ok(HttpResponse::Ok().json(PercentileResp {
difficulty_factor: None,
}))
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileReq {
time: u32,
percentile: f64,
}
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
/// Health check return datatype
pub struct PercentileResp {
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;
}
}

255
src/api/v1/survey.rs Normal file
View file

@ -0,0 +1,255 @@
/*
* Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::web::ServiceConfig;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::AppData;
pub fn services(cfg: &mut ServiceConfig) {
cfg.service(download);
cfg.service(secret);
}
pub mod routes {
pub struct Survey {
pub download: &'static str,
pub secret: &'static str,
}
impl Survey {
pub const fn new() -> Self {
Self {
download: "/api/v1/survey/takeout/{survey_id}/get",
secret: "/api/v1/survey/secret",
}
}
pub fn get_download_route(&self, survey_id: &str, page: usize) -> String {
format!(
"{}?page={}",
self.download.replace("{survey_id}", survey_id),
page
)
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct Page {
pub page: usize,
}
/// emits build details of the bninary
#[my_codegen::get(path = "crate::V1_API_ROUTES.survey.download")]
async fn download(
data: AppData,
page: web::Query<Page>,
psuedo_id: web::Path<uuid::Uuid>,
) -> ServiceResult<impl Responder> {
const LIMIT: usize = 50;
let offset = LIMIT as isize * ((page.page as isize) - 1);
let offset = if offset < 0 { 0 } else { offset };
let psuedo_id = psuedo_id.into_inner();
let campaign_id = data
.db
.analytics_get_capmaign_id_from_psuedo_id(&psuedo_id.to_string())
.await?;
let data = data
.db
.analytics_fetch(&campaign_id, LIMIT, offset as usize)
.await?;
Ok(HttpResponse::Ok().json(data))
}
#[derive(Serialize, Deserialize)]
struct SurveySecretUpload {
secret: String,
auth_token: String,
}
/// mCaptcha/survey upload secret route
#[my_codegen::post(path = "crate::V1_API_ROUTES.survey.secret")]
async fn secret(
data: AppData,
payload: web::Json<SurveySecretUpload>,
) -> ServiceResult<impl Responder> {
match data.survey_secrets.get(&payload.auth_token) {
Some(survey_instance_url) => {
let payload = payload.into_inner();
data.survey_secrets.set(survey_instance_url, payload.secret);
data.survey_secrets.rm(&payload.auth_token);
Ok(HttpResponse::Ok())
}
None => Err(ServiceError::WrongPassword),
}
}
#[cfg(test)]
pub mod tests {
use actix_web::{http::StatusCode, test, App};
use super::*;
use crate::api::v1::mcaptcha::get_random;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn survey_works_pg() {
let data = crate::tests::pg::get_data().await;
survey_registration_works(data.clone()).await;
survey_works(data).await;
}
#[actix_rt::test]
async fn survey_works_maria() {
let data = crate::tests::maria::get_data().await;
survey_registration_works(data.clone()).await;
survey_works(data).await;
}
pub async fn survey_registration_works(data: ArcData) {
let data = &data;
let app = get_app!(data).await;
let survey_instance_url = "http://survey_registration_works.survey.example.org";
let key = get_random(20);
let msg = SurveySecretUpload {
auth_token: key.clone(),
secret: get_random(32),
};
// should fail with ServiceError::WrongPassword since auth token is not loaded into
// keystore
bad_post_req_test_no_auth(
data,
V1_API_ROUTES.survey.secret,
&msg,
errors::ServiceError::WrongPassword,
)
.await;
// load auth token into key store, should succeed
data.survey_secrets
.set(key.clone(), survey_instance_url.to_owned());
let resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.survey.secret).to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// uploaded secret must be in keystore
assert_eq!(
data.survey_secrets.get(survey_instance_url).unwrap(),
msg.secret
);
// should fail since mCaptcha/survey secret upload auth tokens are single-use
bad_post_req_test_no_auth(
data,
V1_API_ROUTES.survey.secret,
&msg,
errors::ServiceError::WrongPassword,
)
.await;
}
pub async fn survey_works(data: ArcData) {
const NAME: &str = "survetuseranalytics";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "survetuseranalytics@a.com";
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 0..60 {
println!("[{i}] Saving analytics");
let analytics = db_core::CreatePerformanceAnalytics {
time: 0,
difficulty_factor: 0,
worker_type: "wasm".into(),
};
data.db.analysis_save(&key.key, &analytics).await.unwrap();
}
for p in 1..3 {
let download_rotue = V1_API_ROUTES.survey.get_download_route(&psuedo_id, p);
println!("page={p}, download={download_rotue}");
let download_req = test::call_service(
&app,
test::TestRequest::get().uri(&download_rotue).to_request(),
)
.await;
assert_eq!(download_req.status(), StatusCode::OK);
let analytics: Vec<db_core::PerformanceAnalytics> =
test::read_body_json(download_req).await;
if p == 1 {
assert_eq!(analytics.len(), 50);
} else if p == 2 {
assert_eq!(analytics.len(), 10);
} else {
assert_eq!(analytics.len(), 0);
}
}
let download_rotue = V1_API_ROUTES.survey.get_download_route(&psuedo_id, 0);
data.db
.analytics_delete_all_records_for_campaign(&key.key)
.await
.unwrap();
let download_req = test::call_service(
&app,
test::TestRequest::get().uri(&download_rotue).to_request(),
)
.await;
assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
}
}

View file

@ -4,8 +4,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
//! App data: redis cache, database connections, etc.
use std::collections::HashMap;
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use actix::prelude::*;
use argon2_creds::{Config, ConfigBuilder, PasswordPolicy};
@ -28,11 +30,17 @@ use libmcaptcha::{
pow::Work,
system::{System, SystemBuilder},
};
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::task::JoinHandle;
use tokio::time::sleep;
use crate::db::{self, BoxDB};
use crate::errors::ServiceResult;
use crate::settings::Settings;
use crate::stats::{Dummy, Real, Stats};
use crate::survey::SecretsStore;
use crate::AppData;
macro_rules! enum_system_actor {
($name:ident, $type:ident) => {
@ -166,6 +174,8 @@ pub struct Data {
pub settings: Settings,
/// stats recorder
pub stats: Box<dyn Stats>,
/// survey secret store
pub survey_secrets: SecretsStore,
}
impl Data {
@ -180,7 +190,7 @@ impl Data {
}
#[cfg(not(tarpaulin_include))]
/// create new instance of app data
pub async fn new(s: &Settings) -> Arc<Self> {
pub async fn new(s: &Settings, survey_secrets: SecretsStore) -> Arc<Self> {
let creds = Self::get_creds();
let c = creds.clone();
@ -209,6 +219,7 @@ impl Data {
mailer: Self::get_mailer(s),
settings: s.clone(),
stats,
survey_secrets,
};
#[cfg(not(debug_assertions))]
@ -242,6 +253,13 @@ impl Data {
None
}
}
async fn upload_survey_job(&self) -> ServiceResult<()> {
unimplemented!()
}
async fn register_survey(&self) -> ServiceResult<()> {
unimplemented!()
}
}
/// Mailer data type AsyncSmtpTransport<Tokio1Executor>

View file

@ -30,6 +30,7 @@ mod routes;
mod settings;
mod static_assets;
mod stats;
mod survey;
#[cfg(test)]
#[macro_use]
mod tests;
@ -45,6 +46,7 @@ use static_assets::FileMap;
pub use widget::WIDGET_ROUTES;
use crate::demo::DemoUser;
use survey::SurveyClientTrait;
lazy_static! {
pub static ref SETTINGS: Settings = Settings::new().unwrap();
@ -93,7 +95,9 @@ pub type AppData = actix_web::web::Data<ArcData>;
async fn main() -> std::io::Result<()> {
use std::time::Duration;
env::set_var("RUST_LOG", "info");
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
pretty_env_logger::init();
info!(
@ -102,7 +106,8 @@ async fn main() -> std::io::Result<()> {
);
let settings = Settings::new().unwrap();
let data = Data::new(&settings).await;
let secrets = survey::SecretsStore::default();
let data = Data::new(&settings, secrets.clone()).await;
let data = actix_web::web::Data::new(data);
let mut demo_user: Option<DemoUser> = None;
@ -115,6 +120,13 @@ async fn main() -> std::io::Result<()> {
);
}
let (mut survey_upload_tx, mut survey_upload_handle) = (None, None);
if settings.survey.is_some() {
let survey_runner_ctx = survey::Survey::new(data.clone());
let (x, y) = survey_runner_ctx.start_job().await.unwrap();
(survey_upload_tx, survey_upload_handle) = (Some(x), Some(y));
}
let ip = settings.server.get_ip();
println!("Starting server on: http://{ip}");
@ -139,9 +151,18 @@ async fn main() -> std::io::Result<()> {
.run()
.await?;
if let Some(survey_upload_tx) = survey_upload_tx {
survey_upload_tx.send(()).unwrap();
}
if let Some(demo_user) = demo_user {
demo_user.abort();
}
if let Some(survey_upload_handle) = survey_upload_handle {
survey_upload_handle.await.unwrap();
}
Ok(())
}

View file

@ -91,6 +91,13 @@ pub struct Redis {
pub pool: u32,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct Survey {
pub nodes: Vec<url::Url>,
pub rate_limit: u64,
pub instance_root_url: Url,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
pub struct Settings {
pub debug: bool,
@ -99,6 +106,7 @@ pub struct Settings {
pub allow_registration: bool,
pub allow_demo: bool,
pub database: Database,
pub survey: Option<Survey>,
pub redis: Option<Redis>,
pub server: Server,
pub captcha: Captcha,
@ -118,8 +126,8 @@ const ENV_VAR_CONFIG: [(&str, &str); 29] = [
("database.pool", "MCAPTCHA_database_POOL"),
/* redis */
("redis.url", "MCPATCHA_redis_URL"),
("redis.pool", "MCPATCHA_redis_POOL"),
("redis.url", "MCAPTCHA_redis_URL"),
("redis.pool", "MCAPTCHA_redis_POOL"),
/* server */
("server.port", "PORT"),
@ -145,17 +153,52 @@ const ENV_VAR_CONFIG: [(&str, &str); 29] = [
/* SMTP */
("smtp.from", "MCPATCHA_smtp_FROM"),
("smtp.reply", "MCPATCHA_smtp_REPLY"),
("smtp.url", "MCPATCHA_smtp_URL"),
("smtp.username", "MCPATCHA_smtp_USERNAME"),
("smtp.password", "MCPATCHA_smtp_PASSWORD"),
("smtp.port", "MCPATCHA_smtp_PORT"),
("smtp.from", "MCAPTCHA_smtp_FROM"),
("smtp.reply", "MCAPTCHA_smtp_REPLY"),
("smtp.url", "MCAPTCHA_smtp_URL"),
("smtp.username", "MCAPTCHA_smtp_USERNAME"),
("smtp.password", "MCAPTCHA_smtp_PASSWORD"),
("smtp.port", "MCAPTCHA_smtp_PORT"),
];
const DEPRECATED_ENV_VARS: [(&str, &str); 23] = [
("debug", "MCAPTCHA_DEBUG"),
("commercial", "MCAPTCHA_COMMERCIAL"),
("source_code", "MCAPTCHA_SOURCE_CODE"),
("allow_registration", "MCAPTCHA_ALLOW_REGISTRATION"),
("allow_demo", "MCAPTCHA_ALLOW_DEMO"),
("redis.pool", "MCAPTCHA_REDIS_POOL"),
("redis.url", "MCAPTCHA_REDIS_URL"),
("server.port", "MCAPTCHA_SERVER_PORT"),
("server.ip", "MCAPTCHA_SERVER_IP"),
("server.domain", "MCAPTCHA_SERVER_DOMAIN"),
("server.cookie_secret", "MCAPTCHA_SERVER_COOKIE_SECRET"),
("server.proxy_has_tls", "MCAPTCHA_SERVER_PROXY_HAS_TLS"),
("captcha.salt", "MCAPTCHA_CAPTCHA_SALT"),
("captcha.gc", "MCAPTCHA_CAPTCHA_GC"),
(
"captcha.default_difficulty_strategy.avg_traffic_difficulty",
"MCAPTCHA_CAPTCHA_AVG_TRAFFIC_DIFFICULTY",
),
(
"captcha.default_difficulty_strategy.peak_sustainable_traffic_difficulty",
"MCAPTCHA_CAPTCHA_PEAK_TRAFFIC_DIFFICULTY",
),
(
"captcha.default_difficulty_strategy.broke_my_site_traffic_difficulty",
"MCAPTCHA_CAPTCHA_BROKE_MY_SITE_TRAFFIC",
),
("smtp.from", "MCAPTCHA_SMTP_FROM"),
("smtp.reply", "MCAPTCHA_SMTP_REPLY_TO"),
("smtp.url", "MCAPTCHA_SMTP_URL"),
("smtp.username", "MCAPTCHA_SMTP_USERNAME"),
("smtp.password", "MCAPTCHA_SMTP_PASSWORD"),
("smtp.port", "MCAPTCHA_SMTP_PORT"),
];
#[cfg(not(tarpaulin_include))]
impl Settings {
pub fn new() -> Result<Self, ConfigError> {
@ -210,6 +253,15 @@ impl Settings {
}
fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
for (parameter, env_var_name) in DEPRECATED_ENV_VARS.iter() {
if let Ok(val) = env::var(env_var_name) {
log::warn!(
"Found {env_var_name}. {env_var_name} will be deprecated soon. Please see https://github.com/mCaptcha/mCaptcha/blob/master/docs/CONFIGURATION.md for latest environment variable names"
);
s = s.set_override(parameter, val).unwrap();
}
}
for (parameter, env_var_name) in ENV_VAR_CONFIG.iter() {
if let Ok(val) = env::var(env_var_name) {
log::debug!(
@ -240,7 +292,7 @@ mod tests {
use super::*;
#[test]
fn env_override_works() {
fn deprecated_env_override_works() {
use crate::tests::get_settings;
let init_settings = get_settings();
// so that it can be tested outside the macro (helper) too
@ -249,6 +301,141 @@ mod tests {
macro_rules! helper {
($env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => {
println!("Setting env var {} to {} for test", $env, $val);
env::set_var($env, $val);
new_settings = get_settings();
assert_eq!(new_settings.$($param).+, $val_typed);
assert_ne!(new_settings.$($param).+, init_settings.$($param).+);
env::remove_var($env);
};
($env:expr, $val:expr, $($param:ident).+) => {
helper!($env, $val.to_string(), $val, $($param).+);
};
}
/* top level */
helper!("MCAPTCHA_DEBUG", !init_settings.debug, debug);
helper!("MCAPTCHA_COMMERCIAL", !init_settings.commercial, commercial);
helper!(
"MCAPTCHA_ALLOW_REGISTRATION",
!init_settings.allow_registration,
allow_registration
);
helper!("MCAPTCHA_ALLOW_DEMO", !init_settings.allow_demo, allow_demo);
/* database_type */
/* redis.url */
let env = "MCAPTCHA_REDIS_URL";
let val = "redis://redis.example.org";
println!("Setting env var {} to {} for test", env, val);
env::set_var(env, val);
new_settings = get_settings();
assert_eq!(new_settings.redis.as_ref().unwrap().url, val);
assert_ne!(
new_settings.redis.as_ref().unwrap().url,
init_settings.redis.as_ref().unwrap().url
);
env::remove_var(env);
/* redis.pool */
let env = "MCAPTCHA_REDIS_POOL";
let val = 999;
println!("Setting env var {} to {} for test", env, val);
env::set_var(env, val.to_string());
new_settings = get_settings();
assert_eq!(new_settings.redis.as_ref().unwrap().pool, val);
assert_ne!(
new_settings.redis.as_ref().unwrap().pool,
init_settings.redis.as_ref().unwrap().pool
);
env::remove_var(env);
helper!("PORT", 0, server.port);
helper!("MCAPTCHA_SERVER_DOMAIN", "example.org", server.domain);
helper!(
"MCAPTCHA_SERVER_COOKIE_SECRET",
"dafasdfsdf",
server.cookie_secret
);
helper!("MCAPTCHA_SERVER_IP", "9.9.9.9", server.ip);
helper!("MCAPTCHA_SERVER_PROXY_HAS_TLS", true, server.proxy_has_tls);
/* captcha */
helper!("MCAPTCHA_CAPTCHA_SALT", "foobarasdfasdf", captcha.salt);
helper!("MCAPTCHA_CAPTCHA_GC", 500, captcha.gc);
helper!(
"MCAPTCHA_captcha_RUNNERS",
"500",
Some(500),
captcha.runners
);
helper!(
"MCAPTCHA_CAPTCHA_AVG_TRAFFIC_DIFFICULTY",
999,
captcha.default_difficulty_strategy.avg_traffic_difficulty
);
helper!(
"MCAPTCHA_CAPTCHA_PEAK_TRAFFIC_DIFFICULTY",
999,
captcha
.default_difficulty_strategy
.peak_sustainable_traffic_difficulty
);
helper!(
"MCAPTCHA_CAPTCHA_BROKE_MY_SITE_TRAFFIC",
999,
captcha
.default_difficulty_strategy
.broke_my_site_traffic_difficulty
);
/* SMTP */
let vals = [
"MCAPTCHA_SMTP_FROM",
"MCAPTCHA_SMTP_REPLY_TO",
"MCAPTCHA_SMTP_URL",
"MCAPTCHA_SMTP_USERNAME",
"MCAPTCHA_SMTP_PASSWORD",
"MCAPTCHA_SMTP_PORT",
];
for env in vals.iter() {
println!("Setting env var {} to {} for test", env, env);
env::set_var(env, env);
}
let port = 9999;
env::set_var("MCAPTCHA_SMTP_PORT", port.to_string());
new_settings = get_settings();
let smtp_new = new_settings.smtp.as_ref().unwrap();
let smtp_old = init_settings.smtp.as_ref().unwrap();
assert_eq!(smtp_new.from, "MCAPTCHA_SMTP_FROM");
assert_eq!(smtp_new.reply, "MCAPTCHA_SMTP_REPLY_TO");
assert_eq!(smtp_new.username, "MCAPTCHA_SMTP_USERNAME");
assert_eq!(smtp_new.password, "MCAPTCHA_SMTP_PASSWORD");
assert_eq!(smtp_new.port, port);
assert_ne!(smtp_new, smtp_old);
for env in vals.iter() {
env::remove_var(env);
}
}
#[test]
fn env_override_works() {
use crate::tests::get_settings;
let init_settings = get_settings();
// so that it can be tested outside the macro (helper) too
let mut new_settings;
macro_rules! helper {
($env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => {
@ -291,7 +478,7 @@ mod tests {
/* redis */
/* redis.url */
let env = "MCPATCHA_redis_URL";
let env = "MCAPTCHA_redis_URL";
let val = "redis://redis.example.org";
println!("Setting env var {} to {} for test", env, val);
env::set_var(env, val);
@ -304,7 +491,7 @@ mod tests {
env::remove_var(env);
/* redis.pool */
let env = "MCPATCHA_redis_POOL";
let env = "MCAPTCHA_redis_POOL";
let val = 999;
println!("Setting env var {} to {} for test", env, val);
env::set_var(env, val.to_string());
@ -355,12 +542,12 @@ mod tests {
/* SMTP */
let vals = [
"MCPATCHA_smtp_FROM",
"MCPATCHA_smtp_REPLY",
"MCPATCHA_smtp_URL",
"MCPATCHA_smtp_USERNAME",
"MCPATCHA_smtp_PASSWORD",
"MCPATCHA_smtp_PORT",
"MCAPTCHA_smtp_FROM",
"MCAPTCHA_smtp_REPLY",
"MCAPTCHA_smtp_URL",
"MCAPTCHA_smtp_USERNAME",
"MCAPTCHA_smtp_PASSWORD",
"MCAPTCHA_smtp_PORT",
];
for env in vals.iter() {
println!("Setting env var {} to {} for test", env, env);
@ -368,15 +555,15 @@ mod tests {
}
let port = 9999;
env::set_var("MCPATCHA_smtp_PORT", port.to_string());
env::set_var("MCAPTCHA_smtp_PORT", port.to_string());
new_settings = get_settings();
let smtp_new = new_settings.smtp.as_ref().unwrap();
let smtp_old = init_settings.smtp.as_ref().unwrap();
assert_eq!(smtp_new.from, "MCPATCHA_smtp_FROM");
assert_eq!(smtp_new.reply, "MCPATCHA_smtp_REPLY");
assert_eq!(smtp_new.username, "MCPATCHA_smtp_USERNAME");
assert_eq!(smtp_new.password, "MCPATCHA_smtp_PASSWORD");
assert_eq!(smtp_new.from, "MCAPTCHA_smtp_FROM");
assert_eq!(smtp_new.reply, "MCAPTCHA_smtp_REPLY");
assert_eq!(smtp_new.username, "MCAPTCHA_smtp_USERNAME");
assert_eq!(smtp_new.password, "MCAPTCHA_smtp_PASSWORD");
assert_eq!(smtp_new.port, port);
assert_ne!(smtp_new, smtp_old);

209
src/survey.rs Normal file
View file

@ -0,0 +1,209 @@
// Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::RwLock;
use std::time::Duration;
use reqwest::Client;
use serde::{Deserialize, Serialize};
use tokio::sync::oneshot;
use tokio::task::JoinHandle;
use tokio::time::sleep;
use crate::errors::*;
use crate::settings::Settings;
use crate::AppData;
use crate::V1_API_ROUTES;
#[async_trait::async_trait]
pub trait SurveyClientTrait {
async fn start_job(&self) -> ServiceResult<(oneshot::Sender<()>, JoinHandle<()>)>;
async fn schedule_upload_job(&self) -> ServiceResult<()>;
async fn is_online(&self) -> ServiceResult<bool>;
async fn register(&self) -> ServiceResult<()>;
}
#[derive(Clone, Debug, Default)]
pub struct SecretsStore {
store: Arc<RwLock<HashMap<String, String>>>,
}
impl SecretsStore {
pub fn get(&self, key: &str) -> Option<String> {
let r = self.store.read().unwrap();
r.get(key).map(|x| x.to_owned())
}
pub fn rm(&self, key: &str) {
let mut w = self.store.write().unwrap();
w.remove(key);
drop(w);
}
pub fn set(&self, key: String, value: String) {
let mut w = self.store.write().unwrap();
w.insert(key, value);
drop(w);
}
}
#[derive(Clone)]
pub struct Survey {
client: Client,
app_ctx: AppData,
}
impl Survey {
pub fn new(app_ctx: AppData) -> Self {
if app_ctx.settings.survey.is_none() {
panic!("Survey uploader shouldn't be initialized it isn't configured, please report this bug")
}
Survey {
client: Client::new(),
app_ctx,
}
}
}
#[async_trait::async_trait]
impl SurveyClientTrait for Survey {
async fn start_job(&self) -> ServiceResult<(oneshot::Sender<()>, JoinHandle<()>)> {
fn can_run(rx: &mut oneshot::Receiver<()>) -> bool {
match rx.try_recv() {
Err(oneshot::error::TryRecvError::Empty) => true,
_ => false,
}
}
let (tx, mut rx) = oneshot::channel();
let this = self.clone();
let mut register = false;
let fut = async move {
loop {
if !can_run(&mut rx) {
log::info!("Stopping survey uploads");
break;
}
if !register {
loop {
if this.is_online().await.unwrap() {
this.register().await.unwrap();
register = true;
break;
} else {
sleep(Duration::new(1, 0)).await;
}
}
}
for i in 0..this.app_ctx.settings.survey.as_ref().unwrap().rate_limit {
if !can_run(&mut rx) {
log::info!("Stopping survey uploads");
break;
}
sleep(Duration::new(1, 0)).await;
}
let _ = this.schedule_upload_job().await;
// for url in this.app_ctx.settings.survey.as_ref().unwrap().nodes.iter() {
// if !can_run(&mut rx) {
// log::info!("Stopping survey uploads");
// break;
// }
// log::info!("Uploading to survey instance {}", url);
// }
}
};
let handle = tokio::spawn(fut);
Ok((tx, handle))
}
async fn is_online(&self) -> ServiceResult<bool> {
let res = self
.client
.get(format!(
"http://{}{}",
self.app_ctx.settings.server.get_ip(),
V1_API_ROUTES.meta.health
))
.send()
.await
.unwrap();
Ok(res.status() == 200)
}
async fn schedule_upload_job(&self) -> ServiceResult<()> {
log::debug!("Running upload job");
#[derive(Serialize)]
struct Secret {
secret: String,
}
let mut page = 0;
loop {
let psuedo_ids = self.app_ctx.db.analytics_get_all_psuedo_ids(page).await?;
if psuedo_ids.is_empty() {
log::debug!("upload job complete, no more IDs to upload");
break;
}
for id in psuedo_ids {
for url in self.app_ctx.settings.survey.as_ref().unwrap().nodes.iter() {
if let Some(secret) = self.app_ctx.survey_secrets.get(url.as_str()) {
let payload = Secret { secret };
log::info!("Uploading to survey instance {} campaign {id}", url);
let mut url = url.clone();
url.set_path(&format!("/mcaptcha/api/v1/{id}/upload"));
let resp =
self.client.post(url).json(&payload).send().await.unwrap();
println!("{}", resp.text().await.unwrap());
}
}
}
page += 1;
}
Ok(())
}
async fn register(&self) -> ServiceResult<()> {
#[derive(Serialize)]
struct MCaptchaInstance {
url: url::Url,
auth_token: String,
}
let this_instance_url = self
.app_ctx
.settings
.survey
.as_ref()
.unwrap()
.instance_root_url
.clone();
for url in self.app_ctx.settings.survey.as_ref().unwrap().nodes.iter() {
// mCaptcha/survey must send this token while uploading secret to authenticate itself
// this token must be sent to mCaptcha/survey with the registration payload
let secret_upload_auth_token = crate::api::v1::mcaptcha::get_random(20);
let payload = MCaptchaInstance {
url: this_instance_url.clone(),
auth_token: secret_upload_auth_token.clone(),
};
// SecretsStore will store auth tokens generated by both mCaptcha/mCaptcha and
// mCaptcha/survey
//
// Storage schema:
// - mCaptcha/mCaptcha generated auth token: (<auth_token>, <survey_instance_url>)
// - mCaptcha/survey generated auth token (<survey_instance_url>, <auth_token)
self.app_ctx
.survey_secrets
.set(secret_upload_auth_token, url.to_string());
let mut url = url.clone();
url.set_path("/mcaptcha/api/v1/register");
let resp = self.client.post(url).json(&payload).send().await.unwrap();
}
Ok(())
}
}

View file

@ -20,6 +20,7 @@ use crate::api::v1::mcaptcha::create::CreateCaptcha;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::api::v1::ROUTES;
use crate::errors::*;
use crate::survey::SecretsStore;
use crate::ArcData;
pub fn get_settings() -> Settings {
@ -28,41 +29,69 @@ pub fn get_settings() -> Settings {
pub mod pg {
use std::env;
use sqlx::migrate::MigrateDatabase;
use crate::api::v1::mcaptcha::get_random;
use crate::data::Data;
use crate::settings::*;
use crate::survey::SecretsStore;
use crate::ArcData;
use super::get_settings;
pub async fn get_data() -> ArcData {
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();
settings.captcha.runners = Some(1);
settings.database.url = url.clone();
settings.database.database_type = DBType::Postgres;
settings.database.pool = 2;
Data::new(&settings).await
Data::new(&settings, SecretsStore::default()).await
}
}
pub mod maria {
use std::env;
use sqlx::migrate::MigrateDatabase;
use crate::api::v1::mcaptcha::get_random;
use crate::data::Data;
use crate::settings::*;
use crate::survey::SecretsStore;
use crate::ArcData;
use super::get_settings;
pub async fn get_data() -> ArcData {
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();
settings.captcha.runners = Some(1);
settings.database.url = url.clone();
settings.database.database_type = DBType::Maria;
settings.database.pool = 2;
Data::new(&settings).await
Data::new(&settings, SecretsStore::default()).await
}
}
//pub async fn get_data() -> ArcData {
@ -181,6 +210,26 @@ pub async fn signin(
(creds, signin_resp)
}
/// pub duplicate test
pub async fn bad_post_req_test_no_auth<T: Serialize>(
data: &ArcData,
url: &str,
payload: &T,
err: ServiceError,
) {
let app = get_app!(data).await;
let resp = test::call_service(&app, post_request!(&payload, url).to_request()).await;
if resp.status() != err.status_code() {
let resp_err: ErrorToResponse = test::read_body_json(resp).await;
panic!("error {}", resp_err.error);
}
assert_eq!(resp.status(), err.status_code());
let resp_err: ErrorToResponse = test::read_body_json(resp).await;
//println!("{}", txt.error);
assert_eq!(resp_err.error, format!("{}", err));
}
/// pub duplicate test
pub async fn bad_post_req_test<T: Serialize>(
data: &ArcData,

29
static/openapi/.gitignore vendored Normal file
View file

@ -0,0 +1,29 @@
node_modules
.idea
.vscode
.deps_check
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
.nyc_output
npm-debug.log*
.eslintcache
*.iml
selenium-debug.log
chromedriver.log
test/e2e/db.json
docs/_book
dev-helpers/examples
# dist
flavors/**/dist/*
/lib
/es
dist/log*
/swagger-ui-*.tgz
# Cypress
test/e2e-cypress/screenshots
test/e2e-cypress/videos

View file

@ -1,5 +1,5 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@ -25,22 +25,22 @@
<script src="./swagger-ui-bundle.js" charset="UTF-8"></script>
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"></script>
<script src="./swagger-initializer.js" charset="UTF-8"></script>
<script>
window.onload = function () {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "./openapi.yaml",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout",
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</body>
<script>
window.onload = function () {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/docs/openapi.yaml",
dom_id: "#swagger-ui",
deepLinking: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
plugins: [SwaggerUIBundle.plugins.DownloadUrl],
layout: "StandaloneLayout",
});
// End Swagger UI call region
window.ui = ui;
};
</script>
</html>

View file

@ -1,99 +1,283 @@
ramda: 361.65 KB (6.14%)
core-js-pure: 300.89 KB (5.11%)
ramda-adjunct: 255.42 KB (4.34%)
lodash: 253.56 KB (4.30%)
@swagger-api/apidom-ns-openapi-3-0: 204.6 KB (3.47%)
autolinker: 203.32 KB (3.45%)
swagger-client: 159.41 KB (2.71%)
is-plain-object: 758 B (0.464%)
<self>: 158.67 KB (99.5%)
@swagger-api/apidom-ns-openapi-3-1: 154.72 KB (2.63%)
immutable: 139.01 KB (2.36%)
remarkable: 125.56 KB (2.13%)
react-dom: 119.19 KB (2.02%)
highlight.js: 111.85 KB (1.90%)
js-yaml: 105.01 KB (1.78%)
readable-stream: 96.66 KB (1.64%)
@swagger-api/apidom-reference: 84.72 KB (1.44%)
dompurify: 61.27 KB (1.04%)
@swagger-api/apidom-ns-json-schema-draft-4: 58.11 KB (0.987%)
minim: 57.35 KB (0.974%)
buffer: 56.99 KB (0.967%)
@swagger-api/apidom-core: 47.72 KB (0.810%)
@swagger-api/apidom-ast: 44.47 KB (0.755%)
react-redux: 43.7 KB (0.742%)
react-syntax-highlighter: 38.01 KB (0.645%)
url: 37.24 KB (0.632%)
punycode: 14.33 KB (38.5%)
<self>: 22.92 KB (61.5%)
fast-json-patch: 31.89 KB (0.541%)
redux: 27.59 KB (0.468%)
qs: 26.61 KB (0.452%)
sha.js: 18.57 KB (0.315%)
object-inspect: 18.15 KB (0.308%)
url-parse: 16.23 KB (0.276%)
events: 14.54 KB (0.247%)
cross-fetch: 14.31 KB (0.243%)
tslib: 14.21 KB (0.241%)
@babel/runtime-corejs3: 13.24 KB (0.225%)
get-intrinsic: 13.01 KB (0.221%)
reselect: 12.51 KB (0.212%)
zenscroll: 12.31 KB (0.209%)
react-debounce-input: 11.95 KB (0.203%)
react-immutable-proptypes: 11.82 KB (0.201%)
ret: 10.82 KB (0.184%)
lodash.debounce: 10.53 KB (0.179%)
unraw: 10.27 KB (0.174%)
string_decoder: 9.24 KB (0.157%)
short-unique-id: 8.29 KB (0.141%)
xml: 7.39 KB (0.125%)
react-copy-to-clipboard: 7.33 KB (0.124%)
traverse: 7.15 KB (0.121%)
react: 6.48 KB (0.110%)
@babel/runtime: 6.29 KB (0.107%)
randexp: 6.15 KB (0.104%)
react-immutable-pure-component: 6.01 KB (0.102%)
redux-immutable: 5.43 KB (0.0922%)
process: 5.29 KB (0.0898%)
cookie: 5.1 KB (0.0866%)
scheduler: 4.91 KB (0.0834%)
drange: 4.8 KB (0.0815%)
lowlight: 4.42 KB (0.0751%)
deep-extend: 4.19 KB (0.0712%)
deepmerge: 3.95 KB (0.0671%)
base64-js: 3.84 KB (0.0652%)
stream-browserify: 3.76 KB (0.0638%)
@swagger-api/apidom-json-pointer: 3.68 KB (0.0625%)
side-channel: 3.31 KB (0.0562%)
copy-to-clipboard: 3.29 KB (0.0558%)
format: 3.26 KB (0.0553%)
stampit: 3.16 KB (0.0536%)
css.escape: 3.08 KB (0.0523%)
serialize-error: 2.93 KB (0.0497%)
hoist-non-react-statics: 2.68 KB (0.0455%)
prop-types: 2.6 KB (0.0441%)
use-sync-external-store: 2.59 KB (0.0440%)
querystringify: 2.5 KB (0.0425%)
react-is: 2.48 KB (0.0422%)
xml-but-prettier: 2.17 KB (0.0368%)
has-symbols: 2.13 KB (0.0362%)
ieee754: 2.1 KB (0.0357%)
object-assign: 2.06 KB (0.0349%)
call-bind: 1.68 KB (0.0285%)
safe-buffer: 1.63 KB (0.0277%)
util-deprecate: 1.58 KB (0.0268%)
function-bind: 1.55 KB (0.0263%)
randombytes: 1.54 KB (0.0261%)
js-file-download: 1.52 KB (0.0258%)
classnames: 1.38 KB (0.0235%)
@braintree/sanitize-url: 1.38 KB (0.0234%)
repeat-string: 1.18 KB (0.0201%)
toggle-selection: 780 B (0.0129%)
inherits: 753 B (0.0125%)
requires-port: 753 B (0.0125%)
fault: 691 B (0.0115%)
formdata-node: 259 B (0.00429%)
has-proto: 197 B (0.00327%)
has: 129 B (0.00214%)
object-inspect|.: 15 B (0.000249%)
<self>: 2.26 MB (39.2%)
.pnpm/ramda@0.29.1: 361.78 KB (5.90%)
ramda: 361.78 KB (100%)
<self>: 0 B (0.00%)
.pnpm/ramda-adjunct@4.1.1_ramda@0.29.1: 257.62 KB (4.20%)
ramda-adjunct: 257.62 KB (100%)
<self>: 0 B (0.00%)
.pnpm/lodash@4.17.21: 253.56 KB (4.14%)
lodash: 253.56 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-ns-openapi-3-0@0.80.0: 203.35 KB (3.32%)
@swagger-api/apidom-ns-openapi-3-0: 203.35 KB (100%)
<self>: 0 B (0.00%)
.pnpm/autolinker@3.16.2: 203.32 KB (3.32%)
autolinker: 203.32 KB (100%)
<self>: 0 B (0.00%)
.pnpm/swagger-client@3.23.1: 166.55 KB (2.72%)
swagger-client: 166.55 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-ns-openapi-3-1@0.80.0: 151.35 KB (2.47%)
@swagger-api/apidom-ns-openapi-3-1: 151.35 KB (100%)
<self>: 0 B (0.00%)
.pnpm/immutable@3.8.2: 139.01 KB (2.27%)
immutable: 139.01 KB (100%)
<self>: 0 B (0.00%)
.pnpm/remarkable@2.0.1: 125.56 KB (2.05%)
remarkable: 125.56 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-dom@17.0.2_react@17.0.2: 119.19 KB (1.94%)
react-dom: 119.19 KB (100%)
<self>: 0 B (0.00%)
.pnpm/highlight.js@10.7.3: 111.85 KB (1.82%)
highlight.js: 111.85 KB (100%)
<self>: 0 B (0.00%)
.pnpm/js-yaml@4.1.0: 105.01 KB (1.71%)
js-yaml: 105.01 KB (100%)
<self>: 0 B (0.00%)
.pnpm/readable-stream@3.6.2: 96.66 KB (1.58%)
readable-stream: 96.66 KB (100%)
<self>: 0 B (0.00%)
.pnpm/core-js-pure@3.33.1: 82.98 KB (1.35%)
core-js-pure: 82.98 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-reference@0.80.0: 81.46 KB (1.33%)
@swagger-api/apidom-reference: 81.46 KB (100%)
<self>: 0 B (0.00%)
.pnpm/dompurify@3.0.6: 62.88 KB (1.03%)
dompurify: 62.88 KB (100%)
<self>: 0 B (0.00%)
.pnpm/minim@0.23.8: 57.35 KB (0.935%)
minim: 57.35 KB (100%)
<self>: 0 B (0.00%)
.pnpm/buffer@6.0.3: 56.99 KB (0.929%)
buffer: 56.99 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-ns-json-schema-draft-4@0.80.0: 54.6 KB (0.890%)
@swagger-api/apidom-ns-json-schema-draft-4: 54.6 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-redux@8.1.3_react-dom@17.0.2_react@17.0.2_redux@4.2.1: 48.26 KB (0.787%)
react-redux: 48.26 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-core@0.80.0: 48.12 KB (0.785%)
@swagger-api/apidom-core: 48.12 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-ast@0.80.0: 46.52 KB (0.759%)
@swagger-api/apidom-ast: 46.52 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-syntax-highlighter@15.5.0_react@17.0.2: 40.15 KB (0.655%)
react-syntax-highlighter: 40.15 KB (100%)
<self>: 0 B (0.00%)
.pnpm/fast-json-patch@3.1.1: 31.89 KB (0.520%)
fast-json-patch: 31.89 KB (100%)
<self>: 0 B (0.00%)
.pnpm/redux@4.2.1: 26.64 KB (0.434%)
redux: 26.64 KB (100%)
<self>: 0 B (0.00%)
.pnpm/qs@6.11.2: 26.23 KB (0.428%)
qs: 26.23 KB (100%)
<self>: 0 B (0.00%)
.pnpm/short-unique-id@5.0.3: 18.88 KB (0.308%)
short-unique-id: 18.88 KB (100%)
<self>: 0 B (0.00%)
.pnpm/sha.js@2.4.11: 18.57 KB (0.303%)
sha.js: 18.57 KB (100%)
<self>: 0 B (0.00%)
.pnpm/object-inspect@1.13.1: 18.45 KB (0.301%)
object-inspect: 18.44 KB (99.9%)
<self>: 15 B (0.0794%)
.pnpm/url-parse@1.5.10: 16.23 KB (0.265%)
url-parse: 16.23 KB (100%)
<self>: 0 B (0.00%)
.pnpm/tslib@2.6.2: 15.87 KB (0.259%)
tslib: 15.87 KB (100%)
<self>: 0 B (0.00%)
.pnpm/events@3.3.0: 14.54 KB (0.237%)
events: 14.54 KB (100%)
<self>: 0 B (0.00%)
.pnpm/get-intrinsic@1.2.2: 13.01 KB (0.212%)
get-intrinsic: 13.01 KB (100%)
<self>: 0 B (0.00%)
.pnpm/zenscroll@4.0.2: 12.31 KB (0.201%)
zenscroll: 12.31 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-debounce-input@3.3.0_react@17.0.2: 11.95 KB (0.195%)
react-debounce-input: 11.95 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-immutable-proptypes@2.2.0_immutable@3.8.2: 11.82 KB (0.193%)
react-immutable-proptypes: 11.82 KB (100%)
<self>: 0 B (0.00%)
.pnpm/ret@0.2.2: 10.82 KB (0.176%)
ret: 10.82 KB (100%)
<self>: 0 B (0.00%)
.pnpm/lodash.debounce@4.0.8: 10.53 KB (0.172%)
lodash.debounce: 10.53 KB (100%)
<self>: 0 B (0.00%)
.pnpm/unraw@3.0.0: 9.9 KB (0.161%)
unraw: 9.9 KB (100%)
<self>: 0 B (0.00%)
.pnpm/string_decoder@1.3.0: 9.24 KB (0.151%)
string_decoder: 9.24 KB (100%)
<self>: 0 B (0.00%)
.pnpm/reselect@4.1.8: 8.85 KB (0.144%)
reselect: 8.85 KB (100%)
<self>: 0 B (0.00%)
.pnpm/xml@1.0.1: 7.39 KB (0.121%)
xml: 7.39 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-copy-to-clipboard@5.1.0_react@17.0.2: 7.33 KB (0.120%)
react-copy-to-clipboard: 7.33 KB (100%)
<self>: 0 B (0.00%)
.pnpm/traverse@0.6.7: 7.15 KB (0.117%)
traverse: 7.15 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react@17.0.2: 6.48 KB (0.106%)
react: 6.48 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-json-pointer@0.80.0: 6.39 KB (0.104%)
@swagger-api/apidom-json-pointer: 6.39 KB (100%)
<self>: 0 B (0.00%)
.pnpm/randexp@0.5.3: 6.15 KB (0.100%)
randexp: 6.15 KB (100%)
<self>: 0 B (0.00%)
.pnpm/react-immutable-pure-component@2.2.2_immutable@3.8.2_react-dom@17.0.2_react@17.0.2: 6.01 KB (0.0980%)
react-immutable-pure-component: 6.01 KB (100%)
<self>: 0 B (0.00%)
.pnpm/redux-immutable@4.0.0_immutable@3.8.2: 5.43 KB (0.0886%)
redux-immutable: 5.43 KB (100%)
<self>: 0 B (0.00%)
.pnpm/process@0.11.10: 5.29 KB (0.0863%)
process: 5.29 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@babel+runtime@7.23.2: 5.1 KB (0.0833%)
@babel/runtime: 5.1 KB (100%)
<self>: 0 B (0.00%)
.pnpm/cookie@0.5.0: 5.1 KB (0.0832%)
cookie: 5.1 KB (100%)
<self>: 0 B (0.00%)
.pnpm/scheduler@0.20.2: 4.91 KB (0.0801%)
scheduler: 4.91 KB (100%)
<self>: 0 B (0.00%)
.pnpm/drange@1.1.1: 4.8 KB (0.0783%)
drange: 4.8 KB (100%)
<self>: 0 B (0.00%)
.pnpm/lowlight@1.20.0: 4.42 KB (0.0721%)
lowlight: 4.42 KB (100%)
<self>: 0 B (0.00%)
.pnpm/deep-extend@0.6.0: 4.19 KB (0.0684%)
deep-extend: 4.19 KB (100%)
<self>: 0 B (0.00%)
.pnpm/deepmerge@4.3.1: 3.95 KB (0.0645%)
deepmerge: 3.95 KB (100%)
<self>: 0 B (0.00%)
.pnpm/base64-js@1.5.1: 3.84 KB (0.0626%)
base64-js: 3.84 KB (100%)
<self>: 0 B (0.00%)
.pnpm/stream-browserify@3.0.0: 3.76 KB (0.0613%)
stream-browserify: 3.76 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@swagger-api+apidom-error@0.80.0: 3.49 KB (0.0570%)
@swagger-api/apidom-error: 3.49 KB (100%)
<self>: 0 B (0.00%)
.pnpm/side-channel@1.0.4: 3.31 KB (0.0540%)
side-channel: 3.31 KB (100%)
<self>: 0 B (0.00%)
.pnpm/copy-to-clipboard@3.3.3: 3.29 KB (0.0536%)
copy-to-clipboard: 3.29 KB (100%)
<self>: 0 B (0.00%)
.pnpm/format@0.2.2: 3.26 KB (0.0531%)
format: 3.26 KB (100%)
<self>: 0 B (0.00%)
.pnpm/stampit@4.3.2: 3.16 KB (0.0515%)
stampit: 3.16 KB (100%)
<self>: 0 B (0.00%)
.pnpm/css.escape@1.5.1: 3.08 KB (0.0502%)
css.escape: 3.08 KB (100%)
<self>: 0 B (0.00%)
.pnpm/serialize-error@8.1.0: 2.93 KB (0.0477%)
serialize-error: 2.93 KB (100%)
<self>: 0 B (0.00%)
.pnpm/define-data-property@1.1.1: 2.77 KB (0.0452%)
define-data-property: 2.77 KB (100%)
<self>: 0 B (0.00%)
.pnpm/hoist-non-react-statics@3.3.2: 2.68 KB (0.0437%)
hoist-non-react-statics: 2.68 KB (100%)
<self>: 0 B (0.00%)
.pnpm/prop-types@15.8.1: 2.6 KB (0.0424%)
prop-types: 2.6 KB (100%)
<self>: 0 B (0.00%)
.pnpm/use-sync-external-store@1.2.0_react@17.0.2: 2.59 KB (0.0423%)
use-sync-external-store: 2.59 KB (100%)
<self>: 0 B (0.00%)
.pnpm/querystringify@2.2.0: 2.5 KB (0.0408%)
querystringify: 2.5 KB (100%)
<self>: 0 B (0.00%)
.pnpm/xml-but-prettier@1.0.1: 2.17 KB (0.0353%)
xml-but-prettier: 2.17 KB (100%)
<self>: 0 B (0.00%)
.pnpm/has-symbols@1.0.3: 2.13 KB (0.0347%)
has-symbols: 2.13 KB (100%)
<self>: 0 B (0.00%)
.pnpm/function-bind@1.1.2: 2.12 KB (0.0345%)
function-bind: 2.12 KB (100%)
<self>: 0 B (0.00%)
.pnpm/ieee754@1.2.1: 2.1 KB (0.0343%)
ieee754: 2.1 KB (100%)
<self>: 0 B (0.00%)
.pnpm/object-assign@4.1.1: 2.06 KB (0.0336%)
object-assign: 2.06 KB (100%)
<self>: 0 B (0.00%)
.pnpm/call-bind@1.0.5: 1.59 KB (0.0259%)
call-bind: 1.59 KB (100%)
<self>: 0 B (0.00%)
.pnpm/util-deprecate@1.0.2: 1.58 KB (0.0257%)
util-deprecate: 1.58 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@braintree+sanitize-url@6.0.4: 1.56 KB (0.0255%)
@braintree/sanitize-url: 1.56 KB (100%)
<self>: 0 B (0.00%)
.pnpm/randombytes@2.1.0: 1.54 KB (0.0251%)
randombytes: 1.54 KB (100%)
<self>: 0 B (0.00%)
.pnpm/js-file-download@0.4.12: 1.52 KB (0.0248%)
js-file-download: 1.52 KB (100%)
<self>: 0 B (0.00%)
.pnpm/classnames@2.3.2: 1.38 KB (0.0226%)
classnames: 1.38 KB (100%)
<self>: 0 B (0.00%)
.pnpm/repeat-string@1.6.1: 1.18 KB (0.0193%)
repeat-string: 1.18 KB (100%)
<self>: 0 B (0.00%)
.pnpm/set-function-length@1.1.1: 1.14 KB (0.0186%)
set-function-length: 1.14 KB (100%)
<self>: 0 B (0.00%)
.pnpm/@babel+runtime-corejs3@7.23.2: 878 B (0.0140%)
@babel/runtime-corejs3: 878 B (100%)
<self>: 0 B (0.00%)
.pnpm/has-property-descriptors@1.0.1: 817 B (0.0130%)
has-property-descriptors: 817 B (100%)
<self>: 0 B (0.00%)
.pnpm/toggle-selection@1.0.6: 780 B (0.0124%)
toggle-selection: 780 B (100%)
<self>: 0 B (0.00%)
.pnpm/is-plain-object@5.0.0: 758 B (0.0121%)
is-plain-object: 758 B (100%)
<self>: 0 B (0.00%)
.pnpm/inherits@2.0.4: 753 B (0.0120%)
inherits: 753 B (100%)
<self>: 0 B (0.00%)
.pnpm/requires-port@1.0.0: 753 B (0.0120%)
requires-port: 753 B (100%)
<self>: 0 B (0.00%)
.pnpm/fault@1.0.4: 691 B (0.0110%)
fault: 691 B (100%)
<self>: 0 B (0.00%)
.pnpm/gopd@1.0.1: 263 B (0.00419%)
gopd: 263 B (100%)
<self>: 0 B (0.00%)
.pnpm/hasown@2.0.0: 234 B (0.00373%)
hasown: 234 B (100%)
<self>: 0 B (0.00%)
.pnpm/has-proto@1.0.1: 197 B (0.00314%)
has-proto: 197 B (100%)
<self>: 0 B (0.00%)
<self>: 2.74 MB (45.8%)

View file

@ -1,16 +0,0 @@
readable-stream: 96.66 KB (7.62%)
buffer: 56.99 KB (4.49%)
core-js-pure: 25.99 KB (2.05%)
sha.js: 18.57 KB (1.46%)
events: 14.54 KB (1.15%)
string_decoder: 9.24 KB (0.728%)
xml: 7.39 KB (0.582%)
process: 5.29 KB (0.417%)
deep-extend: 4.19 KB (0.330%)
stream-browserify: 3.76 KB (0.296%)
safe-buffer: 1.63 KB (0.129%)
util-deprecate: 1.58 KB (0.124%)
randombytes: 1.54 KB (0.121%)
inherits: 753 B (0.0579%)
@babel/runtime-corejs3: 71 B (0.00546%)
<self>: 1020.83 KB (80.4%)

View file

@ -1,99 +0,0 @@
ramda: 361.65 KB (6.14%)
core-js-pure: 300.89 KB (5.11%)
ramda-adjunct: 255.42 KB (4.34%)
lodash: 253.56 KB (4.30%)
@swagger-api/apidom-ns-openapi-3-0: 204.6 KB (3.47%)
autolinker: 203.32 KB (3.45%)
swagger-client: 159.41 KB (2.71%)
is-plain-object: 758 B (0.464%)
<self>: 158.67 KB (99.5%)
@swagger-api/apidom-ns-openapi-3-1: 154.72 KB (2.63%)
immutable: 139.01 KB (2.36%)
remarkable: 125.56 KB (2.13%)
react-dom: 119.19 KB (2.02%)
highlight.js: 111.85 KB (1.90%)
js-yaml: 105.01 KB (1.78%)
readable-stream: 96.66 KB (1.64%)
@swagger-api/apidom-reference: 84.72 KB (1.44%)
dompurify: 61.27 KB (1.04%)
@swagger-api/apidom-ns-json-schema-draft-4: 58.11 KB (0.987%)
minim: 57.35 KB (0.974%)
buffer: 56.99 KB (0.967%)
@swagger-api/apidom-core: 47.72 KB (0.810%)
@swagger-api/apidom-ast: 44.47 KB (0.755%)
react-redux: 43.7 KB (0.742%)
react-syntax-highlighter: 38.01 KB (0.645%)
url: 37.24 KB (0.632%)
punycode: 14.33 KB (38.5%)
<self>: 22.92 KB (61.5%)
fast-json-patch: 31.89 KB (0.541%)
redux: 27.59 KB (0.468%)
qs: 26.61 KB (0.452%)
sha.js: 18.57 KB (0.315%)
object-inspect: 18.15 KB (0.308%)
url-parse: 16.23 KB (0.276%)
events: 14.54 KB (0.247%)
cross-fetch: 14.31 KB (0.243%)
tslib: 14.21 KB (0.241%)
@babel/runtime-corejs3: 13.24 KB (0.225%)
get-intrinsic: 13.01 KB (0.221%)
reselect: 12.51 KB (0.212%)
zenscroll: 12.31 KB (0.209%)
react-debounce-input: 11.95 KB (0.203%)
react-immutable-proptypes: 11.82 KB (0.201%)
ret: 10.82 KB (0.184%)
lodash.debounce: 10.53 KB (0.179%)
unraw: 10.27 KB (0.174%)
string_decoder: 9.24 KB (0.157%)
short-unique-id: 8.29 KB (0.141%)
xml: 7.39 KB (0.125%)
react-copy-to-clipboard: 7.33 KB (0.124%)
traverse: 7.15 KB (0.121%)
react: 6.48 KB (0.110%)
@babel/runtime: 6.29 KB (0.107%)
randexp: 6.15 KB (0.104%)
react-immutable-pure-component: 6.01 KB (0.102%)
redux-immutable: 5.43 KB (0.0922%)
process: 5.29 KB (0.0898%)
cookie: 5.1 KB (0.0866%)
scheduler: 4.91 KB (0.0834%)
drange: 4.8 KB (0.0815%)
lowlight: 4.42 KB (0.0751%)
deep-extend: 4.19 KB (0.0712%)
deepmerge: 3.95 KB (0.0671%)
base64-js: 3.84 KB (0.0652%)
stream-browserify: 3.76 KB (0.0638%)
@swagger-api/apidom-json-pointer: 3.68 KB (0.0625%)
side-channel: 3.31 KB (0.0562%)
copy-to-clipboard: 3.29 KB (0.0558%)
format: 3.26 KB (0.0553%)
stampit: 3.16 KB (0.0536%)
css.escape: 3.08 KB (0.0523%)
serialize-error: 2.93 KB (0.0497%)
hoist-non-react-statics: 2.68 KB (0.0455%)
prop-types: 2.6 KB (0.0441%)
use-sync-external-store: 2.59 KB (0.0440%)
querystringify: 2.5 KB (0.0425%)
react-is: 2.48 KB (0.0422%)
xml-but-prettier: 2.17 KB (0.0368%)
has-symbols: 2.13 KB (0.0362%)
ieee754: 2.1 KB (0.0357%)
object-assign: 2.06 KB (0.0349%)
call-bind: 1.68 KB (0.0285%)
safe-buffer: 1.63 KB (0.0277%)
util-deprecate: 1.58 KB (0.0268%)
function-bind: 1.55 KB (0.0263%)
randombytes: 1.54 KB (0.0261%)
js-file-download: 1.52 KB (0.0258%)
classnames: 1.38 KB (0.0235%)
@braintree/sanitize-url: 1.38 KB (0.0234%)
repeat-string: 1.18 KB (0.0201%)
toggle-selection: 780 B (0.0129%)
inherits: 753 B (0.0125%)
requires-port: 753 B (0.0125%)
fault: 691 B (0.0115%)
formdata-node: 259 B (0.00429%)
has-proto: 197 B (0.00327%)
has: 129 B (0.00214%)
object-inspect|.: 15 B (0.000249%)
<self>: 2.26 MB (39.2%)

File diff suppressed because one or more lines are too long

View file

@ -1,138 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* @description Recursive object extending
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
* @license MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* https://github.com/Starcounter-Jack/JSON-Patch
* (c) 2017-2021 Joachim Wester
* MIT license
*/
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* repeat-string <https://github.com/jonschlinkert/repeat-string>
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
/*! @license DOMPurify 3.0.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.3/LICENSE */
/*! https://mths.be/punycode v1.4.1 by @mathias */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,35 +0,0 @@
/*!
* @description Recursive object extending
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
* @license MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,138 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/*!
* @description Recursive object extending
* @author Viacheslav Lotsmanov <lotsmanov89@gmail.com>
* @license MIT
*
* The MIT License (MIT)
*
* Copyright (c) 2013-2018 Viacheslav Lotsmanov
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*!
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* https://github.com/Starcounter-Jack/JSON-Patch
* (c) 2017-2021 Joachim Wester
* MIT license
*/
/*!
* is-plain-object <https://github.com/jonschlinkert/is-plain-object>
*
* Copyright (c) 2014-2017, Jon Schlinkert.
* Released under the MIT License.
*/
/*!
* repeat-string <https://github.com/jonschlinkert/repeat-string>
*
* Copyright (c) 2014-2015, Jon Schlinkert.
* Licensed under the MIT License.
*/
/*! @license DOMPurify 3.0.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.0.3/LICENSE */
/*! https://mths.be/punycode v1.4.1 by @mathias */
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/**
* @license React
* use-sync-external-store-shim.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* @license React
* use-sync-external-store-shim/with-selector.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v0.20.2
* scheduler.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-dom.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react-is.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,27 +0,0 @@
/*
object-assign
(c) Sindre Sorhus
@license MIT
*/
/*!
* The buffer module from node.js, for the browser.
*
* @author Feross Aboukhadijeh <https://feross.org>
* @license MIT
*/
/*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh <https://feross.org/opensource> */
/*! js-yaml 4.1.0 https://github.com/nodeca/js-yaml @license MIT */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */
/** @license React v17.0.2
* react.production.min.js
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<. } .>
<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
class="sitekey-form__input"
type="checkbox"

View file

@ -76,7 +76,7 @@ SPDX-License-Identifier: AGPL-3.0-or-later
<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
class="sitekey-form__input"
type="checkbox"

View file

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

View file

@ -3,7 +3,7 @@
//
// SPDX-License-Identifier: MIT OR Apache-2.0
import { Work, ServiceWorkerWork } from "./types";
import { Work, ServiceWorkerMessage } from "./types";
import fetchPoWConfig from "./fetchPoWConfig";
import sendWork from "./sendWork";
import sendToParent from "./sendToParent";
@ -24,6 +24,9 @@ export const registerVerificationEventHandler = (): void => {
};
export const solveCaptchaRunner = async (e: Event): Promise<void> => {
const PROGRESS_FILL = <HTMLElement>document.querySelector(".progress__fill");
let width = 0;
if (LOCK) {
e.preventDefault();
return;
@ -32,6 +35,8 @@ export const solveCaptchaRunner = async (e: Event): Promise<void> => {
try {
LOCK = true;
if (CONST.btn().checked == false) {
width = 0;
PROGRESS_FILL.style.width = `${width}%`;
CONST.messageText().before();
LOCK = false;
return;
@ -43,32 +48,49 @@ export const solveCaptchaRunner = async (e: Event): Promise<void> => {
CONST.messageText().during();
// 1. get config
const config = await fetchPoWConfig();
const max_recorded_nonce = config.max_recorded_nonce;
// 2. prove work
worker.postMessage(config);
worker.onmessage = async (event: MessageEvent) => {
const resp: ServiceWorkerWork = event.data;
console.log(
`Proof generated. Difficuly: ${config.difficulty_factor} Duration: ${resp.work.time}`
);
const resp: ServiceWorkerMessage = event.data;
const proof: Work = {
key: CONST.sitekey(),
string: config.string,
nonce: resp.work.nonce,
result: resp.work.result,
time: Math.trunc(resp.work.time),
worker_type: resp.work.worker_type,
};
if (resp.type === "work") {
width = 80;
PROGRESS_FILL.style.width = `${width}%`;
console.log(
`Proof generated. Difficuly: ${config.difficulty_factor} Duration: ${resp.value.work.time}`
);
// 3. submit work
const token = await sendWork(proof);
// 4. send token
sendToParent(token);
// 5. mark checkbox checked
CONST.btn().checked = true;
CONST.messageText().after();
LOCK = false;
const proof: Work = {
key: CONST.sitekey(),
string: config.string,
nonce: resp.value.work.nonce,
result: resp.value.work.result,
time: Math.trunc(resp.value.work.time),
worker_type: resp.value.work.worker_type,
};
width = 90;
PROGRESS_FILL.style.width = `${width}%`;
// 3. submit work
const token = await sendWork(proof);
// 4. send token
sendToParent(token);
// 5. mark checkbox checked
CONST.btn().checked = true;
width = 100;
PROGRESS_FILL.style.width = `${width}%`;
CONST.messageText().after();
LOCK = false;
}
if (resp.type === "progress") {
if (width < 80) {
width = (resp.nonce / max_recorded_nonce) * 100;
PROGRESS_FILL.style.width = `${width}%`;
}
console.log(`received nonce ${resp.nonce}`);
}
};
} catch (e) {
CONST.messageText().error();

View file

@ -7,106 +7,138 @@
@import "../reset";
.widget__contaienr {
align-items: center;
box-sizing: border-box;
display: flex;
height: 100%;
body {
display: flex;
flex-direction: column;
width: 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%;
}
.widget__noscript-container {
display: flex;
font-size: 0.7rem;
line-height: 20px;
flex-direction: column;
padding: 5px;
box-sizing: border-box;
height: 100%;
margin: auto;
display: flex;
font-size: 0.7rem;
line-height: 20px;
flex-direction: column;
padding: 5px;
box-sizing: border-box;
height: 100%;
margin: auto;
}
.widget__noscript-warning {
display: block;
margin: auto;
flex: 2;
width: 100%;
margin: auto;
display: block;
margin: auto;
flex: 2;
width: 100%;
margin: auto;
}
.widget__source-code {
display: block;
flex: 1;
display: block;
flex: 1;
}
.widget__verification-container {
align-items: center;
display: none;
line-height: 30px;
font-size: 1rem;
align-items: center;
display: none;
line-height: 30px;
font-size: 1rem;
}
.widget__verification-checkbox {
width: 30px;
height: 30px;
margin: 0 10px;
width: 30px;
height: 30px;
margin: 0 10px;
}
#widget__verification-text--during {
display: none;
display: none;
}
#widget__verification-text--after {
display: none;
color: green;
display: none;
color: green;
}
#widget__verification-text--error {
display: none;
color: red;
display: none;
color: red;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--before {
display: none;
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--during {
display: none;
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--error {
display: none;
display: none;
}
.widget__verification-checkbox:checked ~ #widget__verification-text--after {
display: block;
display: block;
}
.widget__mcaptcha-details {
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: 10px;
display: flex;
flex-direction: column;
margin-left: auto;
margin-right: 10px;
}
.widget__mcaptcha-brand-name {
font-size: 0.7rem;
font-weight: 600;
margin: auto;
text-align: center;
font-size: 0.7rem;
font-weight: 600;
margin: auto;
text-align: center;
}
.widget__mcaptcha-logo {
display: block;
width: 35px;
margin: auto;
display: block;
width: 35px;
margin: auto;
}
.widget__mcaptcha-info-container {
display: flex;
margin: auto;
display: flex;
margin: auto;
}
.widget__mcaptcha-info-link {
font-size: 0.5rem;
margin: 2px;
font-size: 0.5rem;
margin: 2px;
}
/* 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%;
}

View file

@ -3,7 +3,7 @@
//
// 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 { 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
* 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 JS = "js";
const STEPS = 5000;
if (WasmSupported) {
let proof: WasmWork = null;
let res: SubmitWork = null;
let time: number = null;
const t0 = performance.now();
const proofString = gen_pow(
const proofString = stepped_gen_pow(
config.salt,
config.string,
config.difficulty_factor
config.difficulty_factor,
STEPS,
progress
);
const t1 = performance.now();
time = t1 - t0;
@ -47,10 +53,12 @@ const prove = async (config: PoWConfig): Promise<SubmitWork> => {
const t0 = performance.now();
proof = await p.generate_work(
proof = await p.stepped_generate_work(
config.salt,
config.string,
config.difficulty_factor
config.difficulty_factor,
STEPS,
progress
);
const t1 = performance.now();
time = t1 - t0;

View file

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

View file

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

3168
yarn.lock

File diff suppressed because it is too large Load diff