0
0
mirror of https://github.com/neon-mmd/websurfx.git synced 2024-11-23 14:38:21 -05:00

Merge pull request #288 from neon-mmd/rolling

🚀 Release the first stable version of the app
This commit is contained in:
alamin655 2023-10-01 09:21:24 +05:30 committed by GitHub
commit ca30fd5f53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 6348 additions and 2095 deletions

20
.cspell.json Normal file
View File

@ -0,0 +1,20 @@
{
"ignorePaths": [
"**/node_modules/**",
"**/vscode-extension/**",
"**/.git/**",
"**/.pnpm-lock.json",
".vscode",
"megalinter",
"package-lock.json",
"report"
],
"language": "en",
"noConfigSearch": true,
"words": [
"megalinter",
"oxsecurity",
"websurfx"
],
"version": "0.2"
}

40
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: 🐛 Bug
description: Report an issue to help improve the project.
title: "🐛 <description>"
labels: ["🛠️ goal: fix","🚦 status: awaiting triage"]
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: false
options:
- "Yes"
- "No"
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this bug?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our Discord community [here](https://discord.gg/SWnda7Mw5u)

View File

@ -1,36 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] <your bug report title>"
labels: bug
assignees: ''
---
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF Websurfx -->
**Version of Websurfx, commit number if you are using on master branch**
<!-- If you are running on master branch using git execute this command
in order to fetch the latest commit ID:
```
git log -1
```
-->
**How did you install Websurfx?**
<!-- Did you install Websurfx following the README ? -->
**What happened?**
<!-- A clear and concise description of what the bug is. -->
**Steps To Reproduce**
<!-- How can we reproduce this issue? (as minimally and as precisely as possible) -->
**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
**Screenshots**
<!-- If applicable, provide screenshots to help explain your problem better. -->
**Additional context**
<!-- Add any other context about the problem here. -->

View File

@ -1 +1,5 @@
blank_issues_enabled: true
blank_issues_enabled: false
contact_links:
- name: Question?
url: https://discord.gg/SWnda7Mw5u
about: Feel free to ask your question by joining our Discord server.

40
.github/ISSUE_TEMPLATE/docs.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: 📝 Documentation issue
description: Found an issue in the documentation? You can use this one!
title: "📝 <description>"
labels: ["📄 aspect: text","🚦 status: awaiting triage"]
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the question or issue, also include what you tried and what didn't work
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: false
options:
- "Yes"
- "No"
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this issue?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our Discord community [here](https://discord.gg/SWnda7Mw5u)

View File

@ -1,27 +0,0 @@
---
name: Engine request
about: 'Suggest a new engine to be add '
title: "[ENGINE] <your engine request title>"
labels: engine
assignees: ''
---
<!-- PLEASE FILL THESE FIELDS, IT REALLY HELPS THE MAINTAINERS OF Websurfx -->
**Working URL of the engine**
<!-- Please check if the engine is responding correctly before submitting it. -->
**Why do you want to add this engine?**
<!-- What's special about this engine? Is it open source or libre? -->
**Features of this engine**
<!-- Features of this engine: Doesn't track its users, fast, easy to integrate, ... -->
**Applicable category of this engine**
<!-- Where should this new engine fit in Websurfx? Current categories in Websurfx:
general, files, images, it, map, music, news, science, social media and videos.
You can add multiple categories at the same time. -->
**Additional context**
<!-- Add any other context about this engine here. -->

72
.github/ISSUE_TEMPLATE/engine.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: ✨ Engine
description: Have a new engine to suggest for Websurfx? Please suggest!
title: '✨ <your engine request title>'
labels: ['⭐ goal: addition', '🚦 status: awaiting triage']
body:
- type: textarea
id: workingUrl
attributes:
label: Working URL of the engine
description: Please check if the engine is responding correctly before submitting it.
validations:
required: true
- type: textarea
id: reason
attributes:
label: Why do you want to add this engine?
description: What's special about this engine? Is it open source or libre?
validations:
required: true
- type: textarea
id: features
attributes:
label: Features of this engine
description: Features of this engine: Doesn't track its users, fast, easy to integrate, or anything else that we can know about.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: true
options:
- 'General'
- 'Files'
- 'Images'
- 'IT'
- 'Map'
- 'Music'
- 'News'
- 'Science'
- 'Social Media'
- 'Videos'
validations:
required: true
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: false
options:
- 'Yes'
- 'No'
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this idea?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our Discord community [here](https://discord.gg/SWnda7Mw5u)

40
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,40 @@
name: 💡 General Feature Request
description: Have a new idea/feature for Websurfx? Please suggest!
title: "✨ <description>"
labels: ["⭐ goal: addition", "🚦 status: awaiting triage"]
body:
- type: textarea
id: description
attributes:
label: Description
description: A brief description of the enhancement you propose, also include what you tried and what worked.
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: Please add screenshots if applicable
validations:
required: false
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: false
options:
- "Yes"
- "No"
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this idea?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our Discord community [here](https://discord.gg/SWnda7Mw5u)

View File

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] <your feature request title>"
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

36
.github/ISSUE_TEMPLATE/other.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: 🧱 Other
description: Use this for any other issues. Please do NOT create blank issues
title: "🧱 <Add your title here>"
labels: ["🚦 status: awaiting triage"]
body:
- type: markdown
attributes:
value: "# Other issue"
- type: textarea
id: issuedescription
attributes:
label: What would you like to share?
description: Provide a clear and concise explanation of your issue.
validations:
required: true
- type: dropdown
id: assignee
attributes:
label: Do you want to work on this issue?
multiple: false
options:
- "Yes"
- "No"
validations:
required: false
- type: textarea
id: extrainfo
attributes:
label: Additional information
description: Is there anything else we should know about this issue?
validations:
required: false
- type: markdown
attributes:
value: |
You can also join our Discord community [here](https://discord.gg/SWnda7Mw5u)

11
.github/label-actions.yml vendored Normal file
View File

@ -0,0 +1,11 @@
"🚦 status: awaiting triage":
issues:
comment: >
To reduce notifications, issues are locked until they are https://github.com/neon-mmd/websurfx/labels/%F0%9F%8F%81%20status%3A%20ready%20for%20dev and to be assigned. You can learn more in our contributing guide https://github.com/neon-mmd/websurfx/blob/rolling/CONTRIBUTING.md
lock: true
"🏁 status: ready for dev":
issues:
comment: >
The issue has been unlocked and is now ready for dev. If you would like to work on this issue, you can comment to have it assigned to you. You can learn more in our contributing guide https://github.com/neon-mmd/websurfx/blob/rolling/CONTRIBUTING.md
unlock: true

27
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,27 @@
'💻 aspect: code':
- src/*
- Cargo.toml
- Cargo.lock
- Dockerfile
- docker-compose.yml
- websurfx/*
'🤖 aspect: dx':
- '**/*.json'
- .dockerignore
- .gitignore
- .gitpod.Dockerfile
- .gitpod.yml
- .rusty-hook.toml
- PULL_REQUEST_TEMPLATE.md
- SECURITY.md
- .github/*
- .mega-linter.yml
- tests/*
'📄 aspect: text':
- any: ['**/*.md', '!PULL_REQUEST_TEMPLATE.md', '!SECURITY.md']
- LICENSE
'🕹️ aspect: interface':
- public/*

48
.github/workflows/contributors.yml vendored Normal file
View File

@ -0,0 +1,48 @@
---
name: Contributors List
on:
workflow_dispatch:
schedule:
- cron: "0 1 * * *"
jobs:
contributors:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@8f4b7f84864484a7bf31766abe9204da3cbe65b3 # v3.5.0
with:
fetch-depth: 0
ref: ${{ github.event.repository.default_branch }}
- name: Update contributors list
uses: wow-actions/contributors-list@b9e91f91a51a55460fdcae64daad0cb8122cdd53 # v1.1.0
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
svgPath: images/contributors_list.svg
round: true
includeBots: false
noCommit: true
- name: Commit & PR
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 # v4.2.4
with:
token: ${{ secrets.GITHUB_TOKEN }}
add-paths: .github/assets/CONTRIBUTORS.svg
commit-message: 'chore: update contributors-list'
committer: GitHub <noreply@github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: workflow/update-contributors-list
base: main
delete-branch: true
title: 'chore: update contributors-list'
body: |
Automated update to `images/contributors_list.svg`

View File

@ -1,3 +1,4 @@
---
name: Welcome first time contributors
on:

16
.github/workflows/issue-lock-unlock.yml vendored Normal file
View File

@ -0,0 +1,16 @@
name: "lock/unlock issue"
on:
issues:
types: labeled
permissions:
issues: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/label-actions@v3
with:
process-only: issues

View File

@ -1,9 +1,10 @@
---
name: Import open source standard labels
on:
push:
branches:
- master
- rolling
jobs:
labels:

89
.github/workflows/mega-linter.yml vendored Normal file
View File

@ -0,0 +1,89 @@
---
# MegaLinter GitHub Action configuration file
# More info at https://megalinter.io
name: MegaLinter
on:
# Trigger mega-linter at every push. Action will also be visible from Pull Requests to rolling
push: # Comment this line to trigger action only on pull-requests (not recommended if you don't pay for GH Actions)
pull_request:
branches: [rolling]
env: # Comment env block if you do not want to apply fixes
# Apply linter fixes configuration
APPLY_FIXES: all # When active, APPLY_FIXES must also be defined as environment variable (in github/workflows/mega-linter.yml or other CI tool)
APPLY_FIXES_EVENT: pull_request # Decide which event triggers application of fixes in a commit or a PR (pull_request, push, all)
APPLY_FIXES_MODE: commit # If APPLY_FIXES is used, defines if the fixes are directly committed (commit) or posted in a PR (pull_request)
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
jobs:
build:
name: MegaLinter
runs-on: ubuntu-latest
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push, comment issues & post new PR
# Remove the ones you do not need
contents: write
issues: write
pull-requests: write
steps:
# Git Checkout
- name: Checkout Code
uses: actions/checkout@v3
with:
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
# MegaLinter
- name: MegaLinter
id: ml
# You can override MegaLinter flavor used to have faster performances
# More info at https://megalinter.io/flavors/
uses: oxsecurity/megalinter/flavors/cupcake@v7.1.0
env:
# All available variables are described in documentation
# https://megalinter.io/configuration/
VALIDATE_ALL_CODEBASE: true # Set ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} to validate only diff with main branch
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ADD YOUR CUSTOM ENV VARIABLES HERE TO OVERRIDE VALUES OF .mega-linter.yml AT THE ROOT OF YOUR REPOSITORY
# Upload MegaLinter artifacts
- name: Archive production artifacts
if: ${{ success() }} || ${{ failure() }}
uses: actions/upload-artifact@v3
with:
name: MegaLinter reports
path: |
megalinter-reports
mega-linter.log
# Create pull request if applicable (for now works only on PR from same repository, not from forks)
- name: Create Pull Request with applied fixes
id: cpr
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }}
commit-message: "[MegaLinter] Apply linters automatic fixes"
title: "[MegaLinter] Apply linters automatic fixes"
labels: bot
- name: Create PR output
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'pull_request' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
run: |
echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}"
echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}"
# Push new commit if applicable (for now works only on PR from same repository, not from forks)
- name: Prepare commit
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
run: sudo chown -Rc $UID .git/
- name: Commit and push applied linter fixes
if: steps.ml.outputs.has_updated_sources == 1 && (env.APPLY_FIXES_EVENT == 'all' || env.APPLY_FIXES_EVENT == github.event_name) && env.APPLY_FIXES_MODE == 'commit' && github.ref != 'refs/heads/main' && (github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository) && !contains(github.event.head_commit.message, 'skip fix')
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}
commit_message: "[MegaLinter] Apply linters fixes"
commit_user_name: megalinter-bot
commit_user_email: nicolas.vuillamy@ox.security

15
.github/workflows/pr_labeler.yml vendored Normal file
View File

@ -0,0 +1,15 @@
name: "Pull Request Auto Labeler"
on:
- pull_request_target
jobs:
triage:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
with:
sync-labels: true
dot: true

View File

@ -1,78 +0,0 @@
name: Releases
on:
push:
branches:
- "rolling"
concurrency:
group: "rolling-branch"
jobs:
changelog:
if: github.repository == 'neon-mmd/websurfx'
runs-on: ubuntu-latest
steps:
# Create a temporary, uniquely named branch to push release info to
- name: create temporary branch
uses: peterjgrainger/action-create-branch@v2.3.0
id: create-branch
with:
branch: "release-from-${{ github.sha }}"
sha: "${{ github.sha }}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# check out the repository afterwards
- uses: actions/checkout@v3
# fetch branches and switch to the temporary branch
- name: switch to new branch
run: git fetch --all && git checkout --track origin/release-from-${{ github.sha }}
# update app config with version
- name: Get current rust app version from its Cargo.toml.
id: foo
uses: dante-signal31/rust-app-version@v1.2.0
with:
cargo_toml_folder: rust_app_folder/
- name: Use the version to update the Cargo.toml version.
shell: bash
run: sed -i "3s/version = \"[0-9]*.[0-9]*.[0-9]*\"/version = \"${{ steps.foo.outputs.app_version }}\"/g" Cargo.toml
# create release info and push it upstream
- name: conventional Changelog Action
id: changelog
uses: TriPSs/conventional-changelog-action@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
version-file: "./Cargo.toml"
git-branch: "release-from-${{ github.sha }}"
skip-on-empty: false
skip-git-pull: true
# create PR using GitHub CLI
- name: create PR with release info
id: create-pr
run: gh pr create --base main --head release-from-${{ github.sha }} --title 'Merge new release into rolling' --body 'Created by Github action'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# merge PR using GitHub CLI
- name: merge PR with release info
id: merge-pr
run: gh pr merge --admin --merge --subject 'Merge release info' --delete-branch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# release info is now in main so we can continue as before
- name: create release with last commit
uses: actions/create-release@v1
if: steps.changelog.outputs.skipped == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.changelog.outputs.tag }}
release_name: ${{ steps.changelog.outputs.tag }}
body: ${{ steps.changelog.outputs.clean_changelog }}

View File

@ -1,12 +1,13 @@
---
name: Rust
on:
push:
branches:
- "**"
- '**'
pull_request:
branches:
- "rolling"
- 'rolling'
env:
CARGO_TERM_COLOR: always
@ -20,23 +21,27 @@ jobs:
- stable
steps:
- uses: actions/checkout@v3
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ""
shared-key: ""
key: ""
env-vars: ""
workspaces: ""
cache-directories: ""
cache-targets: ""
cache-on-failure: ""
cache-all-crates: ""
save-if: ""
- uses: actions/checkout@v3
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
- name: Install LuaJIT and Lua
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends liblua5.4-dev liblua5.3-dev liblua5.2-dev liblua5.1-0-dev libluajit-5.1-dev
- uses: actions/checkout@v3
- run: rustup toolchain install stable --profile minimal
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ''
shared-key: ''
key: ''
env-vars: ''
workspaces: ''
cache-directories: ''
cache-targets: ''
cache-on-failure: ''
cache-all-crates: ''
save-if: ''
- uses: actions/checkout@v3
- run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }}
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

View File

@ -1,3 +1,4 @@
---
name: Rust format and clippy checks
on:
push:
@ -12,6 +13,10 @@ jobs:
name: Rust project
runs-on: ubuntu-latest
steps:
- name: Install LuaJIT and Lua
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends liblua5.4-dev liblua5.3-dev liblua5.2-dev liblua5.1-0-dev libluajit-5.1-dev
- uses: actions/checkout@v2
- name: Install minimal stable with clippy and rustfmt
uses: actions-rs/toolchain@v1
@ -19,7 +24,16 @@ jobs:
profile: minimal
toolchain: stable
components: rustfmt, clippy
- name: Format
uses: actions-rs/cargo@v1
with:
command: fmt
args: -- --check
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --all-targets --all
- name: Run cargo check
uses: actions-rs/cargo@v1
with:

View File

@ -1,3 +1,4 @@
---
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# You can adjust the behavior by modifying this file.

7
.gitignore vendored
View File

@ -1 +1,8 @@
.vscode
/target
dhat-heap.json
dump.rdb
megalinter-reports/
package-lock.json
package.json
result

3
.gitpod.Dockerfile vendored Normal file
View File

@ -0,0 +1,3 @@
FROM gitpod/workspace-rust
RUN sudo install-packages redis-server nodejs npm liblua5.4-dev liblua5.3-dev liblua5.2-dev liblua5.1-0-dev libluajit-5.1-dev

50
.gitpod.yml Normal file
View File

@ -0,0 +1,50 @@
---
image:
file: .gitpod.Dockerfile
# Commands that will run on workspace start
tasks:
- name: Start Redis Server
command: redis-server --port 8082
- name: Run The App
init: cargo build
command: PKG_ENV=dev ./target/debug/websurfx
- name: Tests
command: cargo test
- name: Clippy Checks
command: cargo clippy
# vscode IDE setup
vscode:
extensions:
- vadimcn.vscode-lldb
- cschleiden.vscode-github-actions
- rust-lang.rust-analyzer
- bungcip.better-toml
- serayuzgur.crates
- usernamehw.errorlens
- DavidAnson.vscode-markdownlint
- esbenp.prettier-vscode
- stylelint.vscode-stylelint
- dbaeumer.vscode-eslint
- evgeniypeshkov.syntax-highlighter
- ms-azuretools.vscode-docker
- Catppuccin.catppuccin-vsc
- PKief.material-icon-theme
- oderwat.indent-rainbow
- formulahendry.auto-rename-tag
- swellaby.vscode-rust-test-adapter
- belfz.search-crates-io
- hbenl.test-adapter-converter
- hbenl.vscode-test-explorer
- eamodio.gitlens
github:
prebuilds:
master: true
branches: true
pullRequests: true
pullRequestsFromForks: true
addCheck: true
addComment: false
addBadge: true

22
.mega-linter.yml Normal file
View File

@ -0,0 +1,22 @@
---
# Configuration file for MegaLinter
# See all available variables at https://megalinter.io/configuration/ and in linters documentation
APPLY_FIXES: all # all, none, or list of linter keys
# ENABLE: # If you use ENABLE variable, all other languages/formats/tooling-formats will be disabled by default
ENABLE_LINTERS: # If you use ENABLE_LINTERS variable, all other linters will be disabled by default
- RUST_CLIPPY
- JAVASCRIPT_ES
- CSS_STYLELINT
- MARKDOWN_MARKDOWNLINT
- YAML_YAMLLINT
- HTML_DJLINT
- ACTION_ACTIONLINT
- DOCKERFILE_HADOLINT
- SPELL_CSPELL
# DISABLE:
# - COPYPASTE # Uncomment to disable checks of excessive copy-pastes
# - SPELL # Uncomment to disable checks of spelling mistakes
SHOW_ELAPSED_TIME: true
FILEIO_REPORTER: false
# DISABLE_ERRORS: true # Uncomment if you want MegaLinter to detect errors but not block CI to pass

5
.rusty-hook.toml Normal file
View File

@ -0,0 +1,5 @@
[hooks]
pre-commit = "cargo test && cargo fmt -- --check && cargo clippy && stylelint ./public/static/themes/*.css ./public/static/colorschemes/*.css ./public/static/*.js"
[logging]
verbose = true

13
.stylelintrc.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "stylelint-config-standard",
"rules": {
"alpha-value-notation": "number",
"selector-class-pattern": null
},
"overrides": [
{
"files": ["*.js"],
"customSyntax": "postcss-lit"
}
]
}

View File

@ -2,7 +2,7 @@
## Documentation/Wiki
Found a typo, or something that isn't as clear as it could be? Maybe I've missed something off altogether, or you hit a roadblock that took you a while to figure out. Edit the [wiki](https://github.com/neon-mmd/websurfx/wiki) to add to or improve the documentation. This will help future users get Websurfx up and running more easily.
Found a typo, or something that isn't as clear as it could be? Maybe I've missed something off altogether, or you hit a roadblock that took you a while to figure out. Edit the [docs](./docs/) to add to or improve the documentation. This will help future users get Websurfx up and running more easily.
## Readme
@ -14,7 +14,7 @@ Know how to fix or improve a github action?. Consider Submitting a Pull request
## Source Code
You should know atleast one of the things below to start contributing:
You should know at least one of the things below to start contributing:
- Rust basics
- Actix-web crate basics
@ -48,23 +48,7 @@ We have a [Discord](https://discord.gg/SWnda7Mw5u) channel, feel free to join an
# Where To Contribute?
## For Source Code Contributions
The _rolling branch_ is where we intend all source code contributions should go.
## For Readme Contributions
The _master branch_ is where we intend all source code contributions should go.
# How To Fork
![image](./images/fork_button.png)
![image](./images/fork_options_page.png)
Please make sure to leave the `Copy the master branch only` option ticked off.
![image](./images/create_fork_button.png)
The _rolling branch_ is where we intend all contributions should go.
We appreciate any contributions whether be of any size or topic and suggestions to help improve the Websurfx project. Please keep in mind the above requirements and guidelines before submitting a pull request and also if you have any doubts/concerns/questions about the project, its source code or anything related to the project than feel free to ask by opening an [issue](https://github.com/neon-mmd/websurfx/issues) or by asking us on our [Discord](https://discord.gg/SWnda7Mw5u) channel.

2070
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,73 @@
[package]
name = "websurfx"
version = "0.6.0"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "An open-source alternative to Searx that provides clean, ad-free, and organic results with incredible speed while keeping privacy and security in mind."
repository = "https://github.com/neon-mmd/websurfx"
license = "AGPL-3.0"
[dependencies]
reqwest = {version="*",features=["json"]}
tokio = {version="*",features=["full"]}
serde = {version="*",features=["derive"]}
handlebars = { version = "4.3.6", features = ["dir_source"] }
scraper = {version="*"}
actix-web = {version="4.3.1"}
reqwest = {version="0.11.20",features=["json"]}
tokio = {version="1.32.0",features=["rt-multi-thread","macros"]}
serde = {version="1.0.188",features=["derive"]}
handlebars = { version = "4.4.0", features = ["dir_source"] }
scraper = {version="0.17.1"}
actix-web = {version="4.4.0", features = ["cookies"]}
actix-files = {version="0.6.2"}
serde_json = {version="*"}
fake-useragent = {version="*"}
actix-cors = {version="0.6.4"}
serde_json = {version="1.0.105"}
fake-useragent = {version="0.1.3"}
env_logger = {version="0.10.0"}
log = {version="0.4.17"}
rlua = {version="*"}
redis = {version="*"}
md5 = {version="*"}
rand={version="*"}
log = {version="0.4.20"}
mlua = {version="0.8.10", features=["luajit", "vendored"]}
redis = {version="0.23.3", features=["tokio-comp","connection-manager"], optional = true}
md5 = {version="0.7.0"}
rand={version="0.8.5"}
once_cell = {version="1.18.0"}
error-stack = {version="0.4.0"}
async-trait = {version="0.1.73"}
regex = {version="1.9.4", features=["perf"]}
smallvec = {version="1.11.0", features=["union", "serde"]}
futures = {version="0.3.28"}
dhat = {version="0.3.2", optional = true}
mimalloc = { version = "0.1.38", default-features = false }
async-once-cell = {version="0.5.3"}
actix-governor = {version="0.4.1"}
mini-moka = { version="0.10", optional = true}
[dev-dependencies]
rusty-hook = "^0.11.2"
criterion = "0.5.1"
tempfile = "3.8.0"
[profile.dev]
opt-level = 0
debug = true
split-debuginfo = '...'
debug-assertions = true
overflow-checks = true
lto = false
panic = 'unwind'
incremental = true
codegen-units = 256
rpath = false
[profile.release]
opt-level = 3
debug = false # This should only be commented when testing with dhat profiler
# debug = 1 # This should only be uncommented when testing with dhat profiler
split-debuginfo = '...'
debug-assertions = false
overflow-checks = false
lto = true
panic = 'abort'
incremental = false
codegen-units = 1
rpath = false
strip = "debuginfo"
[features]
default = ["memory-cache"]
dhat-heap = ["dep:dhat"]
memory-cache = ["dep:mini-moka"]
redis-cache = ["dep:redis"]

View File

@ -1,9 +1,9 @@
FROM rust:latest AS chef
# We only pay the installation cost once,
# it will be cached from the second build onwards
RUN cargo install cargo-chef
RUN cargo install cargo-chef --locked
WORKDIR app
WORKDIR /app
FROM chef AS planner
COPY . .
@ -12,15 +12,29 @@ RUN cargo chef prepare --recipe-path recipe.json
FROM chef AS builder
COPY --from=planner /app/recipe.json recipe.json
# Build dependencies - this is the caching Docker layer!
# Uncomment the line below if you want to use the `hybrid` caching feature.
# RUN cargo chef cook --release --features redis-cache --recipe-path recipe.json
# Comment the line below if you don't want to use the `In-Memory` caching feature.
RUN cargo chef cook --release --recipe-path recipe.json
# Uncomment the line below if you want to use the `no cache` feature.
# RUN cargo chef cook --release --no-default-features --recipe-path recipe.json
# Uncomment the line below if you want to use the `redis` caching feature.
# RUN cargo chef cook --release --no-default-features --features redis-cache --recipe-path recipe.json
# Build application
COPY . .
# Uncomment the line below if you want to use the `hybrid` caching feature.
# RUN cargo install --path . --features redis-cache
# Comment the line below if you don't want to use the `In-Memory` caching feature.
RUN cargo install --path .
# Uncomment the line below if you want to use the `no cache` feature.
# RUN cargo install --path . --no-default-features
# Uncomment the line below if you want to use the `redis` caching feature.
# RUN cargo install --path . --no-default-features --features redis-cache
# We do not need the Rust toolchain to run the binary!
FROM gcr.io/distroless/cc-debian11
COPY --from=builder ./public/ ./public/
COPY --from=builder ./websurfx/ ./websurfx/
FROM gcr.io/distroless/cc-debian12
COPY --from=builder /app/public/ /opt/websurfx/public/
VOLUME ["/etc/xdg/websurfx/"]
COPY --from=builder /usr/local/cargo/bin/* /usr/local/bin/
CMD ["websurfx"]

View File

@ -16,7 +16,7 @@
## Author's checklist
<!-- additional notes for reviewiers -->
<!-- additional notes for reviewers -->
## Related issues

View File

@ -1,12 +1,22 @@
<h1 align="center">
<h1 align="center">
<img src="./images/websurfx_logo.png" alt="websurfx logo" align="center" />
</h1>
<p align="center">
<b align="center"><a href="README.md">Readme</a></b> |
<b><a href="https://discord.gg/SWnda7Mw5u">Discord</a></b> |
<b><a href="../../tree/HEAD/docs/instances.md">Instances</a></b> |
<b><a href="https://discord.gg/VKCAememnr">User Showcase</a></b> |
<b><a href="https://github.com/neon-mmd/websurfx">GitHub</a></b> |
<b><a href="./docs/README.md">Documentation</a></b>
<b><a href="../../tree/HEAD/docs/">Documentation</a></b>
<br /><br />
<a
href="https://github.com/awesome-selfhosted/awesome-selfhosted#search-engines"
>
<img
src="https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg"
alt="Awesome Self-Hosted"
/>
</a>
<a href="#">
<img
alt="GitHub code size in bytes"
@ -39,7 +49,7 @@
>meta search engine</a
>
(pronounced as websurface or web-surface /wɛbˈːrfəs/.) written in Rust. It
provides a quick and secure search experience while maintaining user
provides a quick and secure search experience while completely respecting user
privacy.</i
>
</p>
@ -49,30 +59,31 @@
<p>
- **Getting Started**
- [🔭 Preview](#preview-)
- [🚀 Features](#features-)
- [🛠️ Installation and Testing](#installation-and-testing-)
- [🔧 Configuration](#configuration-)
- [🔭 Preview](#preview-)
- [🚀 Features](#features-)
- [🔗 Instances](instances-)
- [🛠️ Installation and Testing](#installation-and-testing-%EF%B8%8F)
- [🔧 Configuration](#configuration-)
- **Feature Overview**
- [🎨 Theming](#theming-)
- [🌍 Multi-Language Support](#multi-language-support-)
- [🎨 Theming](#theming-)
- [🌍 Multi-Language Support](#multi-language-support-)
- **Community**
- [📊 System Requirements](#system-requirements-)
- [🗨️ FAQ (Frequently Asked Questions)](#faq-frequently-asked-questions-)
- [📣 More Contributers Wanted](#more-contributers-wanted-)
- [💖 Supporting Websurfx](#supporting-websurfx-)
- [📘 Documentation](#documentation-)
- [🛣️ Roadmap](#roadmap-)
- [🙋 Contributing](#contributing-)
- [📜 License](#license-)
- [🤝 Credits](#credits-)
- [📊 System Requirements](#system-requirements-)
- [🗨️ FAQ (Frequently Asked Questions)](#faq-frequently-asked-questions-)
- [📣 More Contributors Wanted](#more-contributors-wanted-)
- [💖 Supporting Websurfx](#supporting-websurfx-)
- [📘 Documentation](#documentation-)
- [🛣️ Roadmap](#roadmap-)
- [🙋 Contributing](#contributing-)
- [📜 License](#license-)
- [🤝 Credits](#credits-)
</p>
</details>
# Preview 🔭
## Main Page
## Home Page
<img align="center" src="./images/main_page.png" />
@ -86,9 +97,15 @@
**[⬆️ Back to Top](#--)**
# Instances 🔗
> For a full list of publicly available community driven `websurfx` instances to test or for daily use. see [**Instances**](./docs/instances.md)
**[⬆️ Back to Top](#--)**
# Features 🚀
- 🎨 High level of customizability with nine color schemes provided by default with a simple theme, also supporting the creation of your custom themes and colorschemes very quickly and easily
- 🎨 Make Websurfx uniquely yours with twelve color schemes provided by default. It also supports creation of custom themes and color schemes in a quick and easy way, so unleash your creativity!
- 🔐 Fast, private, and secure
- 🆓 100% free and open source
- 💨 Ad-free and clean results
@ -104,9 +121,10 @@ Before you can start building `websurfx`, you will need to have `Cargo` installe
To get started with Websurfx, clone the repository, edit the config file, which is located in the `websurfx/` directory, and install the Redis server by following the instructions located [here](https://redis.io/docs/getting-started/) and then run the websurfx server and redis server using the following commands:
``` shell
```shell
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
git checkout stable
cargo build -r
redis-server --port 8082 &
./target/release/websurfx
@ -114,8 +132,10 @@ redis-server --port 8082 &
Once you have started the server, open your preferred web browser and navigate to <http://127.0.0.1:8080> to start using Websurfx.
> **Warning**
> Please be aware that the project is still in the testing phase and is not ready for production use.
> **Note**
>
> 1. The project is no longer in the testing phase and is now ready for production use.
> 2. There are many features still missing like `support for image search`, `different categories`, `quick apps`, etc but they will be added soon as part of future releases.
**[⬆️ Back to Top](#--)**
@ -131,14 +151,14 @@ Websurfx is configured through the config.lua file, located at `websurfx/config.
> For full theming and customization instructions, see: [**Theming**](./docs/theming.md)
Websurfx comes with several themes and color schemes by default, which you can apply and edit through the config file. Supports custom themes and color schemes using CSS, allowing you to develop your own unique-looking website.
Websurfx comes loaded with several themes and color schemes, which you can apply and edit through the config file. It also supports custom themes and color schemes using CSS, allowing you to make it truly yours.
**[⬆️ Back to Top](#--)**
# Multi-Language Support 🌍
> **Note**
> Currently, we do not support other languages, but in the future, we will start accepting contributions regarding language support because we believe that language should not be a barrier to entry.
> Currently, we do not support other languages but we will start accepting contributions regarding language support in the future. We believe language should never be a barrier to entry.
**[⬆️ Back to Top](#--)**
@ -152,19 +172,19 @@ At present, we only support x86_64 architecture systems, but we would love to ha
## Why Websurfx?
The primary purpose of the Websurfx project is to create a fast, secure, and privacy-focused meta-search engine. While there are numerous meta-search engines available, not all of them guarantee the security of their search engine, which is critical for maintaining privacy. Memory flaws, for example, can expose private or sensitive information, which is never a good thing. Also, there is the added problem of Spam, ads, and unorganic results which most engines don't have the full-proof answer to it till now but with Websurfx I finally put a full stop to this problem, also, Rust is used to write Websurfx, which ensures memory safety and removes such issues. Many meta-search engines also lack important features like advanced picture search, which is required by many graphic designers, content providers, and others. Websurfx attempts to improve the user experience by providing these and other features, such as proper NSFW blocking and Micro-apps or Quick results (like providing a calculator, currency exchanges, etc in the search results).
The primary purpose of the Websurfx project is to create a fast, secure, and privacy-focused meta-search engine. There are numerous meta-search engines available, but not all guarantee the security of their search engine, which is critical for maintaining privacy. Memory flaws, for example, can expose private or sensitive information, which is understandably bad. There is also the added problem of spam, ads, and inorganic results which most engines don't have a fool-proof answer to. Until now. With Websurfx I finally put a full stop to this problem. Websurfx is based on Rust, which ensures memory safety and removes such issues. Many meta-search engines also lack important features like advanced picture search, required by graphic designers, content providers, and others. Websurfx improves the user experience by providing these and other features, such as proper NSFW blocking and Micro-apps or Quick Results (providing a calculator, currency exchanges, etc in the search results).
## Why AGPLv3?
Websurfx is distributed under the **AGPLv3** license to keep the source code open and transparent. This helps to keep malware, telemetry, and other dangerous programs out of the project. **AGPLv3** is a strong copyleft license that ensures the software's source code, including any modifications or improvements made to the code, remains open and available to everyone.
Websurfx is distributed under the **AGPLv3** license to keep the source code open and transparent. This helps keep malware, telemetry, and other dangers out of the project. **AGPLv3** is a strong copyleft license that ensures the software's source code, including any modifications or improvements made to the code, remains open and available to everyone.
## Why Rust?
Rust was chosen as the programming language for Websurfx because of its memory safety features, which can help prevent vulnerabilities and make the codebase more secure. Rust is also faster than C++, which contributes to Websurfx's speed and responsiveness. Furthermore, the Rust ownership and borrowing system enables secure concurrency and thread safety in the program.
Websurfx is based on Rust due to its memory safety features, which prevents vulnerabilities and makes the codebase more secure. Rust is also faster than C++, contributing to Websurfx's speed and responsiveness. Finally, the Rust ownership and borrowing system enables secure concurrency and thread safety in the program.
**[⬆️ Back to Top](#--)**
# More Contributers Wanted 📣
# More Contributors Wanted 📣
We are looking for more willing contributors to help grow this project. For more information on how you can contribute, check out the [project board](https://github.com/neon-mmd/websurfx/projects?query=is%3Aopen) and the [CONTRIBUTING.md](CONTRIBUTING.md) file for guidelines and rules for making contributions.
@ -174,14 +194,15 @@ We are looking for more willing contributors to help grow this project. For more
> For full details and other ways you can help out, see: [**Contributing**]()
If you use Websurfx and would like to contribute to its development, that would be fantastic! Contributions of any size or type are always welcome, and we will properly acknowledge your efforts.
If you use Websurfx and would like to contribute to its development, we're glad to have you on board! Contributions of any size or type are always welcome, and we will always acknowledge your efforts.
Several areas that we need a bit of help with at the moment are:
- **Better and more color schemes**: Help fix color schemes and add other famous color schemes.
- **Improve evasion code for bot detection** - Help improve code related to evading IP blocking and emulating human behaviors located in everyone's engine file.
- **Logo** - Help create a logo for the project and website.
- **Docker Support** - Help write a Docker Compose file for the project.
- Submit a PR to add a new feature, fix a bug, update the docs, add a theme, widget, or something else.
- Submit a PR to add a new feature, fix a bug, update the docs, add a theme, widget, or anything else.
- Star Websurfx on GitHub.
**[⬆️ Back to Top](#--)**
@ -189,19 +210,19 @@ Several areas that we need a bit of help with at the moment are:
# Documentation 📘
> **Note**
> We welcome any contributions to the [documentation](./docs/) as this will benefit everyone who uses this project.
> We welcome any contributions to the [documentation](../../tree/HEAD/docs/) as this will benefit everyone who uses this project.
**[⬆️ Back to Top](#--)**
# Roadmap 🛣️
> Coming soon!! 🙂.
> Coming soon! 🙂.
**[⬆️ Back to Top](#--)**
# Contributing 🙋
Contributions are welcome from anyone. It doesn\'t matter who you are; you can still contribute to the project in your own way.
Contributions are welcome from anyone. It doesn't matter who you are; you can still contribute to the project in your own way.
## Not a developer but still want to contribute?
@ -223,6 +244,8 @@ Websurfx is licensed under the [AGPLv3](LICENSE) license.
We would like to thank the following people for their contributions and support:
**Contributors**
<p>
<br />
<a href="https://github.com/neon-mmd/websurfx/graphs/contributors">
@ -231,6 +254,14 @@ We would like to thank the following people for their contributions and support:
<br />
</p>
**Stargazers**
<p>
<a href="https://github.com/neon-mmd/websurfx/stargazers">
<img src="https://reporoster.com/stars/dark/neon-mmd/websurfx" />
</a>
</p>
**[⬆️ Back to Top](#--)**
---

View File

@ -1,3 +1,4 @@
---
version: "3.9"
services:
app:
@ -5,11 +6,15 @@ services:
build: .
ports:
- 8080:8080
depends_on:
- redis
links:
- redis
redis:
image: redis:latest
ports:
- 6379:6379
# Uncomment the following lines if you are using the `hybrid` or `redis` caching feature.
# depends_on:
# - redis
# links:
# - redis
volumes:
- ./websurfx/:/etc/xdg/websurfx/
# Uncomment the following lines if you are using the `hybrid` or `redis` caching feature.
# redis:
# image: redis:latest
# ports:
# - 6379:6379

View File

@ -7,7 +7,9 @@
# Users
- [Instances](./instances.md)
- [Installation](./installation.md)
- [Features](./features.md)
- [Configuration](./configuration.md)
- [Theming](./theming.md)

View File

@ -11,33 +11,59 @@ If you have built `websurfx` from source then the configuration file will be loc
If you have installed `websurfx` using the package manager of your Linux distro then the default configuration file will be located at `/etc/xdg/websurfx/`. You can copy the default config to `~/.config/websurfx/` and make the changes there and rerun the websurfx server.
Some of the configuration options provided in the file are stated below. These are subdivided into three categories:
Some of the configuration options provided in the file are stated below. These are subdivided into the following categories:
- General
- Server
- Search
- Website
- Cache
- Search Engines
# General
- **logging:** An option to enable or disable logs.
- **debug:** An option to enable or disable debug mode.
- **threads:** The amount of threads that the app will use to run (the value should be greater than 0).
## Server
- **port:** Port number on which server should be launched.
- **binding_ip_addr:** IP address on the which server should be launched.
- **production_use:** Whether to use production mode or not (in other words this option should be used if it is to be used to host it on the server to provide a service to a large number of users). If production_use is set to true. There will be a random delay before sending the request to the search engines, this is to prevent DDoSing the upstream search engines from a large number of simultaneous requests. This is newly added option and hence is only available in the **edge version**.
- **production_use:** Whether to use production mode or not (in other words this option should be used if it is to be used to host it on the server to provide a service to a large number of users). If production_use is set to true. There will be a random delay before sending the request to the search engines, this is to prevent DDoSing the upstream search engines from a large number of simultaneous requests.
- **request_timeout:** Timeout for the search requests sent to the upstream search engines to be fetched (value in seconds).
- **rate_limiter:** The configuration option to configure rate limiting on the search engine website.
## Search
- **safe_search:** This option is used to configure the search filtering based on different safe search levels. (value a number between 0 to 4)
> This option provides 4 levels of search filtering:
>
> - Level 0 - With this level no search filtering occurs.
> - Level 1 - With this level some search filtering occurs.
> - Level 2 - With this level the upstream search engines are restricted to send sensitive contents like NSFW search results, etc.
> - Level 3 - With this level the regex based filter lists is used alongside level 2 to filter more search results that have slipped in or custom results that needs to be filtered using the filter lists.
> - Level 4 - This level is similar to level 3 except in this level the regex based filter lists are used to disallow users to search sensitive or disallowed content. This level could be useful if you are parent or someone who wants to completely disallow their kids or yourself from watching sensitive content.
## Website
- **colorscheme:** The colorscheme name which should be used for the website theme (the name should be in accordance to the colorscheme file name present in `public/static/colorschemes` folder).
> By Default we provide 9 colorschemes to choose from these are:
> By Default we provide 12 colorschemes to choose from these are:
>
> 1. catppuccin-mocha
> 2. dracula
> 3. monokai
> 4. nord
> 5. oceanic-next
> 6. solarized-dark
> 7. solarized-light
> 8. tomorrow-night
> 9. gruvbox-dark
> 2. dark-chocolate
> 3. dracula
> 4. gruvbox-dark
> 5. monokai
> 6. nord
> 7. oceanic-next
> 8. one-dark
> 9. solarized-dark
> 10. solarized-light
> 11. tokyo-night
> 12. tomorrow-night
- **theme:** The theme name which should be used for the website (again, the name should be in accordance to the theme file name present in `public/static/themes` folder).
@ -47,6 +73,13 @@ Some of the configuration options provided in the file are stated below. These a
## Cache
- **redis_connection_url:** Redis connection url address on which the client should connect on.
- **redis_url:** Redis connection url address on which the client should connect on.
[⬅️ Go back to Home](./README.md)
> **Note**
> This option can be commented out if you have compiled the app without the `redis-cache` feature. For more information, See [**building**](./building.md).
## Search Engines
- **upstream_search_engines:** Select from the different upstream search engines from which the results should be fetched.
[⬅️ Go back to Home](./README.md)

42
docs/features.md Normal file
View File

@ -0,0 +1,42 @@
# Features
The project provides 4 caching options as conditionally compiled features. This helps reduce the size of the compiled app by only including the code that is necessary for a particular caching option.
The different caching features provided are as follows:
- No cache
- Redis cache
- In memory cache
- Hybrid cache
## Explaination
### No Cache
This feature can drastically reduce binary size but with the cost that subsequent search requests and previous & next page search results are not cached which can make navigating between pages slower. As well as page refreshes of the same page also becomes slower as each refresh has to fetch the results from the upstream search engines.
### Redis Cache
This feature allows the search engine to cache the results on the redis server. This feature can be useful for having a dedicated cache server for multiple devices hosted with the `Websurfx` server which can use the one dedicated cache server for hosting their cache on it. But a disadvantage of this solution is that if the `Redis`server is located far away (for example provided by a vps as service) and if it is unavailable or down for some reason then the `Websurfx` server would not be able to function properly or will crash on startup.
### In Memory Cache
This feature is the default feature provided by the project. This feature allows the search engine to cache the results in the memory which can help increase the speed of the fetched cache results and it also has an advantage that it is extremely reliable as all the results are stored in memory within the search engine. Though the disadvantage of this solution are that caching of results is slightly slower than the `redis-cache` solution, it requires a good amount of memory on the system and as such is not ideal for very low memory devices and is highly unscalable.
### Hybrid Cache
This feature provides the advantages of both `In Memory` caching and `Redis` caching and it is an ideal solution if you need a very resiliant and reliable solution for the `Websurfx` which can provide both speed and reliability. Like for example if the `Redis` server becomes unavailable then the search engine switches to `In Memory` caching until the server becomes available again. This solution can be useful for hosting `Websurfx` instance which will be used by hundreds or thousands of users over the world.
## Tabular Summary
| **Attributes** | **Hybrid** | **In-Memory** | **No Cache** | **Redis** |
|-----------------------------------------|------------|------------------------------------------------------|-----------------|------------------------|
| **Speed** | Fast | Caching is slow, but retrieval of cache data is fast | Slow | Fastest |
| **Reliability** | ✅ | ✅ | ✅ | ❌ |
| **Scalability** | ✅ | ❌ | - | ✅ |
| **Resiliancy** | ✅ | ✅ | ✅ | ❌ |
| **Production/Large Scale/Instance use** | ✅ | Not Recommended | Not Recommended | Not Recommended |
| **Low Memory Support** | ❌ | ❌ | ✅ | ❌ |
| **Binary Size** | Big | Bigger than `No Cache` | small | Bigger than `No Cache` |
[⬅️ Go back to Home](./README.md)

View File

@ -2,16 +2,17 @@
## Arch Linux
You can install `Websurfx` through the [Aur](https://aur.archlinux.org/packages/websurfx-git), Currently we only support `Rolling/Edge` version. You can install the rolling/edge version by running the following command (using [paru](https://github.com/Morganamilo/paru)):
### Rolling/Edge/Unstable
```bash
You can install `Websurfx` through the [Aur](https://aur.archlinux.org/packages/websurfx-git), By running the following command (using [paru](https://github.com/Morganamilo/paru)):
```shell
paru -S websurfx-edge-git
```
After installing it you can run the websurfx server by running the following commands:
``` bash
redis-server --port 8082 &
```shell
websurfx
```
@ -19,6 +20,78 @@ Once you have started the server, open your preferred web browser and navigate t
If you want to change the port or the ip or any other configuration setting checkout the [configuration docs](./configuration.md).
### Stable
For the stable version, follow the same steps as above (as mentioned for the `unstable/rolling/edge` version) with the only difference being that the package to be installed for stable version is called `websurfx-git` instead of `websurfx-edge-git`.
## NixOS
A `flake.nix` has been provided to allow installing `websurfx` easily. It utilizes [nearsk](https://github.com/nix-community/naersk) to automatically generate a derivation based on `Cargo.toml` and `Cargo.lock`.
The Websurfx project provides 2 versions/flavours for the flake `stable` and `rolling/unstable/edge`. The steps for each are covered below in different sections.
### Rolling/Edge/Unstable
To get started, First clone the repository, edit the config file which is located in the `websurfx` directory and then build and run the websurfx server by running the following commands:
```shell
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
cp -rf ./websurfx/ ~/.config/
$ mkdir /opt/websurfx/
$ cp -rf ./public/ /opt/websurfx/
nix build .#websurfx
nix run .#websurfx
```
> **Note**
> In the above command the dollar sign(**$**) refers to running the command in privilaged mode by using utilities `sudo`, `doas`, `pkgexec` or any other privilage access methods.
Once you have run the above set of commands, then open your preferred web browser and navigate to http://127.0.0.1:8080/ to start using Websurfx.
If you want to change the port or the ip or any other configuration setting checkout the [configuration docs](./configuration.md).
> Optionally, you may include it in your own flake by adding this repo to its inputs and adding it to `environment.systemPackages` as follows:
>
> ```nix
> {
> description = "My awesome configuration";
>
> inputs = {
> websurfx.url = "github:neon-mmd/websurfx";
> };
>
> outputs = { nixpkgs, ... }@inputs: {
> nixosConfigurations = {
> hostname = nixpkgs.lib.nixosSystem {
> system = "x86_64-linux";
> modules = [{
> environment.systemPackages = [inputs.websurfx.packages.x86_64-linux.websurfx];
> }];
> };
> };
> };
> }
> ```
### Stable
For the stable version, follow the same steps as above (as mentioned for the `unstable/rolling/edge version`) with an addition of one command which has to be performed after cloning and changing directory into the repository which makes the building step as follows:
```shell
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
git checkout stable
cp -rf ./websurfx/ ~/.config/
$ mkdir /opt/websurfx/
$ cp -rf ./public/ /opt/websurfx/
nix build .#websurfx
nix run .#websurfx
```
> **Note**
> In the above command the dollar sign(**$**) refers to running the command in privilaged mode by using utilities `sudo`, `doas`, `pkgexec` or any other privilage access methods.
## Other Distros
The package is currently not available on other Linux distros. With contribution and support it can be made available on other distros as well 🙂.
@ -34,6 +107,7 @@ To get started with Websurfx, clone the repository, edit the config file which i
```shell
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
git checkout stable
cargo build -r
redis-server --port 8082 &
./target/release/websurfx
@ -50,13 +124,63 @@ If you want to use the rolling/edge branch, run the following commands instead:
```shell
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
git checkout rolling
```
Once you have changed the directory to the `websurfx` directory then follow the build options listed below:
### Hybrid Cache
> For more information on the features and their pros and cons. see: [**Features**](./features.md)
To build the search engine with the `Hybrid caching` feature. Run the following build command:
```shell
cargo build -r --features redis-cache
```
### Memory Cache (Default Feature)
> For more information on the features and their pros and cons. see: [**Features**](./features.md)
To build the search engine with the `In-Memory caching` feature. Run the following build command:
```shell
cargo build -r
redis-server --port 8082 &
```
### No Cache
> For more information on the features and their pros and cons. see: [**Features**](./features.md)
To build the search engine with the `No caching` feature. Run the following build command:
```shell
cargo build -r --no-default-features
```
### Redis Cache
> For more information on the features and their pros and cons. see: [**Features**](./features.md)
To build the search engine with the `hybrid caching` feature. Run the following build command:
```shell
cargo build -r --no-default-features --features redis-cache
```
> Optionally, If you have build the app with the `Redis cache`or `Hybrid cache` feature (as mentioned above) then before launching the search engine run the following command:
>
> ```shell
> redis-server --port 8082 &
> ```
Once you have finished building the `search engine`. then run the following command to start the search engine:
```shell
./target/release/websurfx
```
Once you have started the server, open your preferred web browser and navigate to http://127.0.0.1:8080/ to start using Websurfx.
Once you have started the server, then launch your preferred web browser and navigate to http://127.0.0.1:8080/ to start using Websurfx.
If you want to change the port or the ip or any other configuration setting checkout the [configuration docs](./configuration.md).
@ -64,7 +188,121 @@ If you want to change the port or the ip or any other configuration setting chec
Before you start, you will need [Docker](https://docs.docker.com/get-docker/) installed on your system first.
## Stable
## Prebuild
The Websurfx project provides several prebuild images based on the different features provided by the search engine. To get started using the prebuild image, you will first need to create a `docker-compose.yml` file with the following content:
```yaml
---
version: '3.9'
services:
app:
# Comment the line below if you don't want to use the `hybrid/latest` image.
image: neonmmd/websurfx:latest
# Uncomment the line below if you want to use the `no cache` image.
# image: neonmmd/websurfx:nocache
# Uncomment the line below if you want to use the `memory` image.
# image: neonmmd/websurfx:memory
# Uncomment the line below if you want to use the `redis` image.
# image: neonmmd/websurfx:redis
ports:
- 8080:8080
# Uncomment the following lines if you are using the `hybrid/latest` or `redis` image.
# depends_on:
# - redis
# links:
# - redis
volumes:
- ./websurfx/:/etc/xdg/websurfx/
# Uncomment the following lines if you are using the `hybrid/latest` or `redis` image.
# redis:
# image: redis:latest
# ports:
# - 6379:6379
```
Then make sure to edit the `docker-compose.yml` file as required. After that create a directory `websurfx` in the directory you have placed the `docker-compose.yml` file, and then in the new directory create two new empty files named `allowlist.txt` and `blocklist.txt`. Finally, create a new config file `config.lua` with the default configuration, which looks something like this:
```lua
-- ### General ###
logging = true -- an option to enable or disable logs.
debug = false -- an option to enable or disable debug mode.
threads = 8 -- the amount of threads that the app will use to run (the value should be greater than 0).
-- ### Server ###
port = "8080" -- port on which server should be launched
binding_ip = "0.0.0.0" --ip address on the which server should be launched.
production_use = false -- whether to use production mode or not (in other words this option should be used if it is to be used to host it on the server to provide a service to a large number of users (more than one))
-- if production_use is set to true
-- There will be a random delay before sending the request to the search engines, this is to prevent DDoSing the upstream search engines from a large number of simultaneous requests.
request_timeout = 30 -- timeout for the search requests sent to the upstream search engines to be fetched (value in seconds).
rate_limiter = {
number_of_requests = 20, -- The number of request that are allowed within a provided time limit.
time_limit = 3, -- The time limit in which the quantity of requests that should be accepted.
}
-- ### Search ###
-- Filter results based on different levels. The levels provided are:
-- {{
-- 0 - None
-- 1 - Low
-- 2 - Moderate
-- 3 - High
-- 4 - Aggressive
-- }}
safe_search = 2
-- ### Website ###
-- The different colorschemes provided are:
-- {{
-- catppuccin-mocha
-- dark-chocolate
-- dracula
-- gruvbox-dark
-- monokai
-- nord
-- oceanic-next
-- one-dark
-- solarized-dark
-- solarized-light
-- tokyo-night
-- tomorrow-night
-- }}
colorscheme = "catppuccin-mocha" -- the colorscheme name which should be used for the website theme
theme = "simple" -- the theme name which should be used for the website
-- ### Caching ###
redis_url = "redis://redis:6379" -- redis connection url address on which the client should connect on.
-- ### Search Engines ###
upstream_search_engines = {
DuckDuckGo = true,
Searx = false,
} -- select the upstream search engines from which the results should be fetched.
```
Then run the following command to deploy the search engine:
```shell
$ docker compose up -d
```
> **Note**
> In the above command the dollar sign(**$**) refers to running the command in privilaged mode by using utilities `sudo`, `doas`, `pkgexec` or any other privilage access methods.
Then launch the browser of your choice and navigate to http://<ip_address_of_the_device>:<whatever_port_you_provided_in_the_config>.
> **Note**
> The official prebuild images only support `stable` versions of the app and will not support `rolling/edge/unstable` versions. But with support and contribution it could be made available for these versions as well 🙂.
## Manual Deployment
This section covers how to deploy the app with docker manually by manually building the image and deploying it.
> **Note**
> This section is provided for those who want to futher customize the docker image or for those who are extra cautious about security.
### Unstable/Edge/Rolling
First clone the the repository by running the following command:
@ -76,49 +314,82 @@ cd websurfx
After that edit the config.lua file located under `websurfx` directory. In the config file you will specifically need to change to values which is `binding_ip_addr` and `redis_connection_url` which should make the config look something like this:
```lua
-- Server
port = "8080" -- port on which server should be launched
binding_ip_addr = "0.0.0.0" --ip address on the which server should be launched.
-- ### General ###
logging = true -- an option to enable or disable logs.
debug = false -- an option to enable or disable debug mode.
threads = 8 -- the amount of threads that the app will use to run (the value should be greater than 0).
-- Website
-- ### Server ###
port = "8080" -- port on which server should be launched
binding_ip = "0.0.0.0" --ip address on the which server should be launched.
production_use = false -- whether to use production mode or not (in other words this option should be used if it is to be used to host it on the server to provide a service to a large number of users (more than one))
-- if production_use is set to true
-- There will be a random delay before sending the request to the search engines, this is to prevent DDoSing the upstream search engines from a large number of simultaneous requests.
request_timeout = 30 -- timeout for the search requests sent to the upstream search engines to be fetched (value in seconds).
rate_limiter = {
number_of_requests = 20, -- The number of request that are allowed within a provided time limit.
time_limit = 3, -- The time limit in which the quantity of requests that should be accepted.
}
-- ### Search ###
-- Filter results based on different levels. The levels provided are:
-- {{
-- 0 - None
-- 1 - Low
-- 2 - Moderate
-- 3 - High
-- 4 - Aggressive
-- }}
safe_search = 2
-- ### Website ###
-- The different colorschemes provided are:
-- {{
-- catppuccin-mocha
-- dark-chocolate
-- dracula
-- gruvbox-dark
-- monokai
-- nord
-- oceanic-next
-- one-dark
-- solarized-dark
-- solarized-light
-- tokyo-night
-- tomorrow-night
-- }}
colorscheme = "catppuccin-mocha" -- the colorscheme name which should be used for the website theme
theme = "simple" -- the theme name which should be used for the website
-- Caching
redis_connection_url = "redis://127.0.0.1:8082" -- redis connection url address on which the client should connect on.
-- ### Caching ###
redis_url = "redis://redis:6379" -- redis connection url address on which the client should connect on.
production_use = false -- whether to use production mode or not (in other words this option should be used if it is to be used to host it on the server to provide a service to a large number of users)
-- if production_use is set to true
-- There will be a random delay before sending the request to the search engines, this is to prevent DDoSing the upstream search engines from a large number of simultaneous requests.
-- ### Search Engines ###
upstream_search_engines = {
DuckDuckGo = true,
Searx = false,
} -- select the upstream search engines from which the results should be fetched.
```
After this run the following command to deploy the app:
After this make sure to edit the `docker-compose.yml` and `Dockerfile` files as required and run the following command to deploy the app:
```bash
docker compose up -d --build
$ docker compose up -d --build
```
> **Note**
> In the above command the dollar sign(**$**) refers to running the command in privilaged mode by using utilities `sudo`, `doas`, `pkgexec` or any other privilage access methods.
This will take around 5-10 mins for first deployment, afterwards the docker build stages will be cached so it will be faster to be build from next time onwards. After the above step finishes launch your preferred browser and then navigate to `http://<ip_address_of_the_device>:<whatever_port_you_provided_in_the_config>`.
## Unstable/Edge/Rolling
### Stable
For the unstable/rolling/edge version, follow the same steps as above (as mentioned for the stable version) with an addition of one command which has to be performed after cloning and changing directory into the repository which makes the cloning step as follows:
For the stable version, follow the same steps as above (as mentioned for the unstable/rolling/edge version) with an addition of one command which has to be performed after cloning and changing directory into the repository which makes the cloning step as follows:
```bash
git clone https://github.com/neon-mmd/websurfx.git
cd websurfx
git checkout rolling
git checkout stable
```
[⬅️ Go back to Home](./README.md)
[⬅️ Go back to Home](./README.md)

12
docs/instances.md Normal file
View File

@ -0,0 +1,12 @@
# Instances
> To contribute your server instance, check out the contributing guide [here](https://github.com/neon-mmd/websurfx/blob/HEAD/CONTRIBUTING.md).
This page provides a list of `Websurfx` instances provided by us and our community.
|URL|Network|Version|Location|Behind Cloudflare?|Maintained By|TLS|IPv6|Comment|
|-|-|-|-|-|-|-|-|-|
|https://alamin655-websurfx.hf.space/|www|v0.21.4|🇺🇸 US||[websurfx project](https://github.com/neon-mmd/websurfx)|✅|||
[⬅️ Go back to Home](./README.md)

View File

@ -8,17 +8,17 @@ By default `websurfx` comes with 9 colorschemes to choose from which can be easi
Creating coloschemes is as easy as it gets it requires the user to have a theme file name with the colorscheme in which every space should be replaced with a `-` (dash) and it should end with a `.css` file extension. After creating the file you need to add the following code with the `colors` you want:
``` css
:root{
--bg: <background color>;
--fg: <foreground color (text color)>;
--1: <color 1>;
--2: <color 2>;
--3: <color 3>;
--4: <color 4>;
--5: <color 5>;
--6: <color 6>;
--7: <color 7>;
```css
:root {
--background-color: <background color>;
--foreground-color: <foreground color (text color on the website) >;
--color-one: <color 1>;
--color-two: <color 2>;
--color-three: <color 3>;
--color-four: <color 4>;
--color-five: <color 5>;
--color-six: <color 6>;
--color-seven: <color 7>;
}
```
@ -27,17 +27,17 @@ Creating coloschemes is as easy as it gets it requires the user to have a theme
**Example of `catppuccin-mocha` colorscheme:**
``` css
```css
:root {
--bg: #1e1e2e;
--fg: #cdd6f4;
--1: #45475a;
--2: #f38ba8;
--3: #a6e3a1;
--4: #f9e2af;
--5: #89b4fa;
--6: #f5c2e7;
--7: #ffffff;
--background-color: #1e1e2e;
--foreground-color: #cdd6f4;
--color-one: #45475a;
--color-two: #f38ba8;
--color-three: #a6e3a1;
--color-four: #f9e2af;
--color-five: #89b4fa;
--color-six: #f5c2e7;
--color-seven: #ffffff;
}
```
@ -54,7 +54,8 @@ To write custom color scheme, it requires the user to have some knowledge of `cs
**Here is an example of `simple theme` (which we provide by default with the app) which will give the user a better idea on how to create a custom theme using it as a template:**
### General
``` css
```css
* {
padding: 0;
margin: 0;
@ -71,11 +72,13 @@ body {
justify-content: space-between;
align-items: center;
height: 100vh;
background: var(--1);
background: var(--color-one);
}
```
### Styles for the index page
``` css
```css
.search-container {
display: flex;
flex-direction: column;
@ -88,8 +91,10 @@ body {
display: flex;
}
```
### Styles for the search box and search button
``` css
```css
.search_bar {
display: flex;
}
@ -101,7 +106,7 @@ body {
outline: none;
border: none;
box-shadow: rgba(0, 0, 0, 1);
background: var(--fg);
background: var(--foreground-color);
}
.search_bar button {
@ -114,8 +119,8 @@ body {
outline: none;
border: none;
gap: 0;
background: var(--bg);
color: var(--3);
background: var(--background-color);
color: var(--color-three);
font-weight: 600;
letter-spacing: 0.1rem;
}
@ -124,11 +129,133 @@ body {
.search_bar button:hover {
filter: brightness(1.2);
}
.search_area .search_options {
display: flex;
justify-content: space-between;
align-items: center;
}
.search_area .search_options select {
margin: 0.7rem 0;
width: 20rem;
background-color: var(--color-one);
color: var(--foreground-color);
padding: 1rem 2rem;
border-radius: 0.5rem;
outline: none;
border: none;
text-transform: capitalize;
}
.search_area .search_options option:hover {
background-color: var(--color-one);
}
.result_not_found {
display: flex;
flex-direction: column;
font-size: 1.5rem;
color: var(--foreground-color);
}
.result_not_found p {
margin: 1rem 0;
}
.result_not_found ul {
margin: 1rem 0;
}
.result_not_found img {
width: 40rem;
}
```
```css
/* styles for the error box */
.error_box .error_box_toggle_button {
background: var(--foreground-color);
}
.error_box .dropdown_error_box {
position: absolute;
display: none;
flex-direction: column;
background: var(--background-color);
border-radius: 0;
margin-left: 2rem;
min-height: 20rem;
min-width: 22rem;
}
.error_box .dropdown_error_box.show {
display: flex;
}
.error_box .dropdown_error_box .error_item,
.error_box .dropdown_error_box .no_errors {
display: flex;
align-items: center;
color: var(--foreground-color);
letter-spacing: 0.1rem;
padding: 1rem;
font-size: 1.2rem;
}
.error_box .dropdown_error_box .error_item {
justify-content: space-between;
}
.error_box .dropdown_error_box .no_errors {
min-height: 18rem;
justify-content: center;
}
.error_box .dropdown_error_box .error_item:hover {
box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.1);
}
.error_box .error_item .severity_color {
width: 1.2rem;
height: 1.2rem;
}
.results .result_disallowed,
.results .result_filtered,
.results .result_engine_not_selected {
display: flex;
justify-content: center;
align-items: center;
gap: 10rem;
font-size: 2rem;
color: var(--foreground-color);
margin: 0rem 7rem;
}
.results .result_disallowed .user_query,
.results .result_filtered .user_query,
.results .result_engine_not_selected .user_query {
color: var(--background-color);
font-weight: 300;
}
.results .result_disallowed img,
.results .result_filtered img,
.results .result_engine_not_selected img {
width: 30rem;
}
.results .result_disallowed div,
.results .result_filtered div,
.results .result_engine_not_selected div {
display: flex;
flex-direction: column;
gap: 1rem;
line-break: strict;
}
```
### Styles for the footer and header
``` css
```css
header {
background: var(--bg);
background: var(--background-color);
width: 100%;
display: flex;
justify-content: right;
@ -151,7 +278,7 @@ footer ul li a,
header ul li a:visited,
footer ul li a:visited {
text-decoration: none;
color: var(--2);
color: var(--color-two);
text-transform: capitalize;
letter-spacing: 0.1rem;
}
@ -162,7 +289,27 @@ header ul li a {
header ul li a:hover,
footer ul li a:hover {
color: var(--5);
color: var(--color-five);
}
footer div span {
font-size: 1.5rem;
color: var(--color-four);
}
footer div {
display: flex;
gap: 1rem;
}
footer {
background: var(--background-color);
width: 100%;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
footer div span {
@ -185,8 +332,10 @@ footer {
align-items: center;
}
```
### Styles for the search page
``` css
```css
.results {
width: 90%;
display: flex;
@ -213,28 +362,28 @@ footer {
.results_aggregated .result h1 a {
font-size: 1.5rem;
color: var(--2);
color: var(--color-two);
text-decoration: none;
letter-spacing: 0.1rem;
}
.results_aggregated .result h1 a:hover {
color: var(--5);
color: var(--color-five);
}
.results_aggregated .result h1 a:visited {
color: var(--bg);
color: var(--background-color);
}
.results_aggregated .result small {
color: var(--3);
color: var(--color-three);
font-size: 1.1rem;
word-wrap: break-word;
line-break: anywhere;
}
.results_aggregated .result p {
color: var(--fg);
color: var(--foreground-color);
font-size: 1.2rem;
margin-top: 0.3rem;
word-wrap: break-word;
@ -245,13 +394,13 @@ footer {
text-align: right;
font-size: 1.2rem;
padding: 1rem;
color: var(--5);
color: var(--color-five);
}
```
### Styles for the 404 page
``` css
```css
.error_container {
display: flex;
justify-content: center;
@ -290,16 +439,18 @@ footer {
.error_content p a,
.error_content p a:visited {
color: var(--2);
color: var(--color-two);
text-decoration: none;
}
.error_content p a:hover {
color: var(--5);
color: var(--color-five);
}
```
### Styles for the previous and next button on the search page
``` css
```css
.page_navigation {
padding: 0 0 2rem 0;
display: flex;
@ -308,8 +459,8 @@ footer {
}
.page_navigation button {
background: var(--bg);
color: var(--fg);
background: var(--background-color);
color: var(--foreground-color);
padding: 1rem;
border-radius: 0.5rem;
outline: none;
@ -326,40 +477,258 @@ footer {
This part is only available right now in the **rolling/edge/unstable** version
```css
.about-container article{
font-size: 1.5rem;
color:var(--fg);
padding-bottom: 10px;
}
.about-container article h1{
color: var(--2);
font-size: 2.8rem;
}
.about-container article div{
padding-bottom: 15px;
}
.about-container a{
color:var(--3);
.about-container article {
font-size: 1.5rem;
color: var(--foreground-color);
padding-bottom: 10px;
}
.about-container article h2{
color: var(--3);
.about-container article h1 {
color: var(--color-two);
font-size: 2.8rem;
}
.about-container article div {
padding-bottom: 15px;
}
.about-container a {
color: var(--color-three);
}
.about-container article h2 {
color: var(--color-three);
font-size: 1.8rem;
padding-bottom: 10px;
}
.about-container p{
color:var(--fg);
font-size: 1.6rem;
.about-container p {
color: var(--foreground-color);
font-size: 1.6rem;
padding-bottom: 10px;
}
.about-container h3{
.about-container h3 {
font-size: 1.5rem;
}
.about-container {
width: 80%;
}
```
[⬅️ Go back to Home](./README.md)
### Styles for the Settings Page
This part is only available right now in the **rolling/edge/unstable** version
```css
.settings_container {
display: flex;
justify-content: space-around;
width: 80dvw;
}
.settings h1 {
color: var(--color-two);
font-size: 2.5rem;
}
.settings hr {
border-color: var(--color-three);
margin: 0.3rem 0 1rem 0;
}
.settings_container .sidebar {
width: 30%;
cursor: pointer;
font-size: 2rem;
display: flex;
flex-direction: column;
margin-right: 0.5rem;
margin-left: -0.7rem;
padding: 0.7rem;
border-radius: 5px;
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--foreground-color);
text-transform: capitalize;
gap: 1.5rem;
}
.settings_container .sidebar .btn {
padding: 0.5rem;
border-radius: 0.5rem;
}
.settings_container .sidebar .btn.active {
background-color: var(--color-two);
}
.settings_container .main_container {
width: 70%;
border-left: 1.5px solid var(--color-three);
padding-left: 3rem;
}
.settings_container .tab {
display: none;
}
.settings_container .tab.active {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.settings_container button {
margin-top: 1rem;
padding: 1rem 2rem;
font-size: 1.5rem;
background: var(--color-three);
color: var(--background-color);
border-radius: 0.5rem;
border: 2px solid transparent;
font-weight: bold;
transition: all 0.1s ease-out;
cursor: pointer;
box-shadow: 5px 5px;
outline: none;
}
.settings_container button:active {
box-shadow: none;
translate: 5px 5px;
}
.settings_container .main_container .message {
font-size: 1.5rem;
color: var(--foreground-color);
}
.settings_container .tab h3 {
font-size: 2rem;
font-weight: bold;
color: var(--color-four);
margin-top: 1.5rem;
text-transform: capitalize;
}
.settings_container .tab .description {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: var(--foreground-color);
}
.settings_container .user_interface select,
.settings_container .general select {
margin: 0.7rem 0;
width: 20rem;
background-color: var(--background-color);
color: var(--foreground-color);
padding: 1rem 2rem;
border-radius: 0.5rem;
outline: none;
border: none;
text-transform: capitalize;
}
.settings_container .user_interface option:hover,
.settings_container .general option:hover {
background-color: var(--color-one);
}
.settings_container .engines .engine_selection {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
padding: 1rem 0;
}
.settings_container .engines .toggle_btn {
color: var(--foreground-color);
font-size: 1.5rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.settings_container .engines hr {
margin: 0;
}
.settings_container .cookies input {
margin: 1rem 0rem;
}
```
### Styles for the Toggle Button
This part is only available right now in the **rolling/edge/unstable** version
```css
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 6rem;
height: 3.4rem;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--background-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: '';
height: 2.6rem;
width: 2.6rem;
left: 0.4rem;
bottom: 0.4rem;
background-color: var(--foreground-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: var(--color-three);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--color-three);
}
input:checked + .slider:before {
-webkit-transform: translateX(2.6rem);
-ms-transform: translateX(2.6rem);
transform: translateX(2.6rem);
}
/* Rounded sliders */
.slider.round {
border-radius: 3.4rem;
}
.slider.round:before {
border-radius: 50%;
}
```
[⬅️ Go back to Home](./README.md)

94
flake.lock Normal file
View File

@ -0,0 +1,94 @@
{
"nodes": {
"naersk": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1694081375,
"narHash": "sha256-vzJXOUnmkMCm3xw8yfPP5m8kypQ3BhAIRe4RRCWpzy8=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3f976d822b7b37fc6fb8e6f157c2dd05e7e94e89",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "master",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"path": "/nix/store/p7iz0r8gs6ppkhj83zjmwyd21k8b7v3y-source",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1695318763,
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"utils": "utils"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

52
flake.nix Normal file
View File

@ -0,0 +1,52 @@
{
# Websurfx NixOS flake
inputs = {
naersk.url = "github:nix-community/naersk/master";
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
utils.url = "github:numtide/flake-utils";
};
outputs = {
naersk,
nixpkgs,
self,
utils,
}:
# We do this for all systems - namely x86_64-linux, aarch64-linux,
# x86_64-darwin and aarch64-darwin
utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {inherit system;};
naersk-lib = pkgs.callPackage naersk {};
in rec {
# Build via "nix build .#default"
packages.default = naersk-lib.buildPackage {
# The build dependencies
buildInputs = with pkgs; [pkg-config openssl];
src = ./.;
};
# Enter devshell with all the tools via "nix develop"
# or "nix-shell"
devShells.default = with pkgs;
mkShell {
buildInputs = [
actionlint
cargo
haskellPackages.hadolint
nodePackages_latest.cspell
nodePackages_latest.eslint
nodePackages_latest.markdownlint-cli2
nodePackages_latest.stylelint
redis
rustPackages.clippy
rustc
yamllint
];
RUST_SRC_PATH = rustPlatform.rustLibSrc;
};
# Build via "nix build .#websurfx", which is basically just
# calls the build function
packages.websurfx = packages.default;
});
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

BIN
public/images/barricade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 892 KiB

BIN
public/images/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

1
public/images/info.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000" data-darkreader-inline-color="" style="--darkreader-inline-color: #e8e6e3;"><path d="M12 11.5v5M12 7.51l.01-.011M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: #000000;"></path></svg>

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg width="24px" height="24px" stroke-width="1.5" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" color="#000000" data-darkreader-inline-color="" style="--darkreader-inline-color: #e8e6e3;"><path d="M20.043 21H3.957c-1.538 0-2.5-1.664-1.734-2.997l8.043-13.988c.77-1.337 2.699-1.337 3.468 0l8.043 13.988C22.543 19.336 21.58 21 20.043 21zM12 9v4" stroke="#000000" stroke-width="1.5" stroke-linecap="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: #000000;"></path><path d="M12 17.01l.01-.011" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" data-darkreader-inline-stroke="" style="--darkreader-inline-stroke: #000000;"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -1,11 +1,11 @@
:root {
--bg: #1e1e2e;
--fg: #cdd6f4;
--1: #45475a;
--2: #f38ba8;
--3: #a6e3a1;
--4: #f9e2af;
--5: #89b4fa;
--6: #f5c2e7;
--7: #ffffff;
--background-color: #1e1e2e;
--foreground-color: #cdd6f4;
--color-one: #45475a;
--color-two: #f38ba8;
--color-three: #a6e3a1;
--color-four: #f9e2af;
--color-five: #89b4fa;
--color-six: #f5c2e7;
--color-seven: #ffffff;
}

View File

@ -0,0 +1,11 @@
:root {
--background-color: #000000;
--foreground-color: #ffffff;
--color-one: #121212;
--color-two: #808080;
--color-three: #999999;
--color-four: #666666;
--color-five: #bfbfbf;
--color-six: #e0e0e0;
--color-seven: #555555;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #44475a;
--fg: #8be9fd;
--1: #ff5555;
--2: #50fa7b;
--3: #ffb86c;
--4: #bd93f9;
--5: #ff79c6;
--6: #94a3a5;
--7: #ffffff;
--background-color: #44475a;
--foreground-color: #8be9fd;
--color-one: #ff5555;
--color-two: #50fa7b;
--color-three: #ffb86c;
--color-four: #bd93f9;
--color-five: #ff79c6;
--color-six: #94a3a5;
--color-seven: #ffffff;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #282828;
--fg: #ebdbb2;
--1: #cc241d;
--2: #98971a;
--3: #d79921;
--4: #458588;
--5: #b16286;
--6: #689d6a;
--7: #ffffff;
--background-color: #1d2021;
--foreground-color: #ebdbb2;
--color-one: #282828;
--color-two: #98971a;
--color-three: #d79921;
--color-four: #458588;
--color-five: #b16286;
--color-six: #689d6a;
--color-seven: #ffffff;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #403e41;
--fg: #fcfcfa;
--1: #ff6188;
--2: #a9dc76;
--3: #ffd866;
--4: #fc9867;
--5: #ab9df2;
--6: #78dce8;
--7: #ffffff;
--background-color: #49483Eff;
--foreground-color: #FFB269;
--color-one: #272822ff;
--color-two: #61AFEF;
--color-three: #ffd866;
--color-four: #fc9867;
--color-five: #ab9df2;
--color-six: #78dce8;
--color-seven: #ffffff;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #2e3440;
--fg: #d8dee9;
--1: #3b4252;
--2: #bf616a;
--3: #a3be8c;
--4: #ebcb8b;
--5: #81a1c1;
--6: #b48ead;
--7: #ffffff;
--background-color: #122736ff;
--foreground-color: #a2e2a9;
--color-one: #121B2Cff;
--color-two: #f08282;
--color-three: #ABC5AAff;
--color-four: #e6d2d2;
--color-five: #81a1c1;
--color-six: #e2ecd6;
--color-seven: #fff;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #1b2b34;
--fg: #d8dee9;
--1: #343d46;
--2: #ec5f67;
--3: #99c794;
--4: #fac863;
--5: #6699cc;
--6: #c594c5;
--7: #ffffff;
--background-color: #1b2b34;
--foreground-color: #d8dee9;
--color-one: #343d46;
--color-two: #5FB3B3ff;
--color-three: #69Cf;
--color-four: #99c794;
--color-five: #69c;
--color-six: #c594c5;
--color-seven: #D8DEE9ff;
}

View File

@ -0,0 +1,11 @@
:root {
--background-color: #282c34;
--foreground-color: #abb2bf;
--color-one: #3b4048;
--color-two: #a3be8c;
--color-three: #b48ead;
--color-four: #c8ccd4;
--color-five: #e06c75;
--color-six: #61afef;
--color-seven: #be5046;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #002b36;
--fg: #839496;
--1: #073642;
--2: #dc322f;
--3: #859900;
--4: #b58900;
--5: #268bd2;
--6: #d33682;
--7: #ffffff;
--background-color: #002b36;
--foreground-color: #c9e0e6;
--color-one: #073642;
--color-two: #2AA198ff;
--color-three: #2AA198ff;
--color-four: #EEE8D5ff;
--color-five: #268bd2;
--color-six: #d33682;
--color-seven: #fff;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #fdf6e3;
--fg: #657b83;
--1: #073642;
--2: #dc322f;
--3: #859900;
--4: #b58900;
--5: #268bd2;
--6: #d33682;
--7: #ffffff;
--background-color: #EEE8D5ff;
--foreground-color: #b1ab97;
--color-one: #fdf6e3;
--color-two: #DC322Fff;
--color-three: #586E75ff;
--color-four: #b58900;
--color-five: #268bd2;
--color-six: #d33682;
--color-seven: #fff;
}

View File

@ -0,0 +1,11 @@
:root {
--background-color: #1a1b26;
--foreground-color: #c0caf5;
--color-one: #32364a;
--color-two: #a9b1d6;
--color-three: #5a5bb8;
--color-four: #6b7089;
--color-five: #e2afff;
--color-six: #a9a1e1;
--color-seven: #988bc7;
}

View File

@ -1,11 +1,11 @@
:root {
--bg: #1d1f21;
--fg: #c5c8c6;
--1: #cc6666;
--2: #b5bd68;
--3: #f0c674;
--4: #81a2be;
--5: #b294bb;
--6: #8abeb7;
--7: #ffffff;
--background-color: #35383Cff;
--foreground-color: #D7DAD8ff;
--color-one: #1d1f21;
--color-two: #D77C79ff;
--color-three: #f0c674;
--color-four: #92B2CAff;
--color-five: #C0A7C7ff;
--color-six: #9AC9C4ff;
--color-seven: #fff;
}

30
public/static/cookies.js Normal file
View File

@ -0,0 +1,30 @@
/**
* This function is executed when any page on the website finishes loading and
* this function retrieves the cookies if it is present on the user's machine.
* If it is available then the saved cookies is display in the cookies tab
* otherwise an appropriate message is displayed if it is not available.
*
* @function
* @listens DOMContentLoaded
* @returns {void}
*/
document.addEventListener(
'DOMContentLoaded',
() => {
try {
// Decode the cookie value
let cookie = decodeURIComponent(document.cookie)
// Set the value of the input field to the decoded cookie value if it is not empty
// Otherwise, display a message indicating that no cookies have been saved on the user's system
document.querySelector('.cookies input').value = cookie.length
? cookie
: 'No cookies have been saved on your system'
} catch (error) {
// If there is an error decoding the cookie, log the error to the console
// and display an error message in the input field
console.error('Error decoding cookie:', error)
document.querySelector('.cookies input').value = 'Error decoding cookie'
}
},
false,
)

View File

@ -0,0 +1,7 @@
/**
* This function provides the ability for the button to toggle the dropdown error-box
* in the search page.
*/
function toggleErrorBox() {
document.querySelector('.dropdown_error_box').classList.toggle('show')
}

View File

@ -1,10 +1,34 @@
let search_box = document.querySelector('input')
function search_web() {
window.location = `search?q=${search_box.value}`
/**
* Selects the input element for the search box
* @type {HTMLInputElement}
*/
const searchBox = document.querySelector('input')
/**
* Redirects the user to the search results page with the query parameter
*/
function searchWeb() {
const query = searchBox.value.trim()
try {
let safeSearchLevel = document.querySelector('.search_options select').value
if (query) {
window.location.href = `search?q=${encodeURIComponent(
query,
)}&safesearch=${encodeURIComponent(safeSearchLevel)}`
}
} catch (error) {
if (query) {
window.location.href = `search?q=${encodeURIComponent(query)}`
}
}
}
search_box.addEventListener('keyup', (e) => {
if (e.keyCode === 13) {
search_web()
}
/**
* Listens for the 'Enter' key press event on the search box and calls the searchWeb function
* @param {KeyboardEvent} e - The keyboard event object
*/
searchBox.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
searchWeb()
}
})

View File

@ -1,26 +1,39 @@
/**
* Navigates to the next page by incrementing the current page number in the URL query string.
* @returns {void}
*/
function navigate_forward() {
const url = new URL(window.location)
const searchParams = url.searchParams
let url = new URL(window.location);
let searchParams = url.searchParams;
let q = searchParams.get('q')
let page = searchParams.get('page')
let q = searchParams.get('q');
let page = parseInt(searchParams.get('page'));
if (page === null) {
page = 2
window.location = `${url.origin}${url.pathname}?q=${q}&page=${page}`
if (isNaN(page)) {
page = 1;
} else {
window.location = `${url.origin}${url.pathname}?q=${q}&page=${++page}`
page++;
}
window.location.href = `${url.origin}${url.pathname}?q=${encodeURIComponent(q)}&page=${page}`;
}
/**
* Navigates to the previous page by decrementing the current page number in the URL query string.
* @returns {void}
*/
function navigate_backward() {
const url = new URL(window.location)
const searchParams = url.searchParams
let url = new URL(window.location);
let searchParams = url.searchParams;
let q = searchParams.get('q')
let page = searchParams.get('page')
let q = searchParams.get('q');
let page = parseInt(searchParams.get('page'));
if (page !== null && page > 1) {
window.location = `${url.origin}${url.pathname}?q=${q}&page=${--page}`
if (isNaN(page)) {
page = 0;
} else if (page > 0) {
page--;
}
window.location.href = `${url.origin}${url.pathname}?q=${encodeURIComponent(q)}&page=${page}`;
}

View File

@ -0,0 +1,18 @@
document.addEventListener(
'DOMContentLoaded',
() => {
let url = new URL(window.location)
let searchParams = url.searchParams
let safeSearchLevel = searchParams.get('safesearch')
if (
safeSearchLevel >= 0 &&
safeSearchLevel <= 2 &&
safeSearchLevel !== null
) {
document.querySelector('.search_options select').value = safeSearchLevel
}
},
false,
)

115
public/static/settings.js Normal file
View File

@ -0,0 +1,115 @@
/**
* This function handles the toggling of selections of all upstream search engines
* options in the settings page under the tab engines.
*/
function toggleAllSelection() {
document
.querySelectorAll('.engine')
.forEach(
(engine_checkbox) =>
(engine_checkbox.checked =
document.querySelector('.select_all').checked),
)
}
/**
* This function adds the functionality to sidebar buttons to only show settings
* related to that tab.
* @param {HTMLElement} current_tab - The current tab that was clicked.
*/
function setActiveTab(current_tab) {
// Remove the active class from all tabs and buttons
document
.querySelectorAll('.tab')
.forEach((tab) => tab.classList.remove('active'))
document
.querySelectorAll('.btn')
.forEach((tab) => tab.classList.remove('active'))
// Add the active class to the current tab and its corresponding settings
current_tab.classList.add('active')
document
.querySelector(`.${current_tab.innerText.toLowerCase().replace(' ', '_')}`)
.classList.add('active')
}
/**
* This function adds the functionality to save all the user selected preferences
* to be saved in a cookie on the users machine.
*/
function setClientSettings() {
// Create an object to store the user's preferences
let cookie_dictionary = new Object()
// Loop through all select tags and add their values to the cookie dictionary
document.querySelectorAll('select').forEach((select_tag) => {
switch (select_tag.name) {
case 'themes':
cookie_dictionary['theme'] = select_tag.value
break
case 'colorschemes':
cookie_dictionary['colorscheme'] = select_tag.value
break
case 'safe_search_levels':
cookie_dictionary['safe_search_level'] = Number(select_tag.value)
break
}
})
// Loop through all engine checkboxes and add their values to the cookie dictionary
let engines = []
document.querySelectorAll('.engine').forEach((engine_checkbox) => {
if (engine_checkbox.checked) {
engines.push(engine_checkbox.parentNode.parentNode.innerText.trim())
}
})
cookie_dictionary['engines'] = engines
// Set the expiration date for the cookie to 1 year from the current date
let expiration_date = new Date()
expiration_date.setFullYear(expiration_date.getFullYear() + 1)
// Save the cookie to the user's machine
document.cookie = `appCookie=${JSON.stringify(
cookie_dictionary,
)}; expires=${expiration_date.toUTCString()}`
// Display a success message to the user
document.querySelector('.message').innerText =
'✅ The settings have been saved sucessfully!!'
// Clear the success message after 10 seconds
setTimeout(() => {
document.querySelector('.message').innerText = ''
}, 10000)
}
/**
* This functions gets the saved cookies if it is present on the user's machine If it
* is available then it is parsed and converted to an object which is then used to
* retrieve the preferences that the user had selected previously and is then loaded in the
* website otherwise the function does nothing and the default server side settings are loaded.
*/
function getClientSettings() {
// Get the appCookie from the user's machine
let cookie = decodeURIComponent(document.cookie)
// If the cookie is not empty, parse it and use it to set the user's preferences
if (cookie.length) {
let cookie_value = cookie
.split(';')
.map((item) => item.split('='))
.reduce((acc, [_, v]) => (acc = JSON.parse(v)) && acc, {})
// Loop through all link tags and update their href values to match the user's preferences
Array.from(document.querySelectorAll('link')).forEach((item) => {
if (item.href.includes('static/themes')) {
item.href = `static/themes/${cookie_value['theme']}.css`
} else if (item.href.includes('static/colorschemes')) {
item.href = `static/colorschemes/${cookie_value['colorscheme']}.css`
}
})
}
}

View File

@ -16,7 +16,7 @@ body {
justify-content: space-between;
align-items: center;
height: 100vh;
background: var(--1);
background: var(--color-one);
}
/* styles for the index page */
@ -46,7 +46,7 @@ body {
outline: none;
border: none;
box-shadow: rgba(0, 0, 0, 1);
background: var(--fg);
background: var(--foreground-color);
}
.search_bar button {
@ -59,8 +59,8 @@ body {
outline: none;
border: none;
gap: 0;
background: var(--bg);
color: var(--3);
background: var(--background-color);
color: var(--color-three);
font-weight: 600;
letter-spacing: 0.1rem;
}
@ -70,15 +70,145 @@ body {
filter: brightness(1.2);
}
.search_area .search_options {
display: flex;
justify-content: space-between;
align-items: center;
}
.search_area .search_options select {
margin: 0.7rem 0;
width: 20rem;
background-color: var(--color-one);
color: var(--foreground-color);
padding: 1rem 2rem;
border-radius: 0.5rem;
outline: none;
border: none;
text-transform: capitalize;
}
.search_area .search_options option:hover {
background-color: var(--color-one);
}
.result_not_found {
display: flex;
flex-direction: column;
font-size: 1.5rem;
color: var(--foreground-color);
}
.result_not_found p {
margin: 1rem 0;
}
.result_not_found ul {
margin: 1rem 0;
}
.result_not_found img {
width: 40rem;
}
/* styles for the error box */
.error_box .error_box_toggle_button {
background: var(--foreground-color);
}
.error_box .dropdown_error_box {
position: absolute;
display: none;
flex-direction: column;
background: var(--background-color);
border-radius: 0;
margin-left: 2rem;
min-height: 20rem;
min-width: 22rem;
}
.error_box .dropdown_error_box.show {
display: flex;
}
.error_box .dropdown_error_box .error_item,
.error_box .dropdown_error_box .no_errors {
display: flex;
align-items: center;
color: var(--foreground-color);
letter-spacing: 0.1rem;
padding: 1rem;
font-size: 1.2rem;
}
.error_box .dropdown_error_box .error_item {
justify-content: space-between;
}
.error_box .dropdown_error_box .no_errors {
min-height: 18rem;
justify-content: center;
}
.error_box .dropdown_error_box .error_item:hover {
box-shadow: inset 0 0 100px 100px rgba(255, 255, 255, 0.1);
}
.error_box .error_item .severity_color {
width: 1.2rem;
height: 1.2rem;
}
.results .result_disallowed,
.results .result_filtered,
.results .result_engine_not_selected {
display: flex;
justify-content: center;
align-items: center;
gap: 10rem;
font-size: 2rem;
color: var(--foreground-color);
margin: 0rem 7rem;
}
.results .result_disallowed .user_query,
.results .result_filtered .user_query,
.results .result_engine_not_selected .user_query {
color: var(--background-color);
font-weight: 300;
}
.results .result_disallowed img,
.results .result_filtered img,
.results .result_engine_not_selected img {
width: 30rem;
}
.results .result_disallowed div,
.results .result_filtered div,
.results .result_engine_not_selected div {
display: flex;
flex-direction: column;
gap: 1rem;
line-break: strict;
}
/* styles for the footer and header */
header {
background: var(--bg);
header,
footer {
width: 100%;
background: var(--background-color);
display: flex;
justify-content: right;
align-items: center;
padding: 1rem;
align-items: center;
}
header {
justify-content: space-between;
}
header h1 a {
text-transform: capitalize;
text-decoration: none;
color: var(--foreground-color);
letter-spacing: 0.1rem;
margin-left: 1rem;
}
header ul,
@ -96,7 +226,7 @@ footer ul li a,
header ul li a:visited,
footer ul li a:visited {
text-decoration: none;
color: var(--2);
color: var(--color-two);
text-transform: capitalize;
letter-spacing: 0.1rem;
}
@ -107,12 +237,12 @@ header ul li a {
header ul li a:hover,
footer ul li a:hover {
color: var(--5);
color: var(--color-five);
}
footer div span {
font-size: 1.5rem;
color: var(--4);
color: var(--color-four);
}
footer div {
@ -121,13 +251,8 @@ footer div {
}
footer {
background: var(--bg);
width: 100%;
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* Styles for the search page */
@ -158,28 +283,28 @@ footer {
.results_aggregated .result h1 a {
font-size: 1.5rem;
color: var(--2);
color: var(--color-two);
text-decoration: none;
letter-spacing: 0.1rem;
}
.results_aggregated .result h1 a:hover {
color: var(--5);
color: var(--color-five);
}
.results_aggregated .result h1 a:visited {
color: var(--bg);
color: var(--background-color);
}
.results_aggregated .result small {
color: var(--3);
color: var(--color-three);
font-size: 1.1rem;
word-wrap: break-word;
line-break: anywhere;
}
.results_aggregated .result p {
color: var(--fg);
color: var(--foreground-color);
font-size: 1.2rem;
margin-top: 0.3rem;
word-wrap: break-word;
@ -190,7 +315,7 @@ footer {
text-align: right;
font-size: 1.2rem;
padding: 1rem;
color: var(--5);
color: var(--color-five);
}
/* Styles for the 404 page */
@ -233,12 +358,12 @@ footer {
.error_content p a,
.error_content p a:visited {
color: var(--2);
color: var(--color-two);
text-decoration: none;
}
.error_content p a:hover {
color: var(--5);
color: var(--color-five);
}
.page_navigation {
@ -249,8 +374,8 @@ footer {
}
.page_navigation button {
background: var(--bg);
color: var(--fg);
background: var(--background-color);
color: var(--foreground-color);
padding: 1rem;
border-radius: 0.5rem;
outline: none;
@ -260,3 +385,248 @@ footer {
.page_navigation button:active {
filter: brightness(1.2);
}
/* Styles for the about page */
.about-container article {
font-size: 1.5rem;
color: var(--foreground-color);
padding-bottom: 10px;
}
.about-container article h1 {
color: var(--color-two);
font-size: 2.8rem;
}
.about-container article div {
padding-bottom: 15px;
}
.about-container a {
color: var(--color-three);
}
.about-container article h2 {
color: var(--color-three);
font-size: 1.8rem;
padding-bottom: 10px;
}
.about-container p {
color: var(--foreground-color);
font-size: 1.6rem;
padding-bottom: 10px;
}
.about-container h3 {
font-size: 1.5rem;
}
.about-container {
width: 80%;
}
/* Styles for the settings page */
.settings_container {
display: flex;
justify-content: space-around;
width: 80dvw;
}
.settings h1 {
color: var(--color-two);
font-size: 2.5rem;
}
.settings hr {
border-color: var(--color-three);
margin: 0.3rem 0 1rem 0;
}
.settings_container .sidebar {
width: 30%;
cursor: pointer;
font-size: 2rem;
display: flex;
flex-direction: column;
margin-right: 0.5rem;
margin-left: -0.7rem;
padding: 0.7rem;
border-radius: 5px;
font-weight: bold;
margin-bottom: 0.5rem;
color: var(--foreground-color);
text-transform: capitalize;
gap: 1.5rem;
}
.settings_container .sidebar .btn {
padding: 0.5rem;
border-radius: 0.5rem;
}
.settings_container .sidebar .btn.active {
background-color: var(--color-two);
}
.settings_container .main_container {
width: 70%;
border-left: 1.5px solid var(--color-three);
padding-left: 3rem;
}
.settings_container .tab {
display: none;
}
.settings_container .tab.active {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.settings_container button {
margin-top: 1rem;
padding: 1rem 2rem;
font-size: 1.5rem;
background: var(--color-three);
color: var(--background-color);
border-radius: 0.5rem;
border: 2px solid transparent;
font-weight: bold;
transition: all 0.1s ease-out;
cursor: pointer;
box-shadow: 5px 5px;
outline: none;
}
.settings_container button:active {
box-shadow: none;
translate: 5px 5px;
}
.settings_container .main_container .message {
font-size: 1.5rem;
color: var(--foreground-color);
}
.settings_container .tab h3 {
font-size: 2rem;
font-weight: bold;
color: var(--color-four);
margin-top: 1.5rem;
text-transform: capitalize;
}
.settings_container .tab .description {
font-size: 1.5rem;
margin-bottom: 0.5rem;
color: var(--foreground-color);
}
.settings_container .user_interface select,
.settings_container .general select {
margin: 0.7rem 0;
width: 20rem;
background-color: var(--background-color);
color: var(--foreground-color);
padding: 1rem 2rem;
border-radius: 0.5rem;
outline: none;
border: none;
text-transform: capitalize;
}
.settings_container .user_interface option:hover,
.settings_container .general option:hover {
background-color: var(--color-one);
}
.settings_container .engines .engine_selection {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
padding: 1rem 0;
}
.settings_container .engines .toggle_btn {
color: var(--foreground-color);
font-size: 1.5rem;
display: flex;
gap: 0.5rem;
align-items: center;
}
.settings_container .engines hr {
margin: 0;
}
.settings_container .cookies input {
margin: 1rem 0rem;
}
/* Styles for the toggle button */
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 6rem;
height: 3.4rem;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--background-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
.slider:before {
position: absolute;
content: '';
height: 2.6rem;
width: 2.6rem;
left: 0.4rem;
bottom: 0.4rem;
background-color: var(--foreground-color);
-webkit-transition: 0.4s;
transition: 0.4s;
}
input:checked + .slider {
background-color: var(--color-three);
}
input:focus + .slider {
box-shadow: 0 0 1px var(--color-three);
}
input:checked + .slider:before {
-webkit-transform: translateX(2.6rem);
-ms-transform: translateX(2.6rem);
transform: translateX(2.6rem);
}
/* Rounded sliders */
.slider.round {
border-radius: 3.4rem;
}
.slider.round:before {
border-radius: 50%;
}

View File

@ -1,20 +1,29 @@
{{>header this}}
<main class="about-container">
<h1>Websurfx</h1>
<small
>a lightening fast, privacy respecting, secure meta search engine</small
>
<article>
Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim
labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet.
Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum
Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident.
Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex
occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat
officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in
Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non
excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut
ea consectetur et est culpa et culpa duis.
<article >
<div>
<h1 >Websurfx</h1>
<hr size="4" width="100%" color="#a6e3a1">
</div>
<p>A modern-looking, lightning-fast, privacy-respecting, secure meta search engine written in Rust. It provides a fast and secure search experience while respecting user privacy.<br> It aggregates results from multiple search engines and presents them in an unbiased manner, filtering out trackers and ads.
</p>
<h2>Some of the Top Features:</h2>
<ul><strong>Lightning fast </strong>- Results load within milliseconds for an instant search experience.</ul>
<ul><strong>Secure search</strong> - All searches are performed over an encrypted connection to prevent snooping.</ul>
<ul><strong>Ad free results</strong> - All search results are ad free and clutter free for a clean search experience.</ul>
<ul><strong>Privacy focused</strong> - Websurface does not track, store or sell your search data. Your privacy is our priority.</ul>
<ul><strong>Free and Open source</strong> - The entire project's code is open source and available for free on <a href="https://github.com/neon-mmd/websurfx">GitHub</a> under an GNU Affero General Public License.</ul>
<ul><strong>Highly customizable</strong> - Websurface comes with 9 built-in color themes and supports creating custom themes effortlessly.</ul>
</article>
<h3>Devoloped by: <a href="https://github.com/neon-mmd/websurfx">Websurfx team</a></h3>
</main>
{{>footer}}

View File

@ -0,0 +1,3 @@
<div class="search_bar">
<input type="search" name="search-box" value="{{this.pageQuery}}" placeholder="Type to search" />
<button type="submit" onclick="searchWeb()">search</button>

View File

@ -0,0 +1,12 @@
<div class="cookies tab">
<h1>Cookies</h1>
<p class="description">
This is the cookies are saved on your system and it contains the preferences
you chose in the settings page
</p>
<input type="text" name="cookie_field" value="" readonly />
<p class="description">
The cookies stored are not used by us for any malicious intend or for
tracking you in any way.
</p>
</div>

View File

@ -0,0 +1,32 @@
<div class="engines tab">
<h1>Engines</h1>
<h3>select search engines</h3>
<p class="description">
Select the search engines from the list of engines that you want results
from
</p>
<div class="engine_selection">
<div class="toggle_btn">
<label class="switch">
<input type="checkbox" class="select_all" onchange="toggleAllSelection()" />
<span class="slider round"></span>
</label>
Select All
</div>
<hr />
<div class="toggle_btn">
<label class="switch">
<input type="checkbox" class="engine" />
<span class="slider round"></span>
</label>
DuckDuckGo
</div>
<div class="toggle_btn">
<label class="switch">
<input type="checkbox" class="engine" />
<span class="slider round"></span>
</label>
Searx
</div>
</div>
</div>

View File

@ -5,11 +5,12 @@
</div>
<div>
<ul>
<li><a href="#">Source Code</a></li>
<li><a href="#">Issues/Bugs</a></li>
<li><a href="https://github.com/neon-mmd/websurfx">Source Code</a></li>
<li><a href="https://github.com/neon-mmd/websurfx/issues">Issues/Bugs</a></li>
</ul>
</div>
</footer>
<script src="static/settings.js"></script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<div class="general tab active">
<h1>General</h1>
<h3>Select a safe search level</h3>
<p class="description">
Select a safe search level from the menu below to filter content based on
the level.
</p>
<select name="safe_search_levels">
<option value=0>None</option>
<option value=1>Low</option>
<option value=2>Moderate</option>
</select>
</div>

View File

@ -1,12 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<head>
<title>Websurfx</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="static/colorschemes/{{colorscheme}}.css" rel="stylesheet" type="text/css" />
<link href="static/themes/{{theme}}.css" rel="stylesheet" type="text/css" />
</head>
</head>
<body>
<header>{{>navbar}}</header>
<body onload="getClientSettings()">
<header>
<h1><a href="/">Websurfx</a></h1>
{{>navbar}}
</header>

View File

@ -1,7 +1,8 @@
{{>header this}}
<main class="search-container">
<img src="images/fps_logo.png" alt="Websurfx meta-search engine logo" />
{{>search_bar}}
<img src="../images/websurfx_logo.png" alt="Websurfx meta-search engine logo" />
{{>bar}}
</div>
</main>
<script src="static/index.js"></script>
{{>footer}}

View File

@ -1,6 +1,6 @@
<nav>
<ul>
<li><a href="about">about</a></li>
<li><a href="settings">settings</a></li>
</ul>
<ul>
<li><a href="about">about</a></li>
<li><a href="settings">settings</a></li>
</ul>
</nav>

View File

@ -1,25 +1,86 @@
{{>header this.style}}
<main class="results">
{{>search_bar}}
<div class="results_aggregated">
{{#each results}}
<div class="result">
<h1><a href="{{this.visitingUrl}}">{{{this.title}}}</a></h1>
<small>{{this.url}}</small>
<p>{{{this.description}}}</p>
<div class="upstream_engines">
{{#each engine}}
<span>{{this}}</span>
{{/each}}
</div>
{{>search_bar this}}
<div class="results_aggregated">
{{#if results}} {{#each results}}
<div class="result">
<h1><a href="{{{this.url}}}">{{{this.title}}}</a></h1>
<small>{{{this.url}}}</small>
<p>{{{this.description}}}</p>
<div class="upstream_engines">
{{#each engine}}
<span>{{{this}}}</span>
{{/each}}
</div>
</div>
{{/each}} {{else}} {{#if disallowed}}
<div class="result_disallowed">
<div class="description">
<p>
Your search - <span class="user_query">{{{this.pageQuery}}}</span> -
has been disallowed.
</p>
<p class="description_paragraph">Dear user,</p>
<p class="description_paragraph">
The query - <span class="user_query">{{{this.pageQuery}}}</span> - has
been blacklisted via server configuration and hence disallowed by the
server. Henceforth no results could be displayed for your query.
</p>
</div>
<img src="./images/barricade.png" alt="Image of a Barricade" />
</div>
{{else}} {{#if filtered}}
<div class="result_filtered">
<div class="description">
<p>
Your search - <span class="user_query">{{{this.pageQuery}}}</span> -
has been filtered.
</p>
<p class="description_paragraph">Dear user,</p>
<p class="description_paragraph">
All the search results contain results that has been configured to be
filtered out via server configuration and henceforth has been
completely filtered out.
</p>
</div>
<img src="./images/filter.png" alt="Image of a paper inside a funnel" />
</div>
{{else}} {{#if noEnginesSelected}}
<div class="result_engine_not_selected">
<div class="description">
<p>
No results could be fetched for your search "<span class="user_query">{{{this.pageQuery}}}</span>" .
</p>
<p class="description_paragraph">Dear user,</p>
<p class="description_paragraph">
No results could be retrieved from the upstream search engines as no
upstream search engines were selected from the settings page.
</p>
</div>
<img src="./images/no_selection.png" alt="Image of a white cross inside a red circle" />
</div>
{{else}}
<div class="result_not_found">
<p>Your search - {{{this.pageQuery}}} - did not match any documents.</p>
<p class="suggestions">Suggestions:</p>
<ul>
<li>Make sure that all words are spelled correctly.</li>
<li>Try different keywords.</li>
<li>Try more general keywords.</li>
</ul>
<img src="./images/no_results.gif" alt="Man fishing gif" />
</div>
{{/if}} {{/if}} {{/if}} {{/if}}
</div>
<div class="page_navigation">
<button type="button" onclick="navigate_backward()">
&#8592; previous
</button>
<button type="button" onclick="navigate_forward()">next &#8594;</button>
</div>
{{/each}}
</div>
<div class="page_navigation">
<button type="button" onclick="navigate_backward()">&#8592; previous</button>
<button type="button" onclick="navigate_forward()">next &#8594;</button>
</div>
</main>
<script src="static/index.js"></script>
<script src="static/search_area_options.js"></script>
<script src="static/pagination.js"></script>
<script src="static/error_box.js"></script>
{{>footer}}

View File

@ -1,9 +1,36 @@
<div class="search_bar">
<input
type="search"
name="search-box"
value="{{this.pageQuery}}"
placeholder="Type to search"
/>
<button type="submit" onclick="search_web()">search</button>
<div class="search_area">
{{>bar this}}
<div class="error_box">
{{#if engineErrorsInfo}}
<button onclick="toggleErrorBox()" class="error_box_toggle_button">
<img src="./images/warning.svg" alt="Info icon for error box" />
</button>
<div class="dropdown_error_box">
{{#each engineErrorsInfo}}
<div class="error_item">
<span class="engine_name">{{{this.engine}}}</span>
<span class="engine_name">{{{this.error}}}</span>
<span class="severity_color" style="background: {{{this.severity_color}}};"></span>
</div>
{{/each}}
</div>
{{else}}
<button onclick="toggleErrorBox()" class="error_box_toggle_button">
<img src="./images/info.svg" alt="Warning icon for error box" />
</button>
<div class="dropdown_error_box">
<div class="no_errors">
Everything looks good 🙂!!
</div>
</div>
{{/if}}
</div>
</div>
<div class="search_options">
<select name="safe_search_levels" {{#if (gte safeSearchLevel 3)}} disabled {{/if}}>
<option value=0 {{#if (eq safeSearchLevel 0)}} selected {{/if}}>SafeSearch: None</option>
<option value=1 {{#if (eq safeSearchLevel 1)}} selected {{/if}}>SafeSearch: Low</option>
<option value=2 {{#if (eq safeSearchLevel 2)}} selected {{/if}}>SafeSearch: Moderate</option>
</select>
</div>
</div>

View File

@ -1,5 +1,22 @@
{{>header this}}
<main class="settings">
<h1>Page is under construction</h1>
<main class="settings" >
<h1>Settings</h1>
<hr />
<div class="settings_container">
<div class="sidebar">
<div class="btn active" onclick="setActiveTab(this)">general</div>
<div class="btn" onclick="setActiveTab(this)">user interface</div>
<div class="btn" onclick="setActiveTab(this)">engines</div>
<div class="btn" onclick="setActiveTab(this)">cookies</div>
</div>
<div class="main_container">
{{> general_tab}} {{> user_interface_tab}} {{> engines_tab}} {{>
cookies_tab}}
<p class="message"></p>
<button type="submit" onclick="setClientSettings()">Save</button>
</div>
</div>
</main>
<script src="static/settings.js"></script>
<script src="static/cookies.js"></script>
{{>footer}}

View File

@ -0,0 +1,28 @@
<div class="user_interface tab">
<h1>User Interface</h1>
<h3>select theme</h3>
<p class="description">
Select the theme from the available themes to be used in user interface
</p>
<select name="themes">
<option value="simple">simple</option>
</select>
<h3>select color scheme</h3>
<p class="description">
Select the color scheme for your theme to be used in user interface
</p>
<select name="colorschemes">
<option value="catppuccin-mocha">catppuccin mocha</option>
<option value="dark-chocolate">dark chocolate</option>
<option value="dracula">dracula</option>
<option value="gruvbox-dark">gruvbox dark</option>
<option value="monokai">monokai</option>
<option value="nord">nord</option>
<option value="oceanic-next">oceanic next</option>
<option value="one-dark">one dark</option>
<option value="solarized-dark">solarized dark</option>
<option value="solarized-light">solarized light</option>
<option value="tokyo-night">tokyo night</option>
<option value="tomorrow-night">tomorrow night</option>
</select>
</div>

View File

@ -3,10 +3,18 @@
//! This module contains the main function which handles the logging of the application to the
//! stdout and handles the command line arguments provided and launches the `websurfx` server.
use mimalloc::MiMalloc;
use std::net::TcpListener;
use websurfx::{cache::cacher::Cache, config::parser::Config, run};
use env_logger::Env;
use websurfx::{config_parser::parser::Config, run};
/// A dhat heap memory profiler
#[cfg(feature = "dhat-heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
#[cfg(not(feature = "dhat-heap"))]
#[global_allocator]
static GLOBAL: MiMalloc = MiMalloc;
/// The function that launches the main server and registers all the routes of the website.
///
@ -16,15 +24,27 @@ use websurfx::{config_parser::parser::Config, run};
/// available for being used for other applications.
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// A dhat heap profiler initialization.
#[cfg(feature = "dhat-heap")]
let _profiler = dhat::Profiler::new_heap();
// Initialize the parsed config file.
let config = Config::parse().unwrap();
let config = Config::parse(false).unwrap();
// Initializing logging middleware with level set to default or info.
env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
let cache = Cache::build(&config).await;
log::info!("started server on port {}", config.port);
log::info!(
"started server on port {} and IP {}",
config.port,
config.binding_ip
);
log::info!(
"Open http://{}:{}/ in your browser",
config.binding_ip,
config.port,
);
let listener = TcpListener::bind((config.binding_ip_addr.clone(), config.port))?;
let listener = TcpListener::bind((config.binding_ip.clone(), config.port))?;
run(listener, config)?.await
run(listener, config, cache)?.await
}

283
src/cache/cacher.rs vendored
View File

@ -1,78 +1,267 @@
//! This module provides the functionality to cache the aggregated results fetched and aggregated
//! from the upstream search engines in a json format.
use md5::compute;
use redis::{Client, Commands, Connection};
use error_stack::Report;
#[cfg(feature = "memory-cache")]
use mini_moka::sync::Cache as MokaCache;
#[cfg(feature = "memory-cache")]
use std::time::Duration;
use tokio::sync::Mutex;
/// A named struct which stores the redis Connection url address to which the client will
/// connect to.
///
/// # Fields
///
/// * `redis_connection_url` - It stores the redis Connection url address.
use crate::{config::parser::Config, models::aggregation_models::SearchResults};
use super::error::CacheError;
#[cfg(feature = "redis-cache")]
use super::redis_cacher::RedisCache;
/// Different implementations for caching, currently it is possible to cache in-memory or in Redis.
#[derive(Clone)]
pub struct RedisCache {
redis_connection_url: String,
pub enum Cache {
/// Caching is disabled
Disabled,
#[cfg(all(feature = "redis-cache", not(feature = "memory-cache")))]
/// Encapsulates the Redis based cache
Redis(RedisCache),
#[cfg(all(feature = "memory-cache", not(feature = "redis-cache")))]
/// Contains the in-memory cache.
InMemory(MokaCache<String, SearchResults>),
#[cfg(all(feature = "redis-cache", feature = "memory-cache"))]
/// Contains both the in-memory cache and Redis based cache
Hybrid(RedisCache, MokaCache<String, SearchResults>),
}
impl RedisCache {
/// Constructs a new `SearchResult` with the given arguments needed for the struct.
impl Cache {
/// A function that builds the cache from the given configuration.
///
/// # Arguments
///
/// * `redis_connection_url` - It stores the redis Connection url address.
pub fn new(redis_connection_url: String) -> Self {
RedisCache {
redis_connection_url,
/// * `config` - It takes the config struct as an argument.
///
/// # Returns
///
/// It returns a newly initialized variant based on the feature enabled by the user.
pub async fn build(_config: &Config) -> Self {
#[cfg(all(feature = "redis-cache", feature = "memory-cache"))]
{
log::info!("Using a hybrid cache");
Cache::new_hybrid(
RedisCache::new(&_config.redis_url, 5)
.await
.expect("Redis cache configured"),
)
}
#[cfg(all(feature = "redis-cache", not(feature = "memory-cache")))]
{
log::info!("Listening redis server on {}", &_config.redis_url);
Cache::new(
RedisCache::new(&_config.redis_url, 5)
.await
.expect("Redis cache configured"),
)
}
#[cfg(all(feature = "memory-cache", not(feature = "redis-cache")))]
{
log::info!("Using an in-memory cache");
Cache::new_in_memory()
}
#[cfg(not(any(feature = "memory-cache", feature = "redis-cache")))]
{
log::info!("Caching is disabled");
Cache::Disabled
}
}
/// A helper function which computes the hash of the url and formats and returns it as string.
/// A function that initializes a new connection pool struct.
///
/// # Arguments
///
/// * `url` - It takes an url as string.
fn compute_url_hash(self, url: &str) -> String {
format!("{:?}", compute(url))
/// * `redis_cache` - It takes the newly initialized connection pool struct as an argument.
///
/// # Returns
///
/// It returns a `Redis` variant with the newly initialized connection pool struct.
#[cfg(all(feature = "redis-cache", not(feature = "memory-cache")))]
pub fn new(redis_cache: RedisCache) -> Self {
Cache::Redis(redis_cache)
}
/// A function which fetches the cached json results as json string from the redis server.
/// A function that initializes the `in memory` cache which is used to cache the results in
/// memory with the search engine thus improving performance by making retrieval and caching of
/// results faster.
///
/// # Returns
///
/// It returns a `InMemory` variant with the newly initialized in memory cache type.
#[cfg(all(feature = "memory-cache", not(feature = "redis-cache")))]
pub fn new_in_memory() -> Self {
let cache = MokaCache::builder()
.max_capacity(1000)
.time_to_live(Duration::from_secs(60))
.build();
Cache::InMemory(cache)
}
/// A function that initializes both in memory cache and redis client connection for being used
/// for managing hybrid cache which increases resiliancy of the search engine by allowing the
/// cache to switch to `in memory` caching if the `redis` cache server is temporarily
/// unavailable.
///
/// # Arguments
///
/// * `redis_cache` - It takes `redis` client connection struct as an argument.
///
/// # Returns
///
/// It returns a tuple variant `Hybrid` storing both the in-memory cache type and the `redis`
/// client connection struct.
#[cfg(all(feature = "redis-cache", feature = "memory-cache"))]
pub fn new_hybrid(redis_cache: RedisCache) -> Self {
let cache = MokaCache::builder()
.max_capacity(1000)
.time_to_live(Duration::from_secs(60))
.build();
Cache::Hybrid(redis_cache, cache)
}
/// A function which fetches the cached json results as json string.
///
/// # Arguments
///
/// * `url` - It takes an url as a string.
pub fn cached_results_json(self, url: String) -> Result<String, Box<dyn std::error::Error>> {
let hashed_url_string = self.clone().compute_url_hash(&url);
let mut redis_connection: Connection =
Client::open(self.redis_connection_url)?.get_connection()?;
Ok(redis_connection.get(hashed_url_string)?)
///
/// # Error
///
/// Returns the `SearchResults` from the cache if the program executes normally otherwise
/// returns a `CacheError` if the results cannot be retrieved from the cache.
pub async fn cached_json(&mut self, _url: &str) -> Result<SearchResults, Report<CacheError>> {
match self {
Cache::Disabled => Err(Report::new(CacheError::MissingValue)),
#[cfg(all(feature = "redis-cache", not(feature = "memory-cache")))]
Cache::Redis(redis_cache) => {
let json = redis_cache.cached_json(_url).await?;
Ok(serde_json::from_str::<SearchResults>(&json)
.map_err(|_| CacheError::SerializationError)?)
}
#[cfg(all(feature = "memory-cache", not(feature = "redis-cache")))]
Cache::InMemory(in_memory) => match in_memory.get(&_url.to_string()) {
Some(res) => Ok(res),
None => Err(Report::new(CacheError::MissingValue)),
},
#[cfg(all(feature = "redis-cache", feature = "memory-cache"))]
Cache::Hybrid(redis_cache, in_memory) => match redis_cache.cached_json(_url).await {
Ok(res) => Ok(serde_json::from_str::<SearchResults>(&res)
.map_err(|_| CacheError::SerializationError)?),
Err(_) => match in_memory.get(&_url.to_string()) {
Some(res) => Ok(res),
None => Err(Report::new(CacheError::MissingValue)),
},
},
}
}
/// A function which caches the results by using the hashed `url` as the key and
/// `json results` as the value and stores it in redis server with ttl(time to live)
/// set to 60 seconds.
/// A function which caches the results by using the `url` as the key and
/// `json results` as the value and stores it in the cache
///
/// # Arguments
///
/// * `json_results` - It takes the json results string as an argument.
/// * `url` - It takes the url as a String.
pub fn cache_results(
self,
json_results: String,
url: String,
) -> Result<(), Box<dyn std::error::Error>> {
let hashed_url_string = self.clone().compute_url_hash(&url);
let mut redis_connection: Connection =
Client::open(self.redis_connection_url)?.get_connection()?;
// put results_json into cache
redis_connection.set(hashed_url_string.clone(), json_results)?;
// Set the TTL for the key to 60 seconds
redis_connection
.expire::<String, u32>(hashed_url_string.clone(), 60)
.unwrap();
Ok(())
///
/// # Error
///
/// Returns a unit type if the program caches the given search results without a failure
/// otherwise it returns a `CacheError` if the search results cannot be cached due to a
/// failure.
pub async fn cache_results(
&mut self,
_search_results: &SearchResults,
_url: &str,
) -> Result<(), Report<CacheError>> {
match self {
Cache::Disabled => Ok(()),
#[cfg(all(feature = "redis-cache", not(feature = "memory-cache")))]
Cache::Redis(redis_cache) => {
let json = serde_json::to_string(_search_results)
.map_err(|_| CacheError::SerializationError)?;
redis_cache.cache_results(&json, _url).await
}
#[cfg(all(feature = "memory-cache", not(feature = "redis-cache")))]
Cache::InMemory(cache) => {
cache.insert(_url.to_string(), _search_results.clone());
Ok(())
}
#[cfg(all(feature = "memory-cache", feature = "redis-cache"))]
Cache::Hybrid(redis_cache, cache) => {
let json = serde_json::to_string(_search_results)
.map_err(|_| CacheError::SerializationError)?;
match redis_cache.cache_results(&json, _url).await {
Ok(_) => Ok(()),
Err(_) => {
cache.insert(_url.to_string(), _search_results.clone());
Ok(())
}
}
}
}
}
}
/// A structure to efficiently share the cache between threads - as it is protected by a Mutex.
pub struct SharedCache {
/// The internal cache protected from concurrent access by a mutex
cache: Mutex<Cache>,
}
impl SharedCache {
/// A function that creates a new `SharedCache` from a Cache implementation.
///
/// # Arguments
///
/// * `cache` - It takes the `Cache` enum variant as an argument with the prefered cache type.
///
/// Returns a newly constructed `SharedCache` struct.
pub fn new(cache: Cache) -> Self {
Self {
cache: Mutex::new(cache),
}
}
/// A getter function which retrieves the cached SearchResulsts from the internal cache.
///
/// # Arguments
///
/// * `url` - It takes the search url as an argument which will be used as the key to fetch the
/// cached results from the cache.
///
/// # Error
///
/// Returns a `SearchResults` struct containing the search results from the cache if nothing
/// goes wrong otherwise returns a `CacheError`.
pub async fn cached_json(&self, url: &str) -> Result<SearchResults, Report<CacheError>> {
let mut mut_cache = self.cache.lock().await;
mut_cache.cached_json(url).await
}
/// A setter function which caches the results by using the `url` as the key and
/// `SearchResults` as the value.
///
/// # Arguments
///
/// * `search_results` - It takes the `SearchResults` as an argument which are results that
/// needs to be cached.
/// * `url` - It takes the search url as an argument which will be used as the key for storing
/// results in the cache.
///
/// # Error
///
/// Returns an unit type if the results are cached succesfully otherwise returns a `CacheError`
/// on a failure.
pub async fn cache_results(
&self,
search_results: &SearchResults,
url: &str,
) -> Result<(), Report<CacheError>> {
let mut mut_cache = self.cache.lock().await;
mut_cache.cache_results(search_results, url).await
}
}

50
src/cache/error.rs vendored Normal file
View File

@ -0,0 +1,50 @@
//! This module provides the error enum to handle different errors associated while requesting data from
//! the redis server using an async connection pool.
use std::fmt;
#[cfg(feature = "redis-cache")]
use redis::RedisError;
/// A custom error type used for handling redis async pool associated errors.
#[derive(Debug)]
pub enum CacheError {
/// This variant handles all errors related to `RedisError`,
#[cfg(feature = "redis-cache")]
RedisError(RedisError),
/// This variant handles the errors which occurs when all the connections
/// in the connection pool return a connection dropped redis error.
PoolExhaustionWithConnectionDropError,
/// Whenever serialization or deserialization fails during communication with the cache.
SerializationError,
/// Returned when the value is missing.
MissingValue,
}
impl fmt::Display for CacheError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(feature = "redis-cache")]
CacheError::RedisError(redis_error) => {
if let Some(detail) = redis_error.detail() {
write!(f, "{}", detail)
} else {
write!(f, "")
}
}
CacheError::PoolExhaustionWithConnectionDropError => {
write!(
f,
"Error all connections from the pool dropped with connection error"
)
}
CacheError::MissingValue => {
write!(f, "The value is missing from the cache")
}
CacheError::SerializationError => {
write!(f, "Unable to serialize, deserialize from the cache")
}
}
}
}
impl error_stack::Context for CacheError {}

6
src/cache/mod.rs vendored
View File

@ -1 +1,7 @@
//! This module provides the modules which provide the functionality to cache the aggregated
//! results fetched and aggregated from the upstream search engines in a json format.
pub mod cacher;
pub mod error;
#[cfg(feature = "redis-cache")]
pub mod redis_cacher;

167
src/cache/redis_cacher.rs vendored Normal file
View File

@ -0,0 +1,167 @@
//! This module provides the functionality to cache the aggregated results fetched and aggregated
//! from the upstream search engines in a json format.
use error_stack::Report;
use futures::future::try_join_all;
use md5::compute;
use redis::{aio::ConnectionManager, AsyncCommands, Client, RedisError};
use super::error::CacheError;
/// A named struct which stores the redis Connection url address to which the client will
/// connect to.
#[derive(Clone)]
pub struct RedisCache {
/// It stores a pool of connections ready to be used.
connection_pool: Vec<ConnectionManager>,
/// It stores the size of the connection pool (in other words the number of
/// connections that should be stored in the pool).
pool_size: u8,
/// It stores the index of which connection is being used at the moment.
current_connection: u8,
}
impl RedisCache {
/// A function which fetches the cached json results as json string.
///
/// # Arguments
///
/// * `redis_connection_url` - It takes the redis Connection url address.
/// * `pool_size` - It takes the size of the connection pool (in other words the number of
/// connections that should be stored in the pool).
///
/// # Error
///
/// Returns a newly constructed `RedisCache` struct on success otherwise returns a standard
/// error type.
pub async fn new(
redis_connection_url: &str,
pool_size: u8,
) -> Result<Self, Box<dyn std::error::Error>> {
let client = Client::open(redis_connection_url)?;
let mut tasks: Vec<_> = Vec::new();
for _ in 0..pool_size {
tasks.push(client.get_tokio_connection_manager());
}
let redis_cache = RedisCache {
connection_pool: try_join_all(tasks).await?,
pool_size,
current_connection: Default::default(),
};
Ok(redis_cache)
}
/// A helper function which computes the hash of the url and formats and returns it as string.
///
/// # Arguments
///
/// * `url` - It takes an url as string.
fn hash_url(&self, url: &str) -> String {
format!("{:?}", compute(url))
}
/// A function which fetches the cached json results as json string from the redis server.
///
/// # Arguments
///
/// * `url` - It takes an url as a string.
///
/// # Error
///
/// Returns the results as a String from the cache on success otherwise returns a `CacheError`
/// on a failure.
pub async fn cached_json(&mut self, url: &str) -> Result<String, Report<CacheError>> {
self.current_connection = Default::default();
let hashed_url_string: &str = &self.hash_url(url);
let mut result: Result<String, RedisError> = self.connection_pool
[self.current_connection as usize]
.get(hashed_url_string)
.await;
// Code to check whether the current connection being used is dropped with connection error
// or not. if it drops with the connection error then the current connection is replaced
// with a new connection from the pool which is then used to run the redis command then
// that connection is also checked whether it is dropped or not if it is not then the
// result is passed as a `Result` or else the same process repeats again and if all of the
// connections in the pool result in connection drop error then a custom pool error is
// returned.
loop {
match result {
Err(error) => match error.is_connection_dropped() {
true => {
self.current_connection += 1;
if self.current_connection == self.pool_size {
return Err(Report::new(
CacheError::PoolExhaustionWithConnectionDropError,
));
}
result = self.connection_pool[self.current_connection as usize]
.get(hashed_url_string)
.await;
continue;
}
false => return Err(Report::new(CacheError::RedisError(error))),
},
Ok(res) => return Ok(res),
}
}
}
/// A function which caches the results by using the hashed `url` as the key and
/// `json results` as the value and stores it in redis server with ttl(time to live)
/// set to 60 seconds.
///
/// # Arguments
///
/// * `json_results` - It takes the json results string as an argument.
/// * `url` - It takes the url as a String.
///
/// # Error
///
/// Returns an unit type if the results are cached succesfully otherwise returns a `CacheError`
/// on a failure.
pub async fn cache_results(
&mut self,
json_results: &str,
url: &str,
) -> Result<(), Report<CacheError>> {
self.current_connection = Default::default();
let hashed_url_string: &str = &self.hash_url(url);
let mut result: Result<(), RedisError> = self.connection_pool
[self.current_connection as usize]
.set_ex(hashed_url_string, json_results, 60)
.await;
// Code to check whether the current connection being used is dropped with connection error
// or not. if it drops with the connection error then the current connection is replaced
// with a new connection from the pool which is then used to run the redis command then
// that connection is also checked whether it is dropped or not if it is not then the
// result is passed as a `Result` or else the same process repeats again and if all of the
// connections in the pool result in connection drop error then a custom pool error is
// returned.
loop {
match result {
Err(error) => match error.is_connection_dropped() {
true => {
self.current_connection += 1;
if self.current_connection == self.pool_size {
return Err(Report::new(
CacheError::PoolExhaustionWithConnectionDropError,
));
}
result = self.connection_pool[self.current_connection as usize]
.set_ex(hashed_url_string, json_results, 60)
.await;
continue;
}
false => return Err(Report::new(CacheError::RedisError(error))),
},
Ok(_) => return Ok(()),
}
}
}
}

4
src/config/mod.rs Normal file
View File

@ -0,0 +1,4 @@
//! This module provides the modules which handles the functionality to parse the lua config
//! and convert the config options into rust readable form.
pub mod parser;

152
src/config/parser.rs Normal file
View File

@ -0,0 +1,152 @@
//! This module provides the functionality to parse the lua config and convert the config options
//! into rust readable form.
use crate::handler::paths::{file_path, FileType};
use crate::models::parser_models::{AggregatorConfig, RateLimiter, Style};
use log::LevelFilter;
use mlua::Lua;
use std::{collections::HashMap, fs, thread::available_parallelism};
/// A named struct which stores the parsed config file options.
#[derive(Clone)]
pub struct Config {
/// It stores the parsed port number option on which the server should launch.
pub port: u16,
/// It stores the parsed ip address option on which the server should launch
pub binding_ip: String,
/// It stores the theming options for the website.
pub style: Style,
#[cfg(feature = "redis-cache")]
/// It stores the redis connection url address on which the redis
/// client should connect.
pub redis_url: String,
/// It stores the option to whether enable or disable production use.
pub aggregator: AggregatorConfig,
/// It stores the option to whether enable or disable logs.
pub logging: bool,
/// It stores the option to whether enable or disable debug mode.
pub debug: bool,
/// It stores all the engine names that were enabled by the user.
pub upstream_search_engines: Vec<crate::models::engine_models::EngineHandler>,
/// It stores the time (secs) which controls the server request timeout.
pub request_timeout: u8,
/// It stores the number of threads which controls the app will use to run.
pub threads: u8,
/// It stores configuration options for the ratelimiting middleware.
pub rate_limiter: RateLimiter,
/// It stores the level of safe search to be used for restricting content in the
/// search results.
pub safe_search: u8,
}
impl Config {
/// A function which parses the config.lua file and puts all the parsed options in the newly
/// constructed Config struct and returns it.
///
/// # Arguments
///
/// * `logging_initialized` - It takes a boolean which ensures that the logging doesn't get
/// initialized twice. Pass false if the logger has not yet been initialized.
///
/// # Error
///
/// Returns a lua parse error if parsing of the config.lua file fails or has a syntax error
/// or io error if the config.lua file doesn't exists otherwise it returns a newly constructed
/// Config struct with all the parsed config options from the parsed config file.
pub fn parse(logging_initialized: bool) -> Result<Self, Box<dyn std::error::Error>> {
let lua = Lua::new();
let globals = lua.globals();
lua.load(&fs::read_to_string(file_path(FileType::Config)?)?)
.exec()?;
let parsed_threads: u8 = globals.get::<_, u8>("threads")?;
let debug: bool = globals.get::<_, bool>("debug")?;
let logging: bool = globals.get::<_, bool>("logging")?;
if !logging_initialized {
set_logging_level(debug, logging);
}
let threads: u8 = if parsed_threads == 0 {
let total_num_of_threads: usize = available_parallelism()?.get() / 2;
log::error!(
"Config Error: The value of `threads` option should be a non zero positive integer"
);
log::error!("Falling back to using {} threads", total_num_of_threads);
total_num_of_threads as u8
} else {
parsed_threads
};
let rate_limiter = globals.get::<_, HashMap<String, u8>>("rate_limiter")?;
let parsed_safe_search: u8 = globals.get::<_, u8>("safe_search")?;
let safe_search: u8 = match parsed_safe_search {
0..=4 => parsed_safe_search,
_ => {
log::error!("Config Error: The value of `safe_search` option should be a non zero positive integer from 0 to 4.");
log::error!("Falling back to using the value `1` for the option");
1
}
};
Ok(Config {
port: globals.get::<_, u16>("port")?,
binding_ip: globals.get::<_, String>("binding_ip")?,
style: Style::new(
globals.get::<_, String>("theme")?,
globals.get::<_, String>("colorscheme")?,
),
#[cfg(feature = "redis-cache")]
redis_url: globals.get::<_, String>("redis_url")?,
aggregator: AggregatorConfig {
random_delay: globals.get::<_, bool>("production_use")?,
},
logging,
debug,
upstream_search_engines: globals
.get::<_, HashMap<String, bool>>("upstream_search_engines")?
.into_iter()
.filter_map(|(key, value)| value.then_some(key))
.filter_map(|engine| crate::models::engine_models::EngineHandler::new(&engine))
.collect(),
request_timeout: globals.get::<_, u8>("request_timeout")?,
threads,
rate_limiter: RateLimiter {
number_of_requests: rate_limiter["number_of_requests"],
time_limit: rate_limiter["time_limit"],
},
safe_search,
})
}
}
/// a helper function that sets the proper logging level
///
/// # Arguments
///
/// * `debug` - It takes the option to whether enable or disable debug mode.
/// * `logging` - It takes the option to whether enable or disable logs.
fn set_logging_level(debug: bool, logging: bool) {
if let Ok(pkg_env_var) = std::env::var("PKG_ENV") {
if pkg_env_var.to_lowercase() == "dev" {
env_logger::Builder::new()
.filter(None, LevelFilter::Trace)
.init();
return;
}
}
// Initializing logging middleware with level set to default or info.
let log_level = match (debug, logging) {
(true, true) => LevelFilter::Debug,
(true, false) => LevelFilter::Debug,
(false, true) => LevelFilter::Info,
(false, false) => LevelFilter::Error,
};
env_logger::Builder::new().filter(None, log_level).init();
}

View File

@ -1,2 +0,0 @@
pub mod parser;
pub mod parser_models;

View File

@ -1,55 +0,0 @@
//! This module provides the functionality to parse the lua config and convert the config options
//! into rust readable form.
use super::parser_models::Style;
use rlua::Lua;
use std::fs;
/// A named struct which stores the parsed config file options.
///
/// # Fields
//
/// * `port` - It stores the parsed port number option on which the server should launch.
/// * `binding_ip_addr` - It stores the parsed ip address option on which the server should launch
/// * `style` - It stores the theming options for the website.
/// * `redis_connection_url` - It stores the redis connection url address on which the redis
/// client should connect.
#[derive(Clone)]
pub struct Config {
pub port: u16,
pub binding_ip_addr: String,
pub style: Style,
pub redis_connection_url: String,
}
impl Config {
/// A function which parses the config.lua file and puts all the parsed options in the newly
/// contructed Config struct and returns it.
///
/// # Error
///
/// Returns a lua parse error if parsing of the config.lua file fails or has a syntax error
/// or io error if the config.lua file doesn't exists otherwise it returns a newly contructed
/// Config struct with all the parsed config options from the parsed config file.
pub fn parse() -> Result<Self, Box<dyn std::error::Error>> {
let lua = Lua::new();
lua.context(|context| {
let globals = context.globals();
context
.load(&fs::read_to_string("./websurfx/config.lua")?)
.exec()?;
Ok(Config {
port: globals.get::<_, u16>("port")?,
binding_ip_addr: globals.get::<_, String>("binding_ip_addr")?,
style: Style::new(
globals.get::<_, String>("theme")?,
globals.get::<_, String>("colorscheme")?,
),
redis_connection_url: globals.get::<_, String>("redis_connection_url")?,
})
})
}
}

View File

@ -4,102 +4,113 @@
use std::collections::HashMap;
use reqwest::header::{HeaderMap, CONTENT_TYPE, COOKIE, REFERER, USER_AGENT};
use reqwest::header::HeaderMap;
use scraper::{Html, Selector};
use crate::search_results_handler::aggregation_models::RawSearchResult;
use crate::models::aggregation_models::SearchResult;
/// This function scrapes results from the upstream engine duckduckgo and puts all the scraped
/// results like title, visiting_url (href in html),engine (from which engine it was fetched from)
/// and description in a RawSearchResult and then adds that to HashMap whose keys are url and
/// values are RawSearchResult struct and then returns it within a Result enum.
///
/// # Arguments
///
/// * `query` - Takes the user provided query to query to the upstream search engine with.
/// * `page` - Takes an u32 as an argument.
/// * `user_agent` - Takes a random user agent string as an argument.
///
/// # Errors
///
/// Returns a reqwest error if the user is not connected to the internet or if their is failure to
/// reach the above `upstream search engine` page and also returns error if the scraping
/// selector fails to initialize"
pub async fn results(
query: &str,
page: u32,
user_agent: &str,
) -> Result<HashMap<String, RawSearchResult>, Box<dyn std::error::Error>> {
// Page number can be missing or empty string and so appropriate handling is required
// so that upstream server recieves valid page number.
let url: String = match page {
1 => {
format!("https://html.duckduckgo.com/html/?q={query}&s=&dc=&v=1&o=json&api=/d.js")
}
_ => {
format!(
"https://duckduckgo.com/html/?q={}&s={}&dc={}&v=1&o=json&api=/d.js",
query,
(page / 2 + (page % 2)) * 30,
(page / 2 + (page % 2)) * 30 + 1
)
}
};
use crate::models::engine_models::{EngineError, SearchEngine};
// initializing HeaderMap and adding appropriate headers.
let mut header_map = HeaderMap::new();
header_map.insert(USER_AGENT, user_agent.parse()?);
header_map.insert(REFERER, "https://google.com/".parse()?);
header_map.insert(CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?);
header_map.insert(COOKIE, "kl=wt-wt".parse()?);
use error_stack::{Report, Result, ResultExt};
// fetch the html from upstream duckduckgo engine
// TODO: Write better error handling code to handle no results case.
let results: String = reqwest::Client::new()
.get(url)
.headers(header_map) // add spoofed headers to emulate human behaviour
.send()
.await?
.text()
.await?;
/// A new DuckDuckGo engine type defined in-order to implement the `SearchEngine` trait which allows to
/// reduce code duplication as well as allows to create vector of different search engines easily.
pub struct DuckDuckGo;
let document: Html = Html::parse_document(&results);
let results: Selector = Selector::parse(".result")?;
let result_title: Selector = Selector::parse(".result__a")?;
let result_url: Selector = Selector::parse(".result__url")?;
let result_desc: Selector = Selector::parse(".result__snippet")?;
// scrape all the results from the html
Ok(document
.select(&results)
.map(|result| {
RawSearchResult::new(
result
.select(&result_title)
.next()
.unwrap()
.inner_html()
.trim()
.to_string(),
#[async_trait::async_trait]
impl SearchEngine for DuckDuckGo {
async fn results(
&self,
query: &str,
page: u32,
user_agent: &str,
request_timeout: u8,
_safe_search: u8,
) -> Result<HashMap<String, SearchResult>, EngineError> {
// Page number can be missing or empty string and so appropriate handling is required
// so that upstream server recieves valid page number.
let url: String = match page {
1 | 0 => {
format!("https://html.duckduckgo.com/html/?q={query}&s=&dc=&v=1&o=json&api=/d.js")
}
_ => {
format!(
"https://{}",
"https://duckduckgo.com/html/?q={}&s={}&dc={}&v=1&o=json&api=/d.js",
query,
(page / 2 + (page % 2)) * 30,
(page / 2 + (page % 2)) * 30 + 1
)
}
};
// initializing HeaderMap and adding appropriate headers.
let header_map = HeaderMap::try_from(&HashMap::from([
("USER_AGENT".to_string(), user_agent.to_string()),
("REFERER".to_string(), "https://google.com/".to_string()),
(
"CONTENT_TYPE".to_string(),
"application/x-www-form-urlencoded".to_string(),
),
("COOKIE".to_string(), "kl=wt-wt".to_string()),
]))
.change_context(EngineError::UnexpectedError)?;
let document: Html = Html::parse_document(
&DuckDuckGo::fetch_html_from_upstream(self, &url, header_map, request_timeout).await?,
);
let no_result: Selector = Selector::parse(".no-results")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".no-results"))?;
if document.select(&no_result).next().is_some() {
return Err(Report::new(EngineError::EmptyResultSet));
}
let results: Selector = Selector::parse(".result")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".result"))?;
let result_title: Selector = Selector::parse(".result__a")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".result__a"))?;
let result_url: Selector = Selector::parse(".result__url")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".result__url"))?;
let result_desc: Selector = Selector::parse(".result__snippet")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".result__snippet"))?;
// scrape all the results from the html
Ok(document
.select(&results)
.map(|result| {
SearchResult::new(
result
.select(&result_url)
.select(&result_title)
.next()
.unwrap()
.inner_html()
.trim()
),
result
.select(&result_desc)
.next()
.unwrap()
.inner_html()
.trim()
.to_string(),
vec!["duckduckgo".to_string()],
)
})
.map(|search_result| (search_result.visiting_url.clone(), search_result))
.collect())
.trim(),
format!(
"https://{}",
result
.select(&result_url)
.next()
.unwrap()
.inner_html()
.trim()
)
.as_str(),
result
.select(&result_desc)
.next()
.unwrap()
.inner_html()
.trim(),
&["duckduckgo"],
)
})
.map(|search_result| (search_result.url.clone(), search_result))
.collect())
}
}

View File

@ -1,2 +1,7 @@
//! This module provides different modules which handles the functionlity to fetch results from the
//! upstream search engines based on user requested queries. Also provides different models to
//! provide a standard functions to be implemented for all the upstream search engine handling
//! code. Moreover, it also provides a custom error for the upstream search engine handling code.
pub mod duckduckgo;
pub mod searx;

View File

@ -2,90 +2,112 @@
//! by querying the upstream searx search engine instance with user provided query and with a page
//! number if provided.
use reqwest::header::{HeaderMap, CONTENT_TYPE, COOKIE, REFERER, USER_AGENT};
use reqwest::header::HeaderMap;
use scraper::{Html, Selector};
use std::collections::HashMap;
use crate::search_results_handler::aggregation_models::RawSearchResult;
use crate::models::aggregation_models::SearchResult;
use crate::models::engine_models::{EngineError, SearchEngine};
use error_stack::{Report, Result, ResultExt};
/// This function scrapes results from the upstream engine duckduckgo and puts all the scraped
/// results like title, visiting_url (href in html),engine (from which engine it was fetched from)
/// and description in a RawSearchResult and then adds that to HashMap whose keys are url and
/// values are RawSearchResult struct and then returns it within a Result enum.
///
/// # Arguments
///
/// * `query` - Takes the user provided query to query to the upstream search engine with.
/// * `page` - Takes an u32 as an argument.
/// * `user_agent` - Takes a random user agent string as an argument.
///
/// # Errors
///
/// Returns a reqwest error if the user is not connected to the internet or if their is failure to
/// reach the above `upstream search engine` page and also returns error if the scraping
/// selector fails to initialize"
pub async fn results(
query: &str,
page: u32,
user_agent: &str,
) -> Result<HashMap<String, RawSearchResult>, Box<dyn std::error::Error>> {
// Page number can be missing or empty string and so appropriate handling is required
// so that upstream server recieves valid page number.
let url: String = format!("https://searx.work/search?q={query}&pageno={page}");
/// A new Searx engine type defined in-order to implement the `SearchEngine` trait which allows to
/// reduce code duplication as well as allows to create vector of different search engines easily.
pub struct Searx;
// initializing headers and adding appropriate headers.
let mut header_map = HeaderMap::new();
header_map.insert(USER_AGENT, user_agent.parse()?);
header_map.insert(REFERER, "https://google.com/".parse()?);
header_map.insert(CONTENT_TYPE, "application/x-www-form-urlencoded".parse()?);
header_map.insert(COOKIE, "categories=general; language=auto; locale=en; autocomplete=duckduckgo; image_proxy=1; method=POST; safesearch=2; theme=simple; results_on_new_tab=1; doi_resolver=oadoi.org; simple_style=auto; center_alignment=1; query_in_title=1; infinite_scroll=0; disabled_engines=; enabled_engines=\"archive is__general\\054yep__general\\054curlie__general\\054currency__general\\054ddg definitions__general\\054wikidata__general\\054duckduckgo__general\\054tineye__general\\054lingva__general\\054startpage__general\\054yahoo__general\\054wiby__general\\054marginalia__general\\054alexandria__general\\054wikibooks__general\\054wikiquote__general\\054wikisource__general\\054wikiversity__general\\054wikivoyage__general\\054dictzone__general\\054seznam__general\\054mojeek__general\\054naver__general\\054wikimini__general\\054brave__general\\054petalsearch__general\\054goo__general\"; disabled_plugins=; enabled_plugins=\"searx.plugins.hostname_replace\\054searx.plugins.oa_doi_rewrite\\054searx.plugins.vim_hotkeys\"; tokens=; maintab=on; enginetab=on".parse()?);
#[async_trait::async_trait]
impl SearchEngine for Searx {
async fn results(
&self,
query: &str,
page: u32,
user_agent: &str,
request_timeout: u8,
mut safe_search: u8,
) -> Result<HashMap<String, SearchResult>, EngineError> {
// Page number can be missing or empty string and so appropriate handling is required
// so that upstream server recieves valid page number.
if safe_search == 3 {
safe_search = 2;
};
// fetch the html from upstream searx instance engine
// TODO: Write better error handling code to handle no results case.
let results: String = reqwest::Client::new()
.get(url)
.headers(header_map) // add spoofed headers to emulate human behaviours.
.send()
.await?
.text()
.await?;
let url: String = match page {
0 | 1 => {
format!("https://searx.work/search?q={query}&pageno=1&safesearch={safe_search}")
}
_ => format!(
"https://searx.work/search?q={query}&pageno={page}&safesearch={safe_search}"
),
};
let document: Html = Html::parse_document(&results);
let results: Selector = Selector::parse(".result")?;
let result_title: Selector = Selector::parse("h3>a")?;
let result_url: Selector = Selector::parse("h3>a")?;
let result_desc: Selector = Selector::parse(".content")?;
// initializing headers and adding appropriate headers.
let header_map = HeaderMap::try_from(&HashMap::from([
("USER_AGENT".to_string(), user_agent.to_string()),
("REFERER".to_string(), "https://google.com/".to_string()),
("CONTENT_TYPE".to_string(), "application/x-www-form-urlencoded".to_string()),
("COOKIE".to_string(), "categories=general; language=auto; locale=en; autocomplete=duckduckgo; image_proxy=1; method=POST; safesearch=2; theme=simple; results_on_new_tab=1; doi_resolver=oadoi.org; simple_style=auto; center_alignment=1; query_in_title=1; infinite_scroll=0; disabled_engines=; enabled_engines=\"archive is__general\\054yep__general\\054curlie__general\\054currency__general\\054ddg definitions__general\\054wikidata__general\\054duckduckgo__general\\054tineye__general\\054lingva__general\\054startpage__general\\054yahoo__general\\054wiby__general\\054marginalia__general\\054alexandria__general\\054wikibooks__general\\054wikiquote__general\\054wikisource__general\\054wikiversity__general\\054wikivoyage__general\\054dictzone__general\\054seznam__general\\054mojeek__general\\054naver__general\\054wikimini__general\\054brave__general\\054petalsearch__general\\054goo__general\"; disabled_plugins=; enabled_plugins=\"searx.plugins.hostname_replace\\054searx.plugins.oa_doi_rewrite\\054searx.plugins.vim_hotkeys\"; tokens=; maintab=on; enginetab=on".to_string())
]))
.change_context(EngineError::UnexpectedError)?;
// scrape all the results from the html
Ok(document
.select(&results)
.map(|result| {
RawSearchResult::new(
result
.select(&result_title)
.next()
.unwrap()
.inner_html()
.trim()
.to_string(),
result
.select(&result_url)
.next()
.unwrap()
.value()
.attr("href")
.unwrap()
.to_string(),
result
.select(&result_desc)
.next()
.unwrap()
.inner_html()
.trim()
.to_string(),
vec!["searx".to_string()],
)
})
.map(|search_result| (search_result.visiting_url.clone(), search_result))
.collect())
let document: Html = Html::parse_document(
&Searx::fetch_html_from_upstream(self, &url, header_map, request_timeout).await?,
);
let no_result: Selector = Selector::parse("#urls>.dialog-error>p")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| {
format!("invalid CSS selector: {}", "#urls>.dialog-error>p")
})?;
if let Some(no_result_msg) = document.select(&no_result).nth(1) {
if no_result_msg.inner_html()
== "we didn't find any results. Please use another query or search in more categories"
{
return Err(Report::new(EngineError::EmptyResultSet));
}
}
let results: Selector = Selector::parse(".result")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".result"))?;
let result_title: Selector = Selector::parse("h3>a")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", "h3>a"))?;
let result_url: Selector = Selector::parse("h3>a")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", "h3>a"))?;
let result_desc: Selector = Selector::parse(".content")
.map_err(|_| Report::new(EngineError::UnexpectedError))
.attach_printable_lazy(|| format!("invalid CSS selector: {}", ".content"))?;
// scrape all the results from the html
Ok(document
.select(&results)
.map(|result| {
SearchResult::new(
result
.select(&result_title)
.next()
.unwrap()
.inner_html()
.trim(),
result
.select(&result_url)
.next()
.unwrap()
.value()
.attr("href")
.unwrap(),
result
.select(&result_desc)
.next()
.unwrap()
.inner_html()
.trim(),
&["searx"],
)
})
.map(|search_result| (search_result.url.clone(), search_result))
.collect())
}
}

5
src/handler/mod.rs Normal file
View File

@ -0,0 +1,5 @@
//! This module provides modules which provide the functionality to handle paths for different
//! files present on different paths and provide one appropriate path on which it is present and
//! can be used.
pub mod paths;

119
src/handler/paths.rs Normal file
View File

@ -0,0 +1,119 @@
//! This module provides the functionality to handle theme folder present on different paths and
//! provide one appropriate path on which it is present and can be used.
use std::collections::HashMap;
use std::io::Error;
use std::path::Path;
use std::sync::OnceLock;
// ------- Constants --------
/// The constant holding the name of the theme folder.
const PUBLIC_DIRECTORY_NAME: &str = "public";
/// The constant holding the name of the common folder.
const COMMON_DIRECTORY_NAME: &str = "websurfx";
/// The constant holding the name of the config file.
const CONFIG_FILE_NAME: &str = "config.lua";
/// The constant holding the name of the AllowList text file.
const ALLOWLIST_FILE_NAME: &str = "allowlist.txt";
/// The constant holding the name of the BlockList text file.
const BLOCKLIST_FILE_NAME: &str = "blocklist.txt";
/// An enum type which provides different variants to handle paths for various files/folders.
#[derive(Hash, PartialEq, Eq, Debug)]
pub enum FileType {
/// This variant handles all the paths associated with the config file.
Config,
/// This variant handles all the paths associated with the Allowlist text file.
AllowList,
/// This variant handles all the paths associated with the BlockList text file.
BlockList,
/// This variant handles all the paths associated with the public folder (Theme folder).
Theme,
}
/// A static variable which stores the different filesystem paths for various file/folder types.
static FILE_PATHS_FOR_DIFF_FILE_TYPES: OnceLock<HashMap<FileType, Vec<String>>> = OnceLock::new();
/// A function which returns an appropriate path for thr provided file type by checking if the path
/// for the given file type exists on that path.
///
/// # Error
///
/// Returns a `<File Name> folder/file not found!!` error if the give file_type folder/file is not
/// present on the path on which it is being tested.
///
/// # Example
///
/// If this function is give the file_type of Theme variant then the theme folder is checked by the
/// following steps:
///
/// 1. `/opt/websurfx` if it not present here then it fallbacks to the next one (2)
/// 2. Under project folder ( or codebase in other words) if it is not present
/// here then it returns an error as mentioned above.
pub fn file_path(file_type: FileType) -> Result<&'static str, Error> {
let file_path: &Vec<String> = FILE_PATHS_FOR_DIFF_FILE_TYPES
.get_or_init(|| {
HashMap::from([
(
FileType::Config,
vec![
format!(
"{}/.config/{}/{}",
std::env::var("HOME").unwrap(),
COMMON_DIRECTORY_NAME,
CONFIG_FILE_NAME
),
format!("/etc/xdg/{}/{}", COMMON_DIRECTORY_NAME, CONFIG_FILE_NAME),
format!("./{}/{}", COMMON_DIRECTORY_NAME, CONFIG_FILE_NAME),
],
),
(
FileType::Theme,
vec![
format!("/opt/websurfx/{}/", PUBLIC_DIRECTORY_NAME),
format!("./{}/", PUBLIC_DIRECTORY_NAME),
],
),
(
FileType::AllowList,
vec![
format!(
"{}/.config/{}/{}",
std::env::var("HOME").unwrap(),
COMMON_DIRECTORY_NAME,
ALLOWLIST_FILE_NAME
),
format!("/etc/xdg/{}/{}", COMMON_DIRECTORY_NAME, ALLOWLIST_FILE_NAME),
format!("./{}/{}", COMMON_DIRECTORY_NAME, ALLOWLIST_FILE_NAME),
],
),
(
FileType::BlockList,
vec![
format!(
"{}/.config/{}/{}",
std::env::var("HOME").unwrap(),
COMMON_DIRECTORY_NAME,
BLOCKLIST_FILE_NAME
),
format!("/etc/xdg/{}/{}", COMMON_DIRECTORY_NAME, BLOCKLIST_FILE_NAME),
format!("./{}/{}", COMMON_DIRECTORY_NAME, BLOCKLIST_FILE_NAME),
],
),
])
})
.get(&file_type)
.unwrap();
for (idx, _) in file_path.iter().enumerate() {
if Path::new(file_path[idx].as_str()).exists() {
return Ok(std::mem::take(&mut &*file_path[idx]));
}
}
// if no of the configs above exist, return error
Err(Error::new(
std::io::ErrorKind::NotFound,
format!("{:?} file/folder not found!!", file_type),
))
}

View File

@ -1,20 +1,30 @@
//! This main library module provides the functionality to provide and handle the Tcp server
//! and register all the routes for the `websurfx` meta search engine website.
#![forbid(unsafe_code, clippy::panic)]
#![deny(missing_docs, clippy::missing_docs_in_private_items, clippy::perf)]
#![warn(clippy::cognitive_complexity, rust_2018_idioms)]
pub mod cache;
pub mod config_parser;
pub mod config;
pub mod engines;
pub mod search_results_handler;
pub mod handler;
pub mod models;
pub mod results;
pub mod server;
use std::net::TcpListener;
use crate::server::routes;
use crate::server::router;
use actix_cors::Cors;
use actix_files as fs;
use actix_web::{dev::Server, middleware::Logger, web, App, HttpServer};
use config_parser::parser::Config;
use actix_governor::{Governor, GovernorConfigBuilder};
use actix_web::{dev::Server, http::header, middleware::Logger, web, App, HttpServer};
use cache::cacher::{Cache, SharedCache};
use config::parser::Config;
use handlebars::Handlebars;
use handler::paths::{file_path, FileType};
/// Runs the web server on the provided TCP listener and returns a `Server` instance.
///
@ -30,36 +40,69 @@ use handlebars::Handlebars;
///
/// ```rust
/// use std::net::TcpListener;
/// use websurfx::{config_parser::parser::Config, run};
/// use websurfx::{config::parser::Config, run, cache::cacher::Cache};
///
/// let config = Config::parse().unwrap();
/// let config = Config::parse(true).unwrap();
/// let listener = TcpListener::bind("127.0.0.1:8080").expect("Failed to bind address");
/// let server = run(listener,config).expect("Failed to start server");
/// let cache = Cache::new_in_memory();
/// let server = run(listener,config,cache).expect("Failed to start server");
/// ```
pub fn run(listener: TcpListener, config: Config) -> std::io::Result<Server> {
let mut handlebars: Handlebars = Handlebars::new();
pub fn run(listener: TcpListener, config: Config, cache: Cache) -> std::io::Result<Server> {
let mut handlebars: Handlebars<'_> = Handlebars::new();
let public_folder_path: &str = file_path(FileType::Theme)?;
handlebars
.register_templates_directory(".html", "./public/templates")
.register_templates_directory(".html", format!("{}/templates", public_folder_path))
.unwrap();
let handlebars_ref: web::Data<Handlebars> = web::Data::new(handlebars);
let handlebars_ref: web::Data<Handlebars<'_>> = web::Data::new(handlebars);
let cloned_config_threads_opt: u8 = config.threads;
let cache = web::Data::new(SharedCache::new(cache));
let server = HttpServer::new(move || {
let cors: Cors = Cors::default()
.allow_any_origin()
.allowed_methods(vec!["GET"])
.allowed_headers(vec![
header::ORIGIN,
header::CONTENT_TYPE,
header::REFERER,
header::COOKIE,
]);
App::new()
.wrap(Logger::default()) // added logging middleware for logging.
.app_data(handlebars_ref.clone())
.app_data(web::Data::new(config.clone()))
.wrap(Logger::default()) // added logging middleware for logging.
.app_data(cache.clone())
.wrap(cors)
.wrap(Governor::new(
&GovernorConfigBuilder::default()
.per_second(config.rate_limiter.time_limit as u64)
.burst_size(config.rate_limiter.number_of_requests as u32)
.finish()
.unwrap(),
))
// Serve images and static files (css and js files).
.service(fs::Files::new("/static", "./public/static").show_files_listing())
.service(fs::Files::new("/images", "./public/images").show_files_listing())
.service(routes::robots_data) // robots.txt
.service(routes::index) // index page
.service(routes::search) // search page
.service(routes::about) // about page
.service(routes::settings) // settings page
.default_service(web::route().to(routes::not_found)) // error page
.service(
fs::Files::new("/static", format!("{}/static", public_folder_path))
.show_files_listing(),
)
.service(
fs::Files::new("/images", format!("{}/images", public_folder_path))
.show_files_listing(),
)
.service(router::robots_data) // robots.txt
.service(router::index) // index page
.service(server::routes::search::search) // search page
.service(router::about) // about page
.service(router::settings) // settings page
.default_service(web::route().to(router::not_found)) // error page
})
.workers(cloned_config_threads_opt as usize)
// Start server on 127.0.0.1 with the user provided port number. for example 127.0.0.1:8080.
.listen(listener)?
.run();

Some files were not shown because too many files have changed in this diff Show More