Made message signing in Cryptography use SHA512 as the message content for... #1

Closed
netkas wants to merge 421 commits from master into dev
226 changed files with 24593 additions and 2539 deletions

36
.env Normal file
View file

@ -0,0 +1,36 @@
# Socialbox Configuration
LOG_LEVEL=debug
SB_MODE=automated
SB_DOMAIN=localhost
SB_INSTANCE_NAME=Socialbox
SB_RPC_ENDPOINT=http://127.0.0.0:8085/
SB_LOGGING_CONSOLE_ENABLED=true
SB_LOGGING_CONSOLE_LEVEL=info
SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS=true
SB_CRYPTO_KEYPAIR_EXPIRES=<duration>
SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM=xchacha20
SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM=chacha20
SB_CACHE_ENABLED=true
SB_CACHE_PORT=6379
SB_CACHE_USERNAME=root
SB_CACHE_PASSWORD=root
SB_CACHE_DATABASE=0
# MariaDB Configuration
MYSQL_ROOT_PASSWORD=sb_root
MYSQL_DATABASE=socialbox
MYSQL_USER=socialbox
MYSQL_PASSWORD=socialbox
# Redis Configuration
REDIS_PASSWORD=root
# Test Configuration, can be ignored. Used for docker-compose-test.yml
SB_COFFEE_NAME=coffee
SB_COFFEE_DOMAIN=coffee.com
SB_COFFEE_RPC_ENDPOINT=http://coffee_socialbox:8085/
SB_INSTANCE_DNS_MOCK_COFFEE="coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"
SB_TEAPOT_DOMAIN=teapot.com
SB_TEAPOT_RPC_ENDPOINT=http://teapot_socialbox:8085/
SB_INSTANCE_DNS_MOCK_TEAPOT="teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"

View file

@ -9,7 +9,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: php:8.3 image: php:8.3
@ -51,14 +51,163 @@ jobs:
- name: Build project - name: Build project
run: | run: |
ncc build --config release --log-level debug ncc build --config release --build-source --log-level debug
- name: Upload build artifacts - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: Socialbox_build name: release
path: build/release/net.nosial.socialbox.ncc path: build/release/net.nosial.socialbox.ncc
debug:
runs-on: ubuntu-latest
container:
image: php:8.3
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt update -yqq
apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
- name: Install phive
run: |
wget -O phive.phar https://phar.io/releases/phive.phar
wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79
gpg --verify phive.phar.asc phive.phar
chmod +x phive.phar
mv phive.phar /usr/local/bin/phive
- name: Install phab
run: |
phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
- name: Install latest version of NCC
run: |
git clone https://git.n64.cc/nosial/ncc.git
cd ncc
make redist
NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1)
if [ -z "$NCC_DIR" ]; then
echo "NCC build directory not found"
exit 1
fi
php "$NCC_DIR/INSTALL" --auto
cd .. && rm -rf ncc
- name: Build project
run: |
ncc build --config debug --build-source --log-level debug
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: debug
path: build/debug/net.nosial.socialbox.ncc
release_executable:
runs-on: ubuntu-latest
container:
image: php:8.3
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt update -yqq
apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
- name: Install phive
run: |
wget -O phive.phar https://phar.io/releases/phive.phar
wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79
gpg --verify phive.phar.asc phive.phar
chmod +x phive.phar
mv phive.phar /usr/local/bin/phive
- name: Install phab
run: |
phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
- name: Install latest version of NCC
run: |
git clone https://git.n64.cc/nosial/ncc.git
cd ncc
make redist
NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1)
if [ -z "$NCC_DIR" ]; then
echo "NCC build directory not found"
exit 1
fi
php "$NCC_DIR/INSTALL" --auto
cd .. && rm -rf ncc
- name: Build project
run: |
ncc build --config release_executable --build-source --log-level debug
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: release_executable
path: build/release/Socialbox
debug_executable:
runs-on: ubuntu-latest
container:
image: php:8.3
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt update -yqq
apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
- name: Install phive
run: |
wget -O phive.phar https://phar.io/releases/phive.phar
wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79
gpg --verify phive.phar.asc phive.phar
chmod +x phive.phar
mv phive.phar /usr/local/bin/phive
- name: Install phab
run: |
phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
- name: Install latest version of NCC
run: |
git clone https://git.n64.cc/nosial/ncc.git
cd ncc
make redist
NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1)
if [ -z "$NCC_DIR" ]; then
echo "NCC build directory not found"
exit 1
fi
php "$NCC_DIR/INSTALL" --auto
cd .. && rm -rf ncc
- name: Build project
run: |
ncc build --config debug_executable --build-source --log-level debug
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: debug_executable
path: build/debug/Socialbox
# Checking for phpunit.xml
check-phpunit: check-phpunit:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
@ -74,9 +223,59 @@ jobs:
else else
echo "phpunit-exists=false" >> $GITHUB_OUTPUT echo "phpunit-exists=false" >> $GITHUB_OUTPUT
fi fi
# Checking for phpdoc.dist.xml
check-phpdoc:
runs-on: ubuntu-latest
outputs:
phpdoc-exists: ${{ steps.check.outputs.phpdoc-exists }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Check for phpdoc.dist.xml
id: check
run: |
if [ -f phpdoc.dist.xml ]; then
echo "phpdoc-exists=true" >> $GITHUB_OUTPUT
else
echo "phpdoc-exists=false" >> $GITHUB_OUTPUT
fi
generate-phpdoc:
needs: [release, check-phpdoc]
runs-on: ubuntu-latest
container:
image: php:8.3
if: needs.check-phpdoc.outputs.phpdoc-exists == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies
run: |
apt update -yqq
apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
- name: Download PHPDocumentor
run: |
wget https://phpdoc.org/phpDocumentor.phar
chmod +x phpDocumentor.phar
- name: Generate PHPDoc
run: |
php phpDocumentor.phar -d src -t docs
- name: Archive PHPDoc
run: |
zip -r docs.zip docs
- name: Upload PHPDoc
uses: actions/upload-artifact@v4
with:
name: documentation
path: docs.zip
test: test:
needs: [build, check-phpunit] needs: [release, debug, release_executable, debug_executable, check-phpunit]
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
image: php:8.3 image: php:8.3
@ -89,8 +288,8 @@ jobs:
- name: Download build artifacts - name: Download build artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: Socialbox_build name: release
path: Socialbox_build # Adjust this to download the artifact directly under 'Socialbox_build' path: release
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -98,7 +297,7 @@ jobs:
apt install git libpq-dev libzip-dev zip make wget gnupg -yqq apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
curl -sSLf -o /usr/local/bin/install-php-extensions https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions curl -sSLf -o /usr/local/bin/install-php-extensions https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions
chmod +x /usr/local/bin/install-php-extensions chmod +x /usr/local/bin/install-php-extensions
install-php-extensions zip pdo_mysql mysqli mcrypt install-php-extensions zip
- name: Install phive - name: Install phive
run: | run: |
@ -128,15 +327,22 @@ jobs:
- name: Install NCC packages - name: Install NCC packages
run: | run: |
ncc package install --package="Socialbox_build/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug ncc package install --package="release/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug
- name: Run PHPUnit tests - name: Run PHPUnit tests
run: | run: |
wget https://phar.phpunit.de/phpunit-11.3.phar wget https://phar.phpunit.de/phpunit-11.3.phar
php phpunit-11.3.phar --configuration phpunit.xml php phpunit-11.3.phar --configuration phpunit.xml --log-junit reports/junit.xml --log-teamcity reports/teamcity --testdox-html reports/testdox.html --testdox-text reports/testdox.txt
release: - name: Upload test reports
needs: [build, test] uses: actions/upload-artifact@v4
with:
name: reports
path: reports
release-documentation:
needs: generate-phpdoc
permissions: write-all permissions: write-all
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: container:
@ -147,16 +353,78 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Download build artifacts - name: Download documentation artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: Socialbox_build name: documentation
path: Socialbox_build path: documentation
- name: Upload to GitHub Release - name: Upload documentation artifact
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v1
with: with:
files: | files: |
Socialbox_build/net.nosial.socialbox.ncc documentation/*
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release-artifacts:
needs: [release, debug, release_executable, debug_executable]
permissions: write-all
runs-on: ubuntu-latest
container:
image: php:8.3
if: github.event_name == 'release'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Download release artifact
uses: actions/download-artifact@v4
with:
name: release
path: release
- name: Upload release artifact to release
uses: softprops/action-gh-release@v1
with:
files: |
release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download debug artifact
uses: actions/download-artifact@v4
with:
name: debug
path: debug
- name: Upload debug artifact to release
uses: softprops/action-gh-release@v1
with:
files: |
debug/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download release_executable artifact
uses: actions/download-artifact@v4
with:
name: release_executable
path: release_executable
- name: Upload release_executable artifact to release
uses: softprops/action-gh-release@v1
with:
files: |
release_executable/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download debug_executable artifact
uses: actions/download-artifact@v4
with:
name: debug_executable
path: debug_executable
- name: Upload debug_executable artifact to release
uses: softprops/action-gh-release@v1
with:
files: |
debug_executable/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

10
.gitignore vendored
View file

@ -1,3 +1,11 @@
/build /build
/.vscode
/.idea/gbrowser_project.xml /.idea/gbrowser_project.xml
.phpunit.result.cache .phpunit.result.cache
/socialbox
/bob_socialbox
/alice_socialbox
/coffee_socialbox/data/
/coffee_socialbox/logs/
/teapot_socialbox/data/
/teapot_socialbox/logs/

View file

@ -1,46 +1,170 @@
image: php:8.1 # Global config
default:
image: php:8.3
before_script: variables:
# Install some stuff that the image doesn't come with GIT_STRATEGY: clone
- apt update -yqq
- apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
# Install phive workflow:
- wget -O phive.phar https://phar.io/releases/phive.phar rules:
- wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc - if: $CI_PIPELINE_SOURCE == "push"
- gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 - if: $CI_PIPELINE_SOURCE == "web"
- gpg --verify phive.phar.asc phive.phar - if: $CI_COMMIT_TAG
- chmod +x phive.phar
- mv phive.phar /usr/local/bin/phive
# Install phab # Reusable template for installing common dependencies
- phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C .setup_template: &setup_definition
before_script:
- apt update -yqq
- apt install git libpq-dev libzip-dev zip make wget gnupg -yqq
# Install phive
- wget -O phive.phar https://phar.io/releases/phive.phar
- wget -O phive.phar.asc https://phar.io/releases/phive.phar.asc
- gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79
- gpg --verify phive.phar.asc phive.phar
- chmod +x phive.phar
- mv phive.phar /usr/local/bin/phive
# Install phab
- phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
# Install NCC
- git clone https://git.n64.cc/nosial/ncc.git
- cd ncc
- make redist
- NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1)
- if [ -z "$NCC_DIR" ]; then echo "NCC build directory not found"; exit 1; fi
- php "$NCC_DIR/INSTALL" --auto
- cd ..
- rm -rf ncc
# Install the latest version of ncc (Nosial Code Compiler) # Build jobs
- git clone https://git.n64.cc/nosial/ncc.git release:
- cd ncc <<: *setup_definition
- make redist
- php build/src/INSTALL --auto --install-composer
- cd .. && rm -rf ncc
build:
stage: build stage: build
script: script:
- ncc build --config release --log-level debug - ncc build --config release --build-source --log-level debug
artifacts: artifacts:
paths: paths:
- build/ - build/release/net.nosial.socialbox.ncc
rules: expire_in: 1 week
- if: $CI_COMMIT_BRANCH
release: debug:
stage: deploy <<: *setup_definition
stage: build
script: script:
- ncc build --config release --log-level debug - ncc build --config debug --build-source --log-level debug
- >
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file build/release/net.nosial.socialbox.ncc "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages/generic/net.nosial.socialbox/$CI_COMMIT_REF_NAME/net.nosial.socialbox.ncc"
artifacts: artifacts:
paths: paths:
- build/ - build/debug/net.nosial.socialbox.ncc
expire_in: 1 week
release_executable:
<<: *setup_definition
stage: build
script:
- ncc build --config release_executable --build-source --log-level debug
artifacts:
paths:
- build/release/Socialbox
expire_in: 1 week
debug_executable:
<<: *setup_definition
stage: build
script:
- ncc build --config debug_executable --build-source --log-level debug
artifacts:
paths:
- build/debug/Socialbox
expire_in: 1 week
# Check for configuration files
check_configs:
stage: .pre
script:
- |
if [ -f phpunit.xml ]; then
echo "PHPUNIT_EXISTS=true" >> build.env
else
echo "PHPUNIT_EXISTS=false" >> build.env
fi
if [ -f phpdoc.dist.xml ]; then
echo "PHPDOC_EXISTS=true" >> build.env
else
echo "PHPDOC_EXISTS=false" >> build.env
fi
artifacts:
reports:
dotenv: build.env
# Documentation generation
generate-phpdoc:
<<: *setup_definition
stage: build
needs: ["check_configs"]
rules:
- if: $PHPDOC_EXISTS == "true"
script:
- wget https://phpdoc.org/phpDocumentor.phar
- chmod +x phpDocumentor.phar
- php phpDocumentor.phar -d src -t docs
- zip -r docs.zip docs
artifacts:
paths:
- docs.zip
expire_in: 1 week
# Testing
test:
<<: *setup_definition
stage: test
needs:
- job: release
- job: check_configs
rules:
- if: $PHPUNIT_EXISTS == "true"
script:
- curl -sSLf -o /usr/local/bin/install-php-extensions https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions
- chmod +x /usr/local/bin/install-php-extensions
- install-php-extensions zip
- ncc package install --package="build/release/net.nosial.socialbox.ncc" --build-source --reinstall -y --log-level debug
- wget https://phar.phpunit.de/phpunit-11.3.phar
- php phpunit-11.3.phar --configuration phpunit.xml --log-junit reports/junit.xml --log-teamcity reports/teamcity --testdox-html reports/testdox.html --testdox-text reports/testdox.txt
artifacts:
reports:
junit: reports/junit.xml
paths:
- reports/
expire_in: 1 week
# Release jobs
release_upload:
stage: deploy
rules: rules:
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
needs:
- job: release
- job: debug
- job: release_executable
- job: debug_executable
- job: generate-phpdoc
optional: true
script:
- |
if [ -f "docs.zip" ]; then
echo "Releasing documentation..."
curl --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--form "file=@docs.zip" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/uploads"
fi
# Upload release artifacts
for artifact in build/release/net.nosial.socialbox.ncc build/debug/net.nosial.socialbox.ncc build/release/Socialbox build/debug/Socialbox; do
if [ -f "$artifact" ]; then
echo "Uploading $artifact..."
curl --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
--form "file=@${artifact}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/uploads"
fi
done
environment: production

20
.idea/dataSources.xml generated
View file

@ -8,5 +8,25 @@
<jdbc-url>jdbc:mariadb://127.0.0.1:3306/socialbox</jdbc-url> <jdbc-url>jdbc:mariadb://127.0.0.1:3306/socialbox</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir> <working-dir>$ProjectFileDir$</working-dir>
</data-source> </data-source>
<data-source source="LOCAL" name="Coffee Database" uuid="d114bae4-44f5-49c3-849a-149881b09342">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://127.0.0.1:3308/socialbox</jdbc-url>
<jdbc-additional-properties>
<property name="database.introspection.mysql.dbe5060" value="true" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="Teapot Database" uuid="64dd3c41-0f4f-4f65-b186-d42813467391">
<driver-ref>mariadb</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mariadb://127.0.0.1:3307/socialbox</jdbc-url>
<jdbc-additional-properties>
<property name="database.introspection.mysql.dbe5060" value="true" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component> </component>
</project> </project>

14
.idea/php-test-framework.xml generated Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PhpTestFrameworkVersionCache">
<tools_cache>
<tool tool_name="PHPUnit">
<cache>
<versions>
<info id="Local/home/netkas/phpunit.phar" version="11.3.5" />
</versions>
</cache>
</tool>
</tools_cache>
</component>
</project>

42
.idea/php.xml generated
View file

@ -10,18 +10,27 @@
<option name="highlightLevel" value="WARNING" /> <option name="highlightLevel" value="WARNING" />
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PhpCodeSniffer">
<phpcs_settings>
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="347c1ee9-eeae-4334-9e17-b7e47c2bca71" timeout="30000" />
</phpcs_settings>
</component>
<component name="PhpIncludePathManager"> <component name="PhpIncludePathManager">
<include_path> <include_path>
<path value="$USER_HOME$/phar" /> <path value="/var/ncc/packages/net.nosial.loglib2=1.0.0" />
<path value="/var/ncc/packages/com.symfony.uid=v7.1.5" /> <path value="/var/ncc/packages/net.nosial.loglib=2.0.0" />
<path value="/var/ncc/packages/com.symfony.filesystem=v7.1.2" /> <path value="/var/ncc/packages/com.symfony.finder=2.0.7" />
<path value="/var/ncc/packages/com.gregwar.captcha=v1.2.1" />
<path value="/var/ncc/packages/com.symfony.filesystem=v7.1.5" />
<path value="/var/ncc/packages/com.symfony.polyfill_ctype=v1.31.0" /> <path value="/var/ncc/packages/com.symfony.polyfill_ctype=v1.31.0" />
<path value="/var/ncc/packages/com.symfony.polyfill_mbstring=v1.31.0" /> <path value="/var/ncc/packages/com.symfony.polyfill_mbstring=v1.31.0" />
<path value="/var/ncc/packages/com.symfony.process=v7.1.3" /> <path value="/var/ncc/packages/com.symfony.polyfill_uuid=v1.31.0" />
<path value="/var/ncc/packages/com.symfony.yaml=v7.1.4" /> <path value="/var/ncc/packages/com.symfony.process=v7.1.5" />
<path value="/var/ncc/packages/com.symfony.uid=v7.1.5" />
<path value="/var/ncc/packages/com.symfony.yaml=v7.1.5" />
<path value="/var/ncc/packages/net.nosial.configlib=1.1.0" /> <path value="/var/ncc/packages/net.nosial.configlib=1.1.0" />
<path value="/var/ncc/packages/net.nosial.loglib=1.1.0" /> <path value="/var/ncc/packages/net.nosial.optslib=1.1.2" />
<path value="/var/ncc/packages/net.nosial.optslib=1.1.0" /> <path value="$USER_HOME$/phar" />
<path value="/usr/share/ncc" /> <path value="/usr/share/ncc" />
</include_path> </include_path>
</component> </component>
@ -50,7 +59,7 @@
<extension name="enchant" enabled="false" /> <extension name="enchant" enabled="false" />
<extension name="fann" enabled="false" /> <extension name="fann" enabled="false" />
<extension name="ffmpeg" enabled="false" /> <extension name="ffmpeg" enabled="false" />
<extension name="gd" enabled="false" /> <extension name="frankenphp" enabled="false" />
<extension name="gearman" enabled="false" /> <extension name="gearman" enabled="false" />
<extension name="geoip" enabled="false" /> <extension name="geoip" enabled="false" />
<extension name="geos" enabled="false" /> <extension name="geos" enabled="false" />
@ -69,7 +78,7 @@
<extension name="libevent" enabled="false" /> <extension name="libevent" enabled="false" />
<extension name="libsodium" enabled="false" /> <extension name="libsodium" enabled="false" />
<extension name="mailparse" enabled="false" /> <extension name="mailparse" enabled="false" />
<extension name="memcache" enabled="false" /> <extension name="memcached" enabled="false" />
<extension name="ming" enabled="false" /> <extension name="ming" enabled="false" />
<extension name="mongo" enabled="false" /> <extension name="mongo" enabled="false" />
<extension name="mongodb" enabled="false" /> <extension name="mongodb" enabled="false" />
@ -86,9 +95,6 @@
<extension name="odbc" enabled="false" /> <extension name="odbc" enabled="false" />
<extension name="opentelemetry" enabled="false" /> <extension name="opentelemetry" enabled="false" />
<extension name="pdflib" enabled="false" /> <extension name="pdflib" enabled="false" />
<extension name="pdo_ibm" enabled="false" />
<extension name="pdo_pgsql" enabled="false" />
<extension name="pdo_sqlite" enabled="false" />
<extension name="pgsql" enabled="false" /> <extension name="pgsql" enabled="false" />
<extension name="pspell" enabled="false" /> <extension name="pspell" enabled="false" />
<extension name="pthreads" enabled="false" /> <extension name="pthreads" enabled="false" />
@ -118,14 +124,24 @@
<extension name="zmq" enabled="false" /> <extension name="zmq" enabled="false" />
</extensions> </extensions>
</component> </component>
<component name="PhpStan">
<PhpStan_settings>
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="347c1ee9-eeae-4334-9e17-b7e47c2bca71" timeout="60000" />
</PhpStan_settings>
</component>
<component name="PhpStanOptionsConfiguration"> <component name="PhpStanOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>
<component name="PhpUnit"> <component name="PhpUnit">
<phpunit_settings> <phpunit_settings>
<PhpUnitSettings load_method="PHPUNIT_PHAR" bootstrap_file_path="$PROJECT_DIR$/phpunit.xml" custom_loader_path="" phpunit_phar_path="$USER_HOME$/phpunit.phar" /> <PhpUnitSettings load_method="PHPUNIT_PHAR" bootstrap_file_path="$PROJECT_DIR$/bootstrap.php" custom_loader_path="" phpunit_phar_path="$USER_HOME$/phpunit.phar" use_bootstrap_file="true" />
</phpunit_settings> </phpunit_settings>
</component> </component>
<component name="Psalm">
<Psalm_settings>
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="347c1ee9-eeae-4334-9e17-b7e47c2bca71" timeout="60000" />
</Psalm_settings>
</component>
<component name="PsalmOptionsConfiguration"> <component name="PsalmOptionsConfiguration">
<option name="transferred" value="true" /> <option name="transferred" value="true" />
</component> </component>

View file

@ -1,6 +1,6 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile"> <configuration default="false" name="Build Release" type="MAKEFILE_TARGET_RUN_CONFIGURATION" factoryName="Makefile">
<makefile filename="$PROJECT_DIR$/Makefile" target="build" workingDirectory="" arguments=""> <makefile filename="$PROJECT_DIR$/Makefile" target="release" workingDirectory="" arguments="">
<envs /> <envs />
</makefile> </makefile>
<method v="2"> <method v="2">

View file

@ -12,8 +12,6 @@
<option name="EXECUTE_IN_TERMINAL" value="true" /> <option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" /> <option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs /> <envs />
<method v="2"> <method v="2" />
<option name="RunConfigurationTask" enabled="true" run_configuration_name="Build" run_configuration_type="MAKEFILE_TARGET_RUN_CONFIGURATION" />
</method>
</configuration> </configuration>
</component> </component>

25
.idea/sqldialects.xml generated
View file

@ -1,12 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="SqlDialectMappings"> <component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/password_authentication.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/authentication_otp.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/registered_peers.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/authentication_passwords.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/captcha_images.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/channel_com.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/contact_known_keys.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/contacts.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/encryption_channels.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/encryption_channels_com.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/external_sessions.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/peer_information.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/peers.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/sessions.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/sessions.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/signing_keys.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources/database/variables.sql" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/CaptchaManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ContactManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/EncryptionChannelManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ExternalSessionManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/OneTimePasswordManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/PasswordManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/PeerInformationManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/RegisteredPeerManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/ResolvedDnsRecordsManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SessionManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SessionManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/SigningKeysManager.php" dialect="MariaDB" />
<file url="file://$PROJECT_DIR$/src/Socialbox/Managers/VariableManager.php" dialect="MariaDB" /> <file url="file://$PROJECT_DIR$/src/Socialbox/Managers/VariableManager.php" dialect="MariaDB" />
<file url="file:///var/ncc/packages/net.nosial.socialbox=1.0.0/bin/src/Socialbox/Managers/VariableManager.php" dialect="MariaDB" />
</component> </component>
</project> </project>

View file

@ -7,6 +7,8 @@
<resourceRoots> <resourceRoots>
<path value="file://$PROJECT_DIR$/examples" /> <path value="file://$PROJECT_DIR$/examples" />
<path value="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources" /> <path value="file://$PROJECT_DIR$/src/Socialbox/Classes/Resources" />
<path value="file://$PROJECT_DIR$/public" />
<path value="file://$PROJECT_DIR$/docker" />
</resourceRoots> </resourceRoots>
</entryData> </entryData>
</entry> </entry>

132
Dockerfile Normal file
View file

@ -0,0 +1,132 @@
# -----------------------------------------------------------------------------
# Dockerfile for PHP 8.3 + FPM with Cron support and Supervisor
# -----------------------------------------------------------------------------
# Base image: Official PHP 8.3 with FPM
FROM php:8.3-fpm AS base
# ----------------------------- Metadata labels ------------------------------
LABEL maintainer="Netkas <netkas@n64.cc>" \
version="1.0" \
description="Socialbox Docker image based off PHP 8.3 FPM and NCC" \
application="SocialBox" \
base_image="php:8.3-fpm"
# Environment variable for non-interactive installations
ENV DEBIAN_FRONTEND=noninteractive
# ----------------------------- System Dependencies --------------------------
# Update system packages and install required dependencies in one step
RUN apt-get update -yqq && apt-get install -yqq --no-install-recommends \
git \
libpq-dev \
libzip-dev \
zip \
make \
wget \
gnupg \
cron \
supervisor \
mariadb-client \
libcurl4-openssl-dev \
libmemcached-dev \
redis \
libgd-dev \
nginx \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
# ----------------------------- PHP Extensions -------------------------------
# Install PHP extensions and enable additional ones
RUN docker-php-ext-install -j$(nproc) \
pdo \
pdo_mysql \
mysqli \
gd \
curl \
opcache \
zip \
sockets \
pcntl && \
pecl install redis memcached && \
docker-php-ext-enable redis memcached
# ----------------------------- Additional Tools -----------------------------
# Install Phive (Package Manager for PHAR libraries) and global tools in one step
RUN wget -O /usr/local/bin/phive https://phar.io/releases/phive.phar && \
wget -O /usr/local/bin/phive.asc https://phar.io/releases/phive.phar.asc && \
gpg --keyserver hkps://keys.openpgp.org --recv-keys 0x9D8A98B29B2D5D79 && \
gpg --verify /usr/local/bin/phive.asc /usr/local/bin/phive && \
chmod +x /usr/local/bin/phive && \
phive install phpab --global --trust-gpg-keys 0x2A8299CE842DD38C
# ----------------------------- Clone and Build NCC --------------------------
# Clone the NCC repository, build the project, and install it
RUN git clone https://git.n64.cc/nosial/ncc.git && \
cd ncc && \
make redist && \
NCC_DIR=$(find build/ -type d -name "ncc_*" | head -n 1) && \
if [ -z "$NCC_DIR" ]; then \
echo "NCC build directory not found"; \
exit 1; \
fi && \
php "$NCC_DIR/INSTALL" --auto && \
cd .. && rm -rf ncc
# ----------------------------- Project Build ---------------------------------
# Set build directory and copy pre-needed project files
WORKDIR /tmp/build
COPY . .
RUN ncc build --config release --build-source --log-level debug && \
ncc package install --package=build/release/net.nosial.socialbox.ncc --build-source -y --log-level=debug
# Clean up
RUN rm -rf /tmp/build && rm -rf /var/www/html/*
# Copy over the required files
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY public/index.php /var/www/html/index.php
RUN chown -R www-data:www-data /var/www/html && chmod -R 755 /var/www/html
# ----------------------------- Environment Configuration ---------------------------
# Configure Cron
RUN echo "*/1 * * * * root for i in {1..12}; do /usr/bin/socialbox process-outgoing; sleep 5; done" > /etc/cron.d/socialbox-process-outgoing && \
echo "*/1 * * * * root /usr/bin/socialbox session-cleanup" > /etc/cron.d/socialbox-session-cleanup && \
echo "*/5 * * * * root /usr/bin/socialbox peer-cleanup" > /etc/cron.d/socialbox-peer-cleanup && \
\
chmod 0644 /etc/cron.d/socialbox-process-outgoing && \
chmod 0644 /etc/cron.d/socialbox-session-cleanup && \
chmod 0644 /etc/cron.d/socialbox-peer-cleanup && \
\
crontab /etc/cron.d/socialbox-process-outgoing && \
crontab /etc/cron.d/socialbox-session-cleanup && \
crontab /etc/cron.d/socialbox-peer-cleanup
# Copy Supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy docker.conf & zz-docker.conf for PHP-FPM
COPY docker/docker.conf /usr/local/etc/php-fpm.d/docker.conf
COPY docker/zz-docker.conf /usr/local/etc/php-fpm.d/zz-docker.conf
# Configure php.ini and enable error and log it to /var/log rather than stdout
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini && \
sed -i 's/^;error_log = php_errors.log/error_log = \/var\/log\/php_errors.log/' /usr/local/etc/php/php.ini && \
sed -i 's/^;log_errors = On/log_errors = On/' /usr/local/etc/php/php.ini && \
sed -i 's/^;error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT/error_reporting = E_ALL/' /usr/local/etc/php/php.ini && \
sed -i 's/^;display_errors = Off/display_errors = On/' /usr/local/etc/php/php.ini && \
sed -i 's/^;date.timezone =/date.timezone = UTC/' /usr/local/etc/php/php.ini
# ----------------------------- Cleanup ---------------------
WORKDIR /
# ----------------------------- Port Exposing ---------------------------------
EXPOSE 8085
# ----------------------------- Container Startup ----------------------------
# Copy over entrypoint script and set it as executable
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Set the entrypoint
ENTRYPOINT ["/usr/bin/bash", "/usr/local/bin/entrypoint.sh"]

View file

@ -1,23 +1,29 @@
# Variables # Variables
CONFIG ?= release DEFAULT_CONFIGURATION ?= release
LOG_LEVEL = debug LOG_LEVEL = debug
OUTDIR = build/$(CONFIG)
PACKAGE = $(OUTDIR)/net.nosial.socialbox.ncc
# Default Target # Default Target
all: build all: release debug release_executable debug_executable
# Build Steps # Build Steps
build: release:
ncc build --config=$(CONFIG) --log-level $(LOG_LEVEL) ncc build --config=release --log-level $(LOG_LEVEL)
debug:
ncc build --config=debug --log-level $(LOG_LEVEL)
release_executable:
ncc build --config=release_executable --log-level $(LOG_LEVEL)
debug_executable:
ncc build --config=debug_executable --log-level $(LOG_LEVEL)
install: build
ncc package install --package=$(PACKAGE) --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL)
test: build install: release
ncc package install --package=build/release/net.nosial.socialbox.ncc --skip-dependencies --build-source --reinstall -y --log-level $(LOG_LEVEL)
test: release
[ -f phpunit.xml ] || { echo "phpunit.xml not found"; exit 1; }
phpunit phpunit
clean: clean:
rm -rf build rm -rf build
.PHONY: all build install test clean .PHONY: all install test clean release debug release_executable debug_executable

View file

@ -0,0 +1,117 @@
{
"instance": {
"enabled": true,
"name": "coffee",
"domain": "coffee.com",
"rpc_endpoint": "http://coffee_socialbox:8085/",
"dns_mocks": {
"teapot.com": "v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0",
"coffee.com": "v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"
}
},
"security": {
"display_internal_exceptions": true,
"resolved_servers_ttl": 600,
"captcha_ttl": 200,
"otp_secret_key_length": 32,
"otp_time_step": 30,
"otp_digits": 6,
"otp_hash_algorithm": "sha512",
"otp_window": 1
},
"cryptography": {
"host_keypair_expires": 0,
"host_public_key": "sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc",
"host_private_key": "sig:tTVe59Ko5XuwgS8PneR92FAOqbgSHTKYn8U-lQRB9KODn0J_yPXCZCZGDUyS95hul2Jn7X7-EVT15FEmZADCZw",
"internal_encryption_keys": [
"c2cpdTkYqIWI93cJPpAuCsoQJcHi9l37lYHA2TpUo9A",
"XUuWyWcKmtCUNVZ7Y0ZDbCE72klHZIniRihIIo78Vbs",
"SGg4GM_0-hO95Q6hBq2UjzGrp9mhVHyklNTHo-OZSNw",
"43WrkV6rDyc04S41E4uwJ1nQFhlll_CflsPW_hMOiqE",
"QDh9KecIdU-6be5ScPagL_WrWp8hQAersLQvLv9YtNQ",
"z4SnLU9Xw9F3yjPH_TmV4HuvZrpaVE0bqxzUGHyXQ-k",
"vg7lWOzkL_59u3o2RKcdrdwc7KVh07NrZRQzBPoJXEU",
"UW6X3XGGLj_e8xYd1bUwX9KYPTczHFtYTmy4FfiqfG0",
"sh-sRIQ3lWgkqR87wcTtZkDrgDKY2FOLuzdtpAvi9Wg",
"SDweTV1kNH0s5Ah1pwbfDo3ThAXAVKo9qJ4V9-hsHIs"
],
"encryption_keys_count": 10,
"encryption_keys_algorithm": "xchacha20",
"transport_encryption_algorithm": "chacha20"
},
"database": {
"host": "coffee_mariadb",
"port": 0,
"username": "socialbox",
"password": "socialbox",
"name": "socialbox"
},
"logging": {
"console_logging_enabled": true,
"console_logging_level": "info",
"file_logging_enabled": true,
"file_logging_level": "debug"
},
"cache": {
"enabled": true,
"engine": "redis",
"host": "coffee_redis",
"port": 6379,
"username": "root",
"password": "root",
"database": "0",
"sessions": {
"enabled": true,
"ttl": 3600,
"max": 1000
}
},
"registration": {
"enabled": true,
"privacy_policy_document": null,
"privacy_policy_date": 1734985525,
"accept_privacy_policy": false,
"terms_of_service_document": null,
"terms_of_service_date": 1734985525,
"accept_terms_of_service": false,
"community_guidelines_document": null,
"community_guidelines_date": 1734985525,
"accept_community_guidelines": false,
"password_required": true,
"otp_required": false,
"display_name_required": true,
"display_picture_required": false,
"email_address_required": false,
"phone_number_required": false,
"birthday_required": false,
"image_captcha_verification_required": false,
"first_name_required": false,
"middle_name_required": false,
"last_name_required": false,
"url_required": false
},
"authentication": {
"enabled": true,
"image_captcha_verification_required": true
},
"policies": {
"max_signing_keys": 20,
"session_inactivity_expires": 43200,
"image_captcha_expires": 300,
"peer_sync_interval": 3600,
"get_contacts_limit": 100,
"default_display_picture_privacy": "PUBLIC",
"default_first_name_privacy": "CONTACTS",
"default_middle_name_privacy": "PRIVATE",
"default_last_name_privacy": "PRIVATE",
"default_email_address_privacy": "CONTACTS",
"default_phone_number_privacy": "CONTACTS",
"default_birthday_privacy": "PRIVATE",
"default_url_privacy": "PUBLIC"
},
"storage": {
"path": "/etc/socialbox",
"user_display_images_path": "user_profiles",
"user_display_images_max_size": 3145728
}
}

View file

@ -1,18 +0,0 @@
{
"name": "vendor_name/socialbox-php",
"description": "description",
"minimum-stability": "stable",
"license": "proprietary",
"authors": [
{
"name": "Netkas",
"email": "netkas@nosial.net"
}
],
"require": {
"ext-pdo": "*",
"ext-openssl": "*",
"ext-redis": "*",
"ext-memcached": "*"
}
}

245
docker-compose.test.yml Normal file
View file

@ -0,0 +1,245 @@
# Test docker-compose file for SocialBox service to setup two instances of the service:
# 1. Teapot Service (teapot.com)
# 2. Coffee Service (coffee.com)
services:
# Coffee Service (coffee.com test)
coffee_socialbox:
container_name: coffee_socialbox
build:
context: .
dockerfile: Dockerfile
ports:
- "8086:8085"
depends_on:
coffee_mariadb:
condition: service_healthy
coffee_redis:
condition: service_healthy
networks:
- coffee_network
- shared_network
restart: unless-stopped
volumes:
- ./coffee_socialbox/config:/etc/config
- ./coffee_socialbox/data:/etc/socialbox
environment:
# No need to change these values
LOG_LEVEL: ${LOG_LEVEL:-debug}
CONFIGLIB_PATH: /etc/config
LOGGING_DIRECTORY: /var/log
SB_MODE: automated
SB_STORAGE_PATH: /etc/socialbox
# Change these values to match your environment or update the .env file
SB_INSTANCE_NAME: ${SB_COFFEE_NAME:-coffee} # Instance name SB_COFFEE_NAME
SB_INSTANCE_DOMAIN: ${SB_COFFEE_DOMAIN:-coffee.com} # Instance domain SB_COFFEE_DOMAIN
SB_INSTANCE_RPC_ENDPOINT: ${SB_COFFEE_RPC_ENDPOINT:-http://coffee_socialbox:8085/} # Instance RPC endpoint SB_COFFEE_RPC_ENDPOINT
SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true}
SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug}
SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true}
SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug}
SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: true
SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES}
SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10}
SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20}
SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20}
SB_DATABASE_HOST: coffee_mariadb
SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox}
SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox}
SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true}
SB_CACHE_ENGINE: redis
SB_CACHE_HOST: coffee_redis
SB_CACHE_PORT: ${SB_CACHE_PORT:-6379}
SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root}
SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root}
SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0}
# Mocking, required for testing without the need for configuring actual DNS records
# Usage: SB_INSTANCE_DNS_MOCK_<INSTANCE_NAME>: <DOMAIN> <TXT_RECORD>
# Environment Variable name is ignored, only the value is used with the prefix being used to detect
# the instance name and the suffix being used to detect the TXT record
SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"}
SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"}
# UDP Logging, won't cause issues if the server is not available
# See https://github.com/nosial/LogLib2/blob/master/server.py for more information
LOGLIB_UDP_ENABLED: true
LOGLIB_UDP_HOST: 172.17.0.1
LOGLIB_UDP_PORT: 5131
healthcheck:
test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://coffee_socialbox:8085/}"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
coffee_mariadb:
container_name: coffee_socialbox_mariadb
image: mariadb:10.5
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox}
MYSQL_USER: ${MYSQL_USER:-socialbox}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
volumes:
- coffee_mariadb_data:/var/lib/mysql
networks:
- coffee_network
ports:
- "3308:3306"
expose:
- "3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "coffee_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
coffee_redis:
container_name: coffee_socialbox_redis
image: redis:alpine
restart: unless-stopped
command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes
volumes:
- coffee_redis_data:/data
- ./docker/redis.conf:/usr/local/etc/redis/redis.conf
networks:
- coffee_network
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
# Teapot Service (teapot.com test)
teapot_socialbox:
container_name: teapot_socialbox
build:
context: .
dockerfile: Dockerfile
ports:
- "8087:8085" # Unique port for Teapot instance
depends_on:
teapot_mariadb:
condition: service_healthy
teapot_redis:
condition: service_healthy
networks:
- teapot_network
- shared_network
restart: unless-stopped
volumes:
- ./teapot_socialbox/config:/etc/config
- ./teapot_socialbox/data:/etc/socialbox
environment:
# No need to change these values
LOG_LEVEL: ${LOG_LEVEL:-debug}
CONFIGLIB_PATH: /etc/config
LOGGING_DIRECTORY: /var/log
SB_MODE: automated
SB_STORAGE_PATH: /etc/socialbox
# Change these values to match your environment or update the .env file
SB_INSTANCE_NAME: ${SB_TEAPOT_NAME:-teapot} # Instance name SB_TEAPOT_NAME
SB_INSTANCE_DOMAIN: ${SB_TEAPOT_DOMAIN:-teapot.com} # Instance domain SB_TEAPOT_DOMAIN
SB_INSTANCE_RPC_ENDPOINT: ${SB_TEAPOT_RPC_ENDPOINT:-http://teapot_socialbox:8085/} # Instance RPC endpoint SB_TEAPOT_RPC_ENDPOINT
SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true}
SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-debug}
SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true}
SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-debug}
SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: true
SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES}
SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10}
SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20}
SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20}
SB_DATABASE_HOST: teapot_mariadb
SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox}
SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox}
SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true}
SB_CACHE_ENGINE: redis
SB_CACHE_HOST: teapot_redis
SB_CACHE_PORT: ${SB_CACHE_PORT:-6379}
SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root}
SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root}
SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0}
# Mocking, required for testing without the need for configuring actual DNS records
# Usage: SB_INSTANCE_DNS_MOCK_<INSTANCE_NAME>: <DOMAIN> <TXT_RECORD>
# Environment Variable name is ignored, only the value is used with the prefix being used to detect
# the instance name and the suffix being used to detect the TXT record
SB_INSTANCE_DNS_MOCK_COFFEE: ${SB_INSTANCE_DNS_MOCK_COFFEE:-"coffee.com v=socialbox;sb-rpc=http://coffee_socialbox:8085/;sb-key=sig:g59Cf8j1wmQmRg1MkveYbpdiZ-1-_hFU9eRRJmQAwmc;sb-exp=0"}
SB_INSTANCE_DNS_MOCK_TEAPOT: ${SB_INSTANCE_DNS_MOCK_TEAPOT:-"teapot.com v=socialbox;sb-rpc=http://teapot_socialbox:8085/;sb-key=sig:MDXUuripAo_IAv-EZTEoFhpIdhsXxfMLNunSnQzxYiY;sb-exp=0"}
# UDP Logging, won't cause issues if the server is not available
# See https://github.com/nosial/LogLib2/blob/master/server.py for more information
LOGLIB_UDP_ENABLED: true
LOGLIB_UDP_HOST: 172.17.0.1
LOGLIB_UDP_PORT: 5131
healthcheck:
test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://teapot_socialbox:8085/}"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
teapot_mariadb:
container_name: teapot_socialbox_mariadb
image: mariadb:10.5
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox}
MYSQL_USER: ${MYSQL_USER:-socialbox}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
volumes:
- teapot_mariadb_data:/var/lib/mysql
networks:
- teapot_network
ports:
- "3307:3306" # Unique port for Teapot instance
expose:
- "3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "teapot_mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
teapot_redis:
container_name: teapot_socialbox_redis
image: redis:alpine
restart: unless-stopped
command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes
volumes:
- teapot_redis_data:/data
- ./docker/redis.conf:/usr/local/etc/redis/redis.conf
networks:
- teapot_network
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
volumes:
teapot_mariadb_data:
driver: local
teapot_redis_data:
driver: local
coffee_redis_data:
driver: local
coffee_mariadb_data:
driver: local
networks:
teapot_network:
driver: bridge
name: teapot_network
coffee_network:
driver: bridge
name: coffee_network
shared_network:
driver: bridge

109
docker-compose.yml Normal file
View file

@ -0,0 +1,109 @@
services:
socialbox:
container_name: socialbox
build:
context: .
dockerfile: Dockerfile
ports:
- "8085:8085"
depends_on:
mariadb:
condition: service_healthy
redis:
condition: service_healthy
networks:
- internal_network
restart: unless-stopped
volumes:
- ./socialbox/config:/etc/config
- ./socialbox/logs:/var/log
- ./socialbox/data:/etc/socialbox
environment:
# No need to change these values
LOG_LEVEL: ${LOG_LEVEL:-debug}
CONFIGLIB_PATH: /etc/config
LOGGING_DIRECTORY: /var/log
SB_MODE: automated
SB_STORAGE_PATH: /etc/socialbox
# Change these values to match your environment or update the .env file
SB_INSTANCE_NAME: ${SB_INSTANCE_NAME:-socialbox}
SB_INSTANCE_DOMAIN: ${SB_DOMAIN:-localhost}
SB_INSTANCE_RPC_ENDPOINT: ${SB_RPC_ENDPOINT:-http://127.0.0.0:8085/}
SB_LOGGING_CONSOLE_ENABLED: ${SB_LOGGING_CONSOLE_ENABLED:-true}
SB_LOGGING_CONSOLE_LEVEL: ${SB_LOGGING_CONSOLE_LEVEL:-info}
SB_LOGGING_FILE_ENABLED: ${SB_LOGGING_FILE_ENABLED:-true}
SB_LOGGING_FILE_LEVEL: ${SB_LOGGING_FILE_LEVEL:-error}
SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS: ${SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS:-false}
SB_CRYPTO_KEYPAIR_EXPIRES: ${SB_CRYPTO_KEYPAIR_EXPIRES}
SB_CRYPTO_ENCRYPTION_KEYS_COUNT: ${SB_CRYPTO_ENCRYPTION_KEYS_COUNT:-10}
SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM: ${SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM:-xchacha20}
SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM: ${SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM:-chacha20}
SB_DATABASE_HOST: mariadb
SB_DATABASE_USERNAME: ${MYSQL_USER:-socialbox}
SB_DATABASE_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
SB_DATABASE_NAME: ${MYSQL_DATABASE:-socialbox}
SB_CACHE_ENABLED: ${SB_CACHE_ENABLED:-true}
SB_CACHE_ENGINE: redis
SB_CACHE_HOST: redis
SB_CACHE_PORT: ${SB_CACHE_PORT:-6379}
SB_CACHE_USERNAME: ${SB_CACHE_USERNAME:-root}
SB_CACHE_PASSWORD: ${SB_CACHE_PASSWORD:-root}
SB_CACHE_DATABASE: ${SB_CACHE_DATABASE:-0}
healthcheck:
test: ["CMD", "curl", "-f", "-H", "Request-Type: ping", "${SB_INSTANCE_RPC_ENDPOINT-http://127.0.0.0:8085/}"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
mariadb:
container_name: socialbox_mariadb
image: mariadb:10.5
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-sb_root}
MYSQL_DATABASE: ${MYSQL_DATABASE:-socialbox}
MYSQL_USER: ${MYSQL_USER:-socialbox}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-socialbox}
volumes:
- mariadb_data:/var/lib/mysql
networks:
- internal_network
expose:
- "3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "mariadb", "-u", "${MYSQL_USER:-socialbox}", "-p${MYSQL_PASSWORD:-socialbox}"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
redis:
container_name: socialbox_redis
image: redis:alpine
restart: unless-stopped
command: redis-server /usr/local/etc/redis/redis.conf --appendonly yes
volumes:
- redis_data:/data
- ./docker/redis.conf:/usr/local/etc/redis/redis.conf
networks:
- internal_network
expose:
- "6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
start_period: 5s
volumes:
mariadb_data:
driver: local
redis_data:
driver: local
networks:
internal_network:
driver: bridge
name: socialbox_network

2
docker/docker.conf Normal file
View file

@ -0,0 +1,2 @@
[www]
clear_env = no

22
docker/entrypoint.sh Normal file
View file

@ -0,0 +1,22 @@
#!/bin/bash
# Banner with cool ASCII art
echo "███████╗ ██████╗ ██████╗██╗ █████╗ ██╗ ██████╗ ██████╗ ██╗ ██╗"
echo "██╔════╝██╔═══██╗██╔════╝██║██╔══██╗██║ ██╔══██╗██╔═══██╗╚██╗██╔╝"
echo "███████╗██║ ██║██║ ██║███████║██║ ██████╔╝██║ ██║ ╚███╔╝ "
echo "╚════██║██║ ██║██║ ██║██╔══██║██║ ██╔══██╗██║ ██║ ██╔██╗ "
echo "███████║╚██████╔╝╚██████╗██║██║ ██║███████╗██████╔╝╚██████╔╝██╔╝ ██╗"
echo "╚══════╝ ╚═════╝ ╚═════╝╚═╝╚═╝ ╚═╝╚══════╝╚═════╝ ╚═════╝ ╚═╝ ╚═╝"
# Check if the environment variable SB_MODE is set to "automated", if not exit.
if [ "$SB_MODE" != "automated" ]; then
echo "SB_MODE is not set to 'automated', exiting..."
exit 1
fi
# Initialize Socialbox
echo "Initializing Socialbox..."
/usr/bin/socialbox init --log-level=${LOG_LEVEL-INFO}
# Run supervisord, final command
/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

37
docker/nginx.conf Normal file
View file

@ -0,0 +1,37 @@
http {
include mime.types;
default_type application/octet-stream;
server {
listen 8085;
server_name localhost;
access_log /var/log/access.log;
error_log /var/log/error.log;
root /var/www/html;
index index.php;
# Handle all requests
location / {
try_files $uri $uri/ /index.php?$query_string =503;
autoindex off;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Block any .ht* files
location ~ /\.ht {
deny all;
}
}
}
events {
worker_connections 1024;
}

5
docker/redis.conf Normal file
View file

@ -0,0 +1,5 @@
bind 0.0.0.0
protected-mode yes
port 6379
appendonly yes
requirepass root

52
docker/supervisord.conf Normal file
View file

@ -0,0 +1,52 @@
[supervisord]
logfile=/var/logd.log
logfile_maxbytes=50MB
logfile_backups=10
loglevel=info
user=root
pidfile=/var/run/supervisord.pid
umask=022
nodaemon=true
minfds=1024
minprocs=200
[program:php-fpm]
command=/usr/local/sbin/php-fpm --nodaemonize
autostart=true
autorestart=true
priority=20
stdout_logfile=/var/log/fpm.log
stderr_logfile=/var/log/fpm_error.log
stdout_logfile_maxbytes=0
stdout_logfile_backups=5
stderr_logfile_maxbytes=0
stderr_logfile_backups=5
[program:php-fpm-log]
command=tail -f /var/log/fpm.log /var/log/fpm_error.log
stdout_events_enabled=true
stderr_events_enabled=true
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;" -c /etc/nginx/nginx.conf
autostart=true
autorestart=true
priority=10
stdout_logfile=/var/log/nginx.log
stderr_logfile=/var/log/nginx_error.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=5
[program:cron]
command=cron -f -L 15
autostart=true
autorestart=true
priority=30
stdout_logfile=/var/log/cron.log
stderr_logfile=/var/log/cron_error.log
stdout_logfile_maxbytes=20MB
stdout_logfile_backups=5
stderr_logfile_maxbytes=20MB
stderr_logfile_backups=5

10
docker/zz-docker.conf Normal file
View file

@ -0,0 +1,10 @@
[global]
daemonize=no
error_log = /var/log/php_fpm_error.log
log_limit = 1048576
log_level = notice
[www]
listen = 9000
catch_workers_output = yes
decorate_workers_output = no

View file

@ -1,6 +0,0 @@
<?php
require 'ncc';
import('net.nosial.socialbox');
\Socialbox\Socialbox::handleRpc();

View file

@ -36,14 +36,19 @@
"source": "nosial/libs.config=latest@n64" "source": "nosial/libs.config=latest@n64"
}, },
{ {
"name": "net.nosial.loglib", "name": "net.nosial.loglib2",
"version": "latest", "version": "latest",
"source": "nosial/libs.log=latest@n64" "source": "nosial/loglib2=latest@github"
}, },
{ {
"name": "net.nosial.optslib", "name": "net.nosial.optslib",
"version": "latest", "version": "latest",
"source": "nosial/libs.opts=latest@n64" "source": "nosial/libs.opts=latest@n64"
},
{
"name": "com.gregwar.captcha",
"version": "latest",
"source": "gregwar/captcha=latest@packagist"
} }
], ],
"configurations": [ "configurations": [
@ -88,7 +93,7 @@
"execute": { "execute": {
"working_directory": "%CWD%", "working_directory": "%CWD%",
"silent": false, "silent": false,
"tty": true, "tty": false,
"timeout": null, "timeout": null,
"idle_timeout": null, "idle_timeout": null,
"target": "main" "target": "main"

21
public/index.php Normal file
View file

@ -0,0 +1,21 @@
<?php
require 'ncc';
import('net.nosial.socialbox');
try
{
\Socialbox\Socialbox::handleRequest();
}
catch(Exception $e)
{
http_response_code(500);
if(\Socialbox\Classes\Configuration::getSecurityConfiguration()->isDisplayInternalExceptions())
{
print_r($e);
return;
}
print('An internal error occurred');
}

View file

@ -1,90 +1,90 @@
<?php <?php
namespace Socialbox\Abstracts; namespace Socialbox\Abstracts;
use RuntimeException; use RuntimeException;
use Socialbox\Classes\CacheLayer\MemcachedCacheLayer; use Socialbox\Classes\CacheLayer\MemcachedCacheLayer;
use Socialbox\Classes\CacheLayer\RedisCacheLayer; use Socialbox\Classes\CacheLayer\RedisCacheLayer;
use Socialbox\Classes\Configuration; use Socialbox\Classes\Configuration;
abstract class CacheLayer abstract class CacheLayer
{
private static ?CacheLayer $instance = null;
/**
* Stores a value in the cache with an associated key and an optional time-to-live (TTL).
*
* @param string $key The key under which the value is stored.
* @param mixed $value The value to be stored.
* @param int $ttl Optional. The time-to-live for the cache entry in seconds. A value of 0 indicates no expiration.
* @return bool Returns true if the value was successfully set, false otherwise.
*/
public abstract function set(string $key, mixed $value, int $ttl=0): bool;
/**
* Retrieves a value from the cache with the specified key.
*
* @param string $key The key of the value to retrieve.
* @return mixed The value associated with the key, or null if the key does not exist.
*/
public abstract function get(string $key): mixed;
/**
* Deletes a value from the cache with the specified key.
*
* @param string $key The key of the value to delete.
* @return bool Returns true if the value was successfully deleted, false otherwise.
*/
public abstract function delete(string $key): bool;
/**
* Checks if a value exists in the cache with the specified key.
*
* @param string $key The key to check.
* @return bool Returns true if the key exists, false otherwise.
*/
public abstract function exists(string $key): bool;
/**
* Counts the number of items that start with the given prefix.
*
* @param string $prefix The prefix to search for.
* @return int The count of items starting with the provided prefix.
*/
public abstract function getPrefixCount(string $prefix): int;
/**
* Clears all values from the cache.
*
* @return bool Returns true if the cache was successfully cleared, false otherwise.
*/
public abstract function clear(): bool;
/**
* Retrieves the singleton instance of the cache layer.
*
* @return CacheLayer The singleton instance of the cache layer.
*/
public static function getInstance(): CacheLayer
{ {
if (self::$instance === null) private static ?CacheLayer $instance = null;
/**
* Stores a value in the cache with an associated key and an optional time-to-live (TTL).
*
* @param string $key The key under which the value is stored.
* @param mixed $value The value to be stored.
* @param int $ttl Optional. The time-to-live for the cache entry in seconds. A value of 0 indicates no expiration.
* @return bool Returns true if the value was successfully set, false otherwise.
*/
public abstract function set(string $key, mixed $value, int $ttl=0): bool;
/**
* Retrieves a value from the cache with the specified key.
*
* @param string $key The key of the value to retrieve.
* @return mixed The value associated with the key, or null if the key does not exist.
*/
public abstract function get(string $key): mixed;
/**
* Deletes a value from the cache with the specified key.
*
* @param string $key The key of the value to delete.
* @return bool Returns true if the value was successfully deleted, false otherwise.
*/
public abstract function delete(string $key): bool;
/**
* Checks if a value exists in the cache with the specified key.
*
* @param string $key The key to check.
* @return bool Returns true if the key exists, false otherwise.
*/
public abstract function exists(string $key): bool;
/**
* Counts the number of items that start with the given prefix.
*
* @param string $prefix The prefix to search for.
* @return int The count of items starting with the provided prefix.
*/
public abstract function getPrefixCount(string $prefix): int;
/**
* Clears all values from the cache.
*
* @return bool Returns true if the cache was successfully cleared, false otherwise.
*/
public abstract function clear(): bool;
/**
* Retrieves the singleton instance of the cache layer.
*
* @return CacheLayer The singleton instance of the cache layer.
*/
public static function getInstance(): CacheLayer
{ {
$engine = Configuration::getConfiguration()['cache']['engine']; if (self::$instance === null)
{
$engine = Configuration::getCacheConfiguration()->getEngine();
if ($engine === 'redis') if ($engine === 'redis')
{ {
self::$instance = new RedisCacheLayer(); self::$instance = new RedisCacheLayer();
} }
else if ($engine === 'memcached') else if ($engine === 'memcached')
{ {
self::$instance = new MemcachedCacheLayer(); self::$instance = new MemcachedCacheLayer();
} }
else else
{ {
throw new RuntimeException('Invalid cache engine specified in the configuration, must be either "redis" or "memcached".'); throw new RuntimeException('Invalid cache engine specified in the configuration, must be either "redis" or "memcached".');
}
} }
return self::$instance;
} }
}
return self::$instance;
}
}

View file

@ -1,48 +1,21 @@
<?php <?php
namespace Socialbox\Abstracts; namespace Socialbox\Abstracts;
use Socialbox\Enums\StandardError; use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Exceptions\StandardException; use Socialbox\Objects\ClientRequest;
use Socialbox\Interfaces\SerializableInterface; use Socialbox\Objects\RpcRequest;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Objects\SessionRecord;
abstract class Method abstract class Method
{
/**
* Executes the method and returns RpcResponse/RpcError which implements SerializableInterface
*
* @param ClientRequest $request The full client request object, used to identify the client & it's requests
* @param RpcRequest $rpcRequest The selected RPC request for the method to handle
* @return SerializableInterface|null Returns RpcResponse/RpcError on success, null if the request is a notification
* @throws StandardException If a standard exception is thrown, it will be handled by the engine.
*/
public static abstract function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface;
/**
* @param ClientRequest $request The client request object
* @return SessionRecord|null Returns null if the client has not provided a Session UUID
* @throws StandardException Thrown if standard exceptions are to be thrown regarding this
*/
protected static function getSession(ClientRequest $request): ?SessionRecord
{ {
if($request->getSessionUuid() === null) /**
{ * Executes the method and returns RpcResponse/RpcError which implements SerializableInterface
return null; *
} * @param ClientRequest $request The full client request object, used to identify the client & it's requests
* @param RpcRequest $rpcRequest The selected RPC request for the method to handle
try * @return SerializableInterface|null Returns RpcResponse/RpcError on success, null if the request is a notification
{ * @throws StandardRpcException If a standard exception is thrown, it will be handled by the engine.
// NOTE: If the session UUID was provided, it has already been validated up until this point */
return SessionManager::getSession($request->getSessionUuid()); public static abstract function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface;
} }
catch(DatabaseOperationException $e)
{
throw new StandardException("There was an error while retrieving the session from the server", StandardError::INTERNAL_SERVER_ERROR);
}
}
}

View file

@ -1,114 +0,0 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
class Base32
{
/**
* Array with all 32 characters for decoding from/encoding to base32.
*/
private const LOOKUP_TABLE = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 7
'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 15
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 23
'Y', 'Z', '2', '3', '4', '5', '6', '7', // 31
'=' // padding char
];
/**
* Allowed padding lengths for base32 data.
*/
private const ALLOWED_PADDING = [6, 4, 3, 1, 0];
/**
* Decodes base32 data.
*
* @param string $data
* @return string
* @throws InvalidArgumentException
*/
public static function decode(string $data): string
{
if (empty($data))
{
throw new InvalidArgumentException('No data provided to decode');
}
$chars = self::LOOKUP_TABLE;
$chars_flipped = array_flip($chars);
$char_count = substr_count($data, $chars[32]);
if (!in_array($char_count, self::ALLOWED_PADDING, true))
{
throw new InvalidArgumentException(sprintf('Invalid padding in the base32 data: %s', $data));
}
for ($i = 0; $i < 4; ++$i)
{
if ($char_count === self::ALLOWED_PADDING[$i] &&
substr($data, -self::ALLOWED_PADDING[$i]) !== str_repeat($chars[32], self::ALLOWED_PADDING[$i]))
{
throw new InvalidArgumentException(sprintf('Invalid padding in the base32 data: %s', $data));
}
}
// Remove padding characters
$data = str_replace('=', (string)null, $data);
$data = str_split($data);
$binary_string = (string)null;
for ($i = 0, $data_count = count($data); $i < $data_count; $i += 8)
{
$x = (string)null;
for ($j = 0; $j < 8; ++$j)
{
$char = $data[$i + $j] ?? null;
if ($char === null || !isset($chars_flipped[$char]))
{
throw new InvalidArgumentException(sprintf('Invalid character in the base32 data: %s', $char));
}
$x .= str_pad(base_convert((string)$chars_flipped[$char], 10, 2), 5, '0', STR_PAD_LEFT);
}
$eight_bits = str_split($x, 8);
foreach ($eight_bits as $bits)
{
$binary_string .= ($y = chr((int)base_convert($bits, 2, 10))) !== false ? $y : (string)null;
}
}
return $binary_string;
}
public static function encode(string $data): string
{
$binaryLength = strlen($data);
$base32String = '';
$buffer = 0;
$bitsLeft = 0;
for ($i = 0; $i < $binaryLength; $i++)
{
$buffer = ($buffer << 8) | (ord($data[$i]) & 0xFF);
$bitsLeft += 8;
while ($bitsLeft >= 5)
{
$base32String .= self::LOOKUP_TABLE[($buffer >> ($bitsLeft - 5)) & 0x1F];
$bitsLeft -= 5;
}
}
if ($bitsLeft > 0)
{
$base32String .= self::LOOKUP_TABLE[($buffer << (5 - $bitsLeft)) & 0x1F];
}
return $base32String;
}
}

View file

@ -22,10 +22,10 @@ class MemcachedCacheLayer extends CacheLayer
} }
$this->memcached = new Memcached(); $this->memcached = new Memcached();
$this->memcached->addServer(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']); $this->memcached->addServer(Configuration::getCacheConfiguration()->getHost(), Configuration::getCacheConfiguration()->getPort());
if(Configuration::getConfiguration()['cache']['username'] !== null || Configuration::getConfiguration()['cache']['password'] !== null) if(Configuration::getCacheConfiguration()->getUsername() !== null || Configuration::getCacheConfiguration()->getPassword() !== null)
{ {
$this->memcached->setSaslAuthData(Configuration::getConfiguration()['cache']['username'], Configuration::getConfiguration()['cache']['password']); $this->memcached->setSaslAuthData(Configuration::getCacheConfiguration()->getUsername(), Configuration::getCacheConfiguration()->getPassword());
} }
} }

View file

@ -26,15 +26,15 @@ class RedisCacheLayer extends CacheLayer
try try
{ {
$this->redis->connect(Configuration::getConfiguration()['cache']['host'], (int)Configuration::getConfiguration()['cache']['port']); $this->redis->connect(Configuration::getCacheConfiguration()->getHost(), Configuration::getCacheConfiguration()->getPort());
if (Configuration::getConfiguration()['cache']['password'] !== null) if (Configuration::getCacheConfiguration()->getPassword() !== null)
{ {
$this->redis->auth(Configuration::getConfiguration()['cache']['password']); $this->redis->auth(Configuration::getCacheConfiguration()->getPassword());
} }
if (Configuration::getConfiguration()['cache']['database'] !== 0) if (Configuration::getCacheConfiguration()->getDatabase() !== null)
{ {
$this->redis->select((int)Configuration::getConfiguration()['cache']['database']); $this->redis->select(Configuration::getCacheConfiguration()->getDatabase());
} }
} }
catch (RedisException $e) catch (RedisException $e)

View file

@ -0,0 +1,41 @@
<?php
namespace Socialbox\Classes\CliCommands;
use Socialbox\Classes\Configuration;
use Socialbox\Classes\Logger;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Socialbox;
class DnsRecordCommand implements CliCommandInterface
{
/**
* @inheritDoc
*/
public static function execute(array $args): int
{
Logger::getLogger()->info('Please set the following DNS TXT record for the domain:');
Logger::getLogger()->info(sprintf(' %s', Socialbox::getDnsRecord()));
return 0;
}
/**
* @inheritDoc
*/
public static function getHelpMessage(): string
{
return <<<HELP
Usage: socialbox dns-record
Displays the DNS TXT record that should be set for the domain.
HELP;
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return 'Displays the DNS TXT record that should be set for the domain.';
}
}

View file

@ -1,111 +1,472 @@
<?php <?php
namespace Socialbox\Classes\CliCommands; namespace Socialbox\Classes\CliCommands;
use Exception; use Exception;
use LogLib\Log; use ncc\CLI\Main;
use PDOException; use ncc\ThirdParty\Symfony\Process\Exception\InvalidArgumentException;
use Socialbox\Abstracts\CacheLayer; use PDOException;
use Socialbox\Classes\Configuration; use Socialbox\Abstracts\CacheLayer;
use Socialbox\Classes\Cryptography; use Socialbox\Classes\Configuration;
use Socialbox\Classes\Database; use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Resources; use Socialbox\Classes\Database;
use Socialbox\Enums\DatabaseObjects; use Socialbox\Classes\Logger;
use Socialbox\Exceptions\DatabaseOperationException; use Socialbox\Classes\Resources;
use Socialbox\Interfaces\CliCommandInterface; use Socialbox\Enums\DatabaseObjects;
use Socialbox\Managers\VariableManager; use Socialbox\Exceptions\CryptographyException;
use Socialbox\Interfaces\CliCommandInterface;
use Socialbox\Program;
use Socialbox\Socialbox;
class InitializeCommand implements CliCommandInterface class InitializeCommand implements CliCommandInterface
{
/**
* @inheritDoc
*/
public static function execute(array $args): int
{ {
if(Configuration::getConfiguration()['instance']['enabled'] === false && !isset($args['force'])) /**
* @inheritDoc
*/
public static function execute(array $args): int
{ {
Log::info('net.nosial.socialbox', 'Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration'); if(Configuration::getInstanceConfiguration()->isEnabled() === false && !isset($args['force']) && getenv('SB_MODE') !== 'automated')
return 1;
}
Log::info('net.nosial.socialbox', 'Initializing Socialbox...');
if(Configuration::getConfiguration()['cache']['enabled'])
{
Log::verbose('net.nosial.socialbox', 'Clearing cache layer...');
CacheLayer::getInstance()->clear();
}
foreach(DatabaseObjects::casesOrdered() as $object)
{
Log::verbose('net.nosial.socialbox', "Initializing database object {$object->value}");
try
{ {
Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object))); $required_configurations = [
} 'database.host', 'database.port', 'database.username', 'database.password', 'database.name',
catch (PDOException $e) 'instance.enabled', 'instance.domain', 'registration.*'
{ ];
// Check if the error code is for "table already exists"
if ($e->getCode() === '42S01') Program::getLogger()->error('Socialbox is disabled. Use --force to initialize the instance or set `instance.enabled` to True in the configuration');
Program::getLogger()->info('The reason you are required to do this is to allow you to configure the instance before enabling it');
Program::getLogger()->info('The following configurations are required to be set before enabling the instance:');
foreach($required_configurations as $config)
{ {
Log::warning('net.nosial.socialbox', "Database object {$object->value} already exists, skipping..."); Program::getLogger()->info(sprintf(' - %s', $config));
continue; }
Program::getLogger()->info('instance.private_key & instance.public_key are automatically generated if not set');
Program::getLogger()->info('instance.domain is required to be set to the domain name of the instance');
Program::getLogger()->info('instance.rpc_endpoint is required to be set to the publicly accessible http rpc endpoint of this server');
Program::getLogger()->info('registration.* are required to be set to allow users to register to the instance');
Program::getLogger()->info('You will be given a DNS TXT record to set for the public key after the initialization process');
Program::getLogger()->info('The configuration file can be edited using ConfigLib:');
Program::getLogger()->info(' configlib --conf socialbox -e nano');
Program::getLogger()->info('Or manually at:');
Program::getLogger()->info(sprintf(' %s', Configuration::getConfigurationLib()->getPath()));
if(getenv('SB_MODE') === 'automated')
{
// Wait & Reload the configuration
while(!Configuration::getInstanceConfiguration()->isEnabled())
{
Program::getLogger()->info('Waiting for configuration, retrying in 5 seconds...');
sleep(5);
Configuration::reload();
}
} }
else else
{ {
Log::error('net.nosial.socialbox', "Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); return 1;
}
return 1;
}
// Overwrite the configuration if the automated setup procedure is detected
// This is useful for CI/CD pipelines & Docker
if(getenv('SB_MODE') === 'automated')
{
Program::getLogger()->info('Automated Setup Procedure is detected');
self::applyEnvironmentVariables();
}
if(Configuration::getInstanceConfiguration()->getDomain() === null)
{
Program::getLogger()->error('instance.domain is required but was not set');
return 1;
}
if(Configuration::getInstanceConfiguration()->getRpcEndpoint() === null)
{
Program::getLogger()->error('instance.rpc_endpoint is required but was not set');
return 1;
}
Program::getLogger()->info('Initializing Socialbox...');
if(Configuration::getCacheConfiguration()->isEnabled())
{
Program::getLogger()->verbose('Clearing cache layer...');
CacheLayer::getInstance()->clear();
}
foreach(DatabaseObjects::casesOrdered() as $object)
{
Program::getLogger()->verbose("Initializing database object {$object->value}");
try
{
Database::getConnection()->exec(file_get_contents(Resources::getDatabaseResource($object)));
}
catch (PDOException $e)
{
// Check if the error code is for "table already exists"
if ($e->getCode() === '42S01')
{
Program::getLogger()->warning("Database object {$object->value} already exists, skipping...");
continue;
}
else
{
Program::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1;
}
}
catch(Exception $e)
{
Program::getLogger()->error("Failed to initialize database object {$object->value}: {$e->getMessage()}", $e);
return 1; return 1;
} }
} }
catch(Exception $e)
if(
!Configuration::getCryptographyConfiguration()->getHostPublicKey() ||
!Configuration::getCryptographyConfiguration()->getHostPrivateKey() ||
!Configuration::getCryptographyConfiguration()->getHostPublicKey()
)
{ {
Log::error('net.nosial.socialbox', "Failed to initialize database object {$object->value}: {$e->getMessage()}", $e); $expires = time() + 31536000;
return 1;
try
{
Program::getLogger()->info('Generating new key pair (expires ' . date('Y-m-d H:i:s', $expires) . ')...');
$signingKeyPair = Cryptography::generateSigningKeyPair();
}
catch (CryptographyException $e)
{
Program::getLogger()->error('Failed to generate cryptography values', $e);
return 1;
}
Configuration::getConfigurationLib()->set('cryptography.host_keypair_expires', $expires);
Configuration::getConfigurationLib()->set('cryptography.host_private_key', $signingKeyPair->getPrivateKey());
Configuration::getConfigurationLib()->set('cryptography.host_public_key', $signingKeyPair->getPublicKey());
} }
}
try // If Internal Encryption keys are null or has less keys than configured, populate the configuration
{ // property with encryption keys.
if(
if(!VariableManager::variableExists('PUBLIC_KEY') || !VariableManager::variableExists('PRIVATE_KEY')) Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() === null ||
count(Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys()) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount())
{ {
Log::info('net.nosial.socialbox', 'Generating new key pair...'); Program::getLogger()->info('Generating internal encryption keys...');
$encryptionKeys = Configuration::getCryptographyConfiguration()->getInternalEncryptionKeys() ?? [];
while(count($encryptionKeys) < Configuration::getCryptographyConfiguration()->getEncryptionKeysCount())
{
$encryptionKeys[] = Cryptography::generateEncryptionKey(Configuration::getCryptographyConfiguration()->getEncryptionKeysAlgorithm());
}
$keyPair = Cryptography::generateKeyPair(); Configuration::getConfigurationLib()->set('cryptography.internal_encryption_keys', $encryptionKeys);
VariableManager::setVariable('PUBLIC_KEY', $keyPair->getPublicKey());
VariableManager::setVariable('PRIVATE_KEY', $keyPair->getPrivateKey());
Log::info('net.nosial.socialbox', 'Set the DNS TXT record for the public key to the following value:');
Log::info('net.nosial.socialbox', "socialbox-key={$keyPair->getPublicKey()}");
} }
Program::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->save();
Configuration::reload();
Program::getLogger()->info('Socialbox has been initialized successfully');
Program::getLogger()->info(sprintf('Set the DNS TXT record for the domain %s to the following value:', Configuration::getInstanceConfiguration()->getDomain()));
Program::getLogger()->info(Socialbox::getDnsRecord());
return 0;
} }
catch(DatabaseOperationException $e)
/**
* Applies environment variables to the application's configuration system.
* This method maps predefined environment variables to their corresponding
* configuration keys, validates their values, and updates the configuration
* library accordingly. If expected environment variables are missing and
* critical for certain components, warning logs are generated.
* Additionally, the configuration changes are saved and reloaded after being applied.
*
* @return void
*/
private static function applyEnvironmentVariables(): void
{ {
Log::error('net.nosial.socialbox', "Failed to generate key pair: {$e->getMessage()}", $e); // Always set the 'instance.enabled' to true if the automated setup procedure is detected
return 1; Configuration::getConfigurationLib()->set('instance.enabled', true);
$configurationMap = [
// Instance Configuration
'SB_INSTANCE_NAME' => 'instance.name',
'SB_INSTANCE_DOMAIN' => 'instance.domain',
'SB_INSTANCE_RPC_ENDPOINT' => 'instance.rpc_endpoint',
'SB_STORAGE_PATH' => 'storage.path',
// Logging Configuration
'SB_LOGGING_CONSOLE_ENABLED' => 'logging.console_logging_enabled',
'SB_LOGGING_CONSOLE_LEVEL' => 'logging.console_logging_level',
'SB_LOGGING_FILE_ENABLED' => 'logging.file_logging_enabled',
'SB_LOGGING_FILE_LEVEL' => 'logging.file_logging_level',
// Security & Cryptography Configuration
'SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS' => 'security.display_internal_exceptions',
'SB_CRYPTO_KEYPAIR_EXPIRES' => 'cryptography.host_keypair_expires',
'SB_CRYPTO_ENCRYPTION_KEYS_COUNT' => 'cryptography.encryption_keys_count',
'SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM' => 'cryptography.encryption_keys_algorithm',
'SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM' => 'cryptography.transport_encryption_algorithm',
// Database Configuration
'SB_DATABASE_HOST' => 'database.host',
'SB_DATABASE_PORT' => 'database.port',
'SB_DATABASE_USERNAME' => 'database.username',
'SB_DATABASE_PASSWORD' => 'database.password',
'SB_DATABASE_NAME' => 'database.name',
'SB_CACHE_ENABLED' => 'cache.enabled',
'SB_CACHE_ENGINE' => 'cache.engine',
'SB_CACHE_HOST' => 'cache.host',
'SB_CACHE_PORT' => 'cache.port',
'SB_CACHE_USERNAME' => 'cache.username',
'SB_CACHE_PASSWORD' => 'cache.password',
'SB_CACHE_DATABASE' => 'cache.database',
];
foreach($configurationMap as $env => $config)
{
$variable = getenv($env);
Program::getLogger()->info(sprintf('Checking environment variable %s...', $env));
switch($env)
{
case 'SB_STORAGE_PATH':
case 'SB_LOGGING_FILE_LEVEL':
case 'SB_LOGGING_CONSOLE_LEVEL':
case 'SB_INSTANCE_NAME':
case 'SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM':
case 'SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM':
case 'SB_CACHE_ENGINE':
case 'SB_CACHE_HOST':
case 'SB_CACHE_USERNAME':
case 'SB_CACHE_PASSWORD':
case 'SB_CACHE_DATABASE':
if($variable !== false)
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_INSTANCE_DOMAIN':
if($variable === false && Configuration::getInstanceConfiguration()->getDomain() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_DATABASE_HOST':
if($variable === false && Configuration::getDatabaseConfiguration()->getHost() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_DATABASE_PORT':
if($variable === false && Configuration::getDatabaseConfiguration()->getPort() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, (int) $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_DATABASE_USERNAME':
if($variable === false && Configuration::getDatabaseConfiguration()->getUsername() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_DATABASE_PASSWORD':
if($variable === false && Configuration::getDatabaseConfiguration()->getPassword() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_DATABASE_NAME':
if($variable === false && Configuration::getDatabaseConfiguration()->getName() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_INSTANCE_RPC_ENDPOINT':
if($variable === false && Configuration::getInstanceConfiguration()->getRpcEndpoint() === null)
{
Program::getLogger()->warning(sprintf('%s is not set, expected %s environment variable', $config, $env));
}
else
{
Configuration::getConfigurationLib()->set($config, $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_LOGGING_CONSOLE_ENABLED':
case 'SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS':
case 'SB_LOGGING_FILE_ENABLED':
case 'SB_CACHE_ENABLED':
if($variable !== false)
{
Configuration::getConfigurationLib()->set($config, filter_var($variable, FILTER_VALIDATE_BOOLEAN));
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
case 'SB_CRYPTO_KEYPAIR_EXPIRES':
case 'SB_CRYPTO_ENCRYPTION_KEYS_COUNT':
case 'SB_CACHE_PORT':
if($variable !== false)
{
Configuration::getConfigurationLib()->set($config, (int) $variable);
Program::getLogger()->info(sprintf('Set %s to %s', $config, $variable));
}
break;
default:
Program::getLogger()->warning("Environment variable $env is not supported");
break;
}
}
// Handle Mock Servers environment variables (SB_INSTANCE_DNS_MOCK_*)
$mockServers = [];
foreach(self::getMockServerValues() as $mockServer)
{
$mockServer = explode(' ', $mockServer);
if(count($mockServer) !== 2)
{
Program::getLogger()->warning(sprintf('Invalid DNS Mock Server format: %s', implode(' ', $mockServer)));
continue;
}
$domain = $mockServer[0] ?? null;
$txt = $mockServer[1] ?? null;
if($domain === null || $txt === null)
{
Program::getLogger()->warning(sprintf('Invalid DNS Mock Server format, domain or txt missing: %s', implode(' ', $mockServer)));
continue;
}
try
{
$mockServers[$domain] = $txt;
Program::getLogger()->info(sprintf('Added Mock Server %s: %s', $domain, $txt));
}
catch(InvalidArgumentException $e)
{
Program::getLogger()->error(sprintf('Invalid TXT record format for %s', $domain), $e);
continue;
}
}
if(count($mockServers) > 0)
{
Program::getLogger()->info('Setting Mock Servers...');
Configuration::getConfigurationLib()->set('instance.dns_mocks', $mockServers);
}
// Apply changes & reload the configuration
Program::getLogger()->info('Updating configuration...');
Configuration::getConfigurationLib()->save(); // Save
Configuration::reload(); // Reload
} }
Log::info('net.nosial.socialbox', 'Socialbox has been initialized successfully'); /**
return 0; * Retrieves all environment variable values that start with the prefix 'SB_INSTANCE_DNS_MOCK_'.
} *
* @return array An array of environment variable values filtered by the specified prefix.
*/
private static function getMockServerValues(): array
{
// Fetch all environment variables
$envVars = getenv();
/** // Filter variables that start with the specified prefix
* @inheritDoc $filtered = array_filter($envVars, function ($key)
*/ {
public static function getHelpMessage(): string return str_starts_with($key, 'SB_INSTANCE_DNS_MOCK_');
{ }, ARRAY_FILTER_USE_KEY);
return "Initialize Command - Initializes Socialbox for first-runs\n" .
"Usage: socialbox init [arguments]\n\n" .
"Arguments:\n" .
" --force - Forces the initialization process to run even the instance is disabled\n";
}
/** // Return only the values as an array
* @inheritDoc return array_values($filtered);
*/ }
public static function getShortHelpMessage(): string
{ /**
return "Initializes Socialbox for first-runs"; * @inheritDoc
} */
} public static function getHelpMessage(): string
{
return "Initialize Command - Initializes Socialbox for first-runs\n" .
"Usage: socialbox init [arguments]\n\n" .
"Arguments:\n" .
" --force - Forces the initialization process to run even the instance is disabled\n\n" .
"Environment Variables:\n" .
" SB_MODE - Set to 'automated' to enable automated setup procedure (Must be set to enable environment variables)\n" .
" SB_INSTANCE_DOMAIN - The domain name of the instance (eg; Socialbox)\n" .
" SB_INSTANCE_RPC_ENDPOINT - The public RPC endpoint of the instance (eg; https://rpc.teapot.com/)\n" .
" SB_STORAGE_PATH - The path to store files (default: /etc/socialbox)\n" .
" SB_LOGGING_CONSOLE_ENABLED - Enable console logging (default: true)\n" .
" SB_LOGGING_CONSOLE_LEVEL - Console logging level (default: info)\n" .
" SB_LOGGING_FILE_ENABLED - Enable file logging (default: true)\n" .
" SB_LOGGING_FILE_LEVEL - File logging level (default: error)\n" .
" SB_SECURITY_DISPLAY_INTERNAL_EXCEPTIONS - Display internal exceptions (default: false)\n" .
" SB_CRYPTO_KEYPAIR_EXPIRES - The expiration date of the key pair in Unix timestamp (default: current time + 1 year)\n" .
" SB_CRYPTO_ENCRYPTION_KEYS_COUNT - The number of internal encryption keys to generate (default: 5)\n" .
" SB_CRYPTO_ENCRYPTION_KEYS_ALGORITHM - The algorithm to use for encryption keys (default: xchacha20)\n" .
" SB_CRYPTO_TRANSPORT_ENCRYPTION_ALGORITHM - The algorithm to use for transport encryption (default: chacha20)\n" .
" SB_DATABASE_HOST - The database host (default: localhost)\n" .
" SB_DATABASE_PORT - The database port (default: 3306)\n" .
" SB_DATABASE_USERNAME - The database username (default: root)\n" .
" SB_DATABASE_PASSWORD - The database password (default: null)\n" .
" SB_DATABASE_NAME - The database name (default: socialbox)\n" .
" SB_CACHE_ENABLED - Enable cache layer (default: false)\n" .
" SB_CACHE_ENGINE - The cache engine to use (default: redis)\n" .
" SB_CACHE_HOST - The cache host (default: localhost)\n" .
" SB_CACHE_PORT - The cache port (default: 6379)\n" .
" SB_CACHE_USERNAME - The cache username (default: null)\n" .
" SB_CACHE_PASSWORD - The cache password (default: null)\n" .
" SB_CACHE_DATABASE - The cache database (default: 0)\n" .
" SB_INSTANCE_DNS_MOCK_* - Mock server environment variables, format: (<domain> <txt>), eg; SB_INSTANCE_DNS_MOCK_N64: teapot.com <txt>\n";
}
/**
* @inheritDoc
*/
public static function getShortHelpMessage(): string
{
return "Initializes Socialbox for first-runs";
}
}

View file

@ -1,28 +1,113 @@
<?php <?php
namespace Socialbox\Classes; namespace Socialbox\Classes;
class Configuration use Socialbox\Classes\Configuration\AuthenticationConfiguration;
{ use Socialbox\Classes\Configuration\CacheConfiguration;
private static ?array $configuration = null; use Socialbox\Classes\Configuration\CryptographyConfiguration;
use Socialbox\Classes\Configuration\DatabaseConfiguration;
use Socialbox\Classes\Configuration\InstanceConfiguration;
use Socialbox\Classes\Configuration\LoggingConfiguration;
use Socialbox\Classes\Configuration\PoliciesConfiguration;
use Socialbox\Classes\Configuration\RegistrationConfiguration;
use Socialbox\Classes\Configuration\SecurityConfiguration;
use Socialbox\Classes\Configuration\StorageConfiguration;
public static function getConfiguration(): array class Configuration
{ {
if(self::$configuration === null) private static ?\ConfigLib\Configuration $configuration = null;
private static ?InstanceConfiguration $instanceConfiguration = null;
private static ?SecurityConfiguration $securityConfiguration = null;
private static ?CryptographyConfiguration $cryptographyConfiguration = null;
private static ?DatabaseConfiguration $databaseConfiguration = null;
private static ?LoggingConfiguration $loggingConfiguration = null;
private static ?CacheConfiguration $cacheConfiguration = null;
private static ?RegistrationConfiguration $registrationConfiguration = null;
private static ?AuthenticationConfiguration $authenticationConfiguration = null;
private static ?PoliciesConfiguration $policiesConfiguration = null;
private static ?StorageConfiguration $storageConfiguration = null;
/**
* Initializes the configuration settings for the application. This includes
* settings for the instance, security, database, cache layer, and registration.
*
* @return void
*/
private static function initializeConfiguration(): void
{ {
$config = new \ConfigLib\Configuration('socialbox'); $config = new \ConfigLib\Configuration('socialbox');
// False by default, requires the user to enable it. // Instance configuration
$config->setDefault('instance.enabled', false); $config->setDefault('instance.enabled', false); // False by default, requires the user to enable it.
$config->setDefault('instance.name', "Socialbox Server");
$config->setDefault('instance.domain', null);
$config->setDefault('instance.rpc_endpoint', null);
// DNS Mocking Configuration, usually used for testing purposes
// Allows the user to mock a domain to use a specific TXT record
$config->setDefault('instance.dns_mocks', []);
// Security Configuration
$config->setDefault('security.display_internal_exceptions', false); $config->setDefault('security.display_internal_exceptions', false);
$config->setDefault('security.resolved_servers_ttl', 600);
$config->setDefault('security.captcha_ttl', 200);
// Server-side OTP Security options
// The time step in seconds for the OTP generation
// Default: 30 seconds
$config->setDefault('security.otp_secret_key_length', 32);
$config->setDefault('security.otp_time_step', 30);
// The number of digits in the OTP
$config->setDefault('security.otp_digits', 6);
// The hash algorithm to use for the OTP generation (sha1, sha256, sha512)
$config->setDefault('security.otp_hash_algorithm', 'sha512');
// The window of time steps to allow for OTP verification
$config->setDefault('security.otp_window', 1);
// Cryptography Configuration
// The Unix Timestamp for when the host's keypair should expire
// Setting this value to 0 means the keypair never expires
// Setting this value to null will automatically set the current unix timestamp + 1 year as the value
// This means at initialization, the key is automatically set to expire in a year.
$config->setDefault('cryptography.host_keypair_expires', null);
// The host's public/private keypair in base64 encoding, when null; the initialization process
// will automatically generate a new keypair
$config->setDefault('cryptography.host_public_key', null);
$config->setDefault('cryptography.host_private_key', null);
// The internal encryption keys used for encrypting data in the database when needed.
// When null, the initialization process will automatically generate a set of keys
// based on the `encryption_keys_count` and `encryption_keys_algorithm` configuration.
// This is an array of base64 encoded keys.
$config->setDefault('cryptography.internal_encryption_keys', null);
// The number of encryption keys to generate and set to `instance.encryption_keys` this will be used
// to randomly encrypt/decrypt sensitive data in the database, this includes hashes.
// The higher the number the higher performance impact it will have on the server
$config->setDefault('cryptography.encryption_keys_count', 10);
// The host's encryption algorithm, this will be used to generate a set of encryption keys
// This is for internal encryption, these keys are never shared outside this configuration.
// Recommendation: Higher security over performance
$config->setDefault('cryptography.encryption_keys_algorithm', 'xchacha20');
// The encryption algorithm to use for encrypted message transport between the client aand the server
// This is the encryption the server tells the client to use and the client must support it.
// Recommendation: Good balance between security and performance
// For universal support & performance, use aes256gcm for best performance or for best security use xchacha20
$config->setDefault('cryptography.transport_encryption_algorithm', 'chacha20');
// Database configuration
$config->setDefault('database.host', '127.0.0.1'); $config->setDefault('database.host', '127.0.0.1');
$config->setDefault('database.port', 3306); $config->setDefault('database.port', 3306);
$config->setDefault('database.username', 'root'); $config->setDefault('database.username', 'root');
$config->setDefault('database.password', 'root'); $config->setDefault('database.password', 'root');
$config->setDefault('database.name', 'test'); $config->setDefault('database.name', 'test');
// Logging configuration
$config->setDefault('logging.console_logging_enabled', true);
$config->setDefault('logging.console_logging_level', 'info');
$config->setDefault('logging.file_logging_enabled', true);
$config->setDefault('logging.file_logging_level', 'error');
// Cache layer configuration
$config->setDefault('cache.enabled', false); $config->setDefault('cache.enabled', false);
$config->setDefault('cache.engine', 'redis'); $config->setDefault('cache.engine', 'redis');
$config->setDefault('cache.host', '127.0.0.1'); $config->setDefault('cache.host', '127.0.0.1');
@ -30,15 +115,308 @@ class Configuration
$config->setDefault('cache.username', null); $config->setDefault('cache.username', null);
$config->setDefault('cache.password', null); $config->setDefault('cache.password', null);
$config->setDefault('cache.database', 0); $config->setDefault('cache.database', 0);
$config->setDefault('cache.variables.enabled', true); $config->setDefault('cache.sessions.enabled', true);
$config->setDefault('cache.variables.ttl', 3600); $config->setDefault('cache.sessions.ttl', 3600);
$config->setDefault('cache.variables.max', 1000); $config->setDefault('cache.sessions.max', 1000);
// Registration configuration
$config->setDefault('registration.enabled', true);
$config->setDefault('registration.privacy_policy_document', null);
$config->setDefault('registration.privacy_policy_date', 1734985525);
$config->setDefault('registration.accept_privacy_policy', true);
$config->setDefault('registration.terms_of_service_document', null);
$config->setDefault('registration.terms_of_service_date', 1734985525);
$config->setDefault('registration.accept_terms_of_service', true);
$config->setDefault('registration.community_guidelines_document', null);
$config->setDefault('registration.community_guidelines_date', 1734985525);
$config->setDefault('registration.accept_community_guidelines', true);
$config->setDefault('registration.password_required', true);
$config->setDefault('registration.otp_required', false);
$config->setDefault('registration.display_name_required', true);
$config->setDefault('registration.first_name_required', false);
$config->setDefault('registration.middle_name_required', false);
$config->setDefault('registration.last_name_required', false);
$config->setDefault('registration.display_picture_required', false);
$config->setDefault('registration.email_address_required', false);
$config->setDefault('registration.phone_number_required', false);
$config->setDefault('registration.birthday_required', false);
$config->setDefault('registration.url_required', false);
$config->setDefault('registration.image_captcha_verification_required', true);
// Authentication configuration
$config->setDefault('authentication.enabled', true);
$config->setDefault('authentication.image_captcha_verification_required', true);
// Server Policies
// The maximum number of signing keys a peer can register onto the server at once
$config->setDefault('policies.max_signing_keys', 20);
$config->setDefault('policies.max_contact_signing_keys', 50);
// The amount of time in seconds it takes before a session is considered expired due to inactivity
// Default: 12hours
$config->setDefault('policies.session_inactivity_expires', 43200);
// The amount of time in seconds it takes before an image captcha is considered expired due to lack of
// answer within the time-frame that the captcha was generated
// If expired; client is expected to request for a new captcha which will generate a new random answer.
$config->setDefault('policies.image_captcha_expires', 300);
// The amount of time in seconds it takes before a peer's external address is resolved again
// When a peer's external address is resolved, it is cached for this amount of time before resolving again.
// This reduces the amount of times a resolution request is made to the external server.
$config->setDefault('policies.peer_sync_interval', 3600);
// The maximum number of contacts a peer can retrieve from the server at once, if the client puts a
// value that exceeds this limit, the server will use this limit instead.
// recommendation: 100
$config->setDefault('policies.get_contacts_limit', 100);
$config->setDefault('policies.get_encryption_channel_requests_limit', 100);
$config->setDefault('policies.get_encryption_channels_limit', 100);
$config->setDefault('policies.get_encryption_channel_incoming_limit', 100);
$config->setDefault('policies.get_encryption_channel_outgoing_limit', 100);
// Default privacy states for information fields associated with the peer
$config->setDefault('policies.default_display_picture_privacy', 'PUBLIC');
$config->setDefault('policies.default_first_name_privacy', 'CONTACTS');
$config->setDefault('policies.default_middle_name_privacy', 'PRIVATE');
$config->setDefault('policies.default_last_name_privacy', 'PRIVATE');
$config->setDefault('policies.default_email_address_privacy', 'CONTACTS');
$config->setDefault('policies.default_phone_number_privacy', 'CONTACTS');
$config->setDefault('policies.default_birthday_privacy', 'PRIVATE');
$config->setDefault('policies.default_url_privacy', 'PUBLIC');
// Storage configuration
$config->setDefault('storage.path', '/etc/socialbox'); // The main path for file storage
$config->setDefault('storage.user_display_images_path', 'user_profiles'); // eg; `/etc/socialbox/user_profiles`
$config->setDefault('storage.user_display_images_max_size', 3145728); // 3MB
$config->save(); $config->save();
self::$configuration = $config->getConfiguration(); self::$configuration = $config;
self::$instanceConfiguration = new InstanceConfiguration(self::$configuration->getConfiguration()['instance']);
self::$securityConfiguration = new SecurityConfiguration(self::$configuration->getConfiguration()['security']);
self::$cryptographyConfiguration = new CryptographyConfiguration(self::$configuration->getConfiguration()['cryptography']);
self::$databaseConfiguration = new DatabaseConfiguration(self::$configuration->getConfiguration()['database']);
self::$loggingConfiguration = new LoggingConfiguration(self::$configuration->getConfiguration()['logging']);
self::$cacheConfiguration = new CacheConfiguration(self::$configuration->getConfiguration()['cache']);
self::$registrationConfiguration = new RegistrationConfiguration(self::$configuration->getConfiguration()['registration']);
self::$authenticationConfiguration = new AuthenticationConfiguration(self::$configuration->getConfiguration()['authentication']);
self::$policiesConfiguration = new PoliciesConfiguration(self::$configuration->getConfiguration()['policies']);
self::$storageConfiguration = new StorageConfiguration(self::$configuration->getConfiguration()['storage']);
} }
return self::$configuration; /**
} * Resets all configuration instances by setting them to null and then
} * reinitializes the configurations.
*
* @return void
*/
public static function reload(): void
{
self::$configuration = null;
self::$instanceConfiguration = null;
self::$securityConfiguration = null;
self::$databaseConfiguration = null;
self::$loggingConfiguration = null;
self::$cacheConfiguration = null;
self::$registrationConfiguration = null;
self::initializeConfiguration();
}
/**
* Retrieves the current configuration array. If the configuration is not initialized,
* it triggers the initialization process.
*
* @return array The current configuration array.
*/
public static function getConfiguration(): array
{
if(self::$configuration === null)
{
self::initializeConfiguration();
}
return self::$configuration->getConfiguration();
}
/**
* Retrieves the configuration library instance.
*
* This method returns the current Configuration instance from the ConfigLib namespace.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return \ConfigLib\Configuration The configuration library instance.
*/
public static function getConfigurationLib(): \ConfigLib\Configuration
{
if(self::$configuration === null)
{
self::initializeConfiguration();
}
return self::$configuration;
}
/**
* Retrieves the current instance configuration.
*
* @return InstanceConfiguration The current instance configuration instance.
*/
public static function getInstanceConfiguration(): InstanceConfiguration
{
if(self::$instanceConfiguration === null)
{
self::initializeConfiguration();
}
return self::$instanceConfiguration;
}
/**
* Retrieves the current security configuration.
*
* @return SecurityConfiguration The current security configuration instance.
*/
public static function getSecurityConfiguration(): SecurityConfiguration
{
if(self::$securityConfiguration === null)
{
self::initializeConfiguration();
}
return self::$securityConfiguration;
}
/**
* Retrieves the cryptography configuration.
*
* This method returns the current CryptographyConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return CryptographyConfiguration|null The cryptography configuration instance or null if not available.
*/
public static function getCryptographyConfiguration(): ?CryptographyConfiguration
{
if(self::$cryptographyConfiguration === null)
{
self::initializeConfiguration();
}
return self::$cryptographyConfiguration;
}
/**
* Retrieves the current database configuration.
*
* @return DatabaseConfiguration The configuration settings for the database.
*/
public static function getDatabaseConfiguration(): DatabaseConfiguration
{
if(self::$databaseConfiguration === null)
{
self::initializeConfiguration();
}
return self::$databaseConfiguration;
}
/**
* Retrieves the current logging configuration.
*
* @return LoggingConfiguration The current logging configuration instance.
*/
public static function getLoggingConfiguration(): LoggingConfiguration
{
if(self::$loggingConfiguration === null)
{
self::initializeConfiguration();
}
return self::$loggingConfiguration;
}
/**
* Retrieves the current cache configuration. If the cache configuration
* has not been initialized, it will initialize it first.
*
* @return CacheConfiguration The current cache configuration instance.
*/
public static function getCacheConfiguration(): CacheConfiguration
{
if(self::$cacheConfiguration === null)
{
self::initializeConfiguration();
}
return self::$cacheConfiguration;
}
/**
* Retrieves the registration configuration.
*
* This method returns the current RegistrationConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return RegistrationConfiguration The registration configuration instance.
*/
public static function getRegistrationConfiguration(): RegistrationConfiguration
{
if(self::$registrationConfiguration === null)
{
self::initializeConfiguration();
}
return self::$registrationConfiguration;
}
/**
* Retrieves the authentication configuration.
*
* This method returns the current AuthenticationConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return AuthenticationConfiguration The authentication configuration instance.
*/
public static function getAuthenticationConfiguration(): AuthenticationConfiguration
{
if(self::$authenticationConfiguration === null)
{
self::initializeConfiguration();
}
return self::$authenticationConfiguration;
}
/**
* Retrieves the policies configuration.
*
* This method returns the current PoliciesConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return PoliciesConfiguration The policies configuration instance.
*/
public static function getPoliciesConfiguration(): PoliciesConfiguration
{
if(self::$policiesConfiguration === null)
{
self::initializeConfiguration();
}
return self::$policiesConfiguration;
}
/**
* Retrieves the storage configuration.
*
* This method returns the current StorageConfiguration instance.
* If the configuration has not been initialized yet, it initializes it first.
*
* @return StorageConfiguration The storage configuration instance.
*/
public static function getStorageConfiguration(): StorageConfiguration
{
if(self::$storageConfiguration === null)
{
self::initializeConfiguration();
}
return self::$storageConfiguration;
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Socialbox\Classes\Configuration;
class AuthenticationConfiguration
{
private bool $enabled;
private bool $imageCaptchaVerificationRequired;
/**
* Public Constructor for the AuthenticationConfiguration class
*
* @param array $data The array data configuration
*/
public function __construct(array $data)
{
$this->enabled = (bool)$data['enabled'];
$this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required'];
}
/**
* @return bool
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* @return bool
*/
public function isImageCaptchaVerificationRequired(): bool
{
return $this->imageCaptchaVerificationRequired;
}
}

View file

@ -0,0 +1,151 @@
<?php
namespace Socialbox\Classes\Configuration;
class CacheConfiguration
{
private bool $enabled;
private string $engine;
private string $host;
private int $port;
private ?string $username;
private ?string $password;
private ?int $database;
private bool $sessionsEnabled;
private int $sessionsTtl;
private int $sessionsMax;
/**
* Constructor to initialize configuration values.
*
* @param array $data An associative array containing configuration data.
* Keys include:
* - enabled (bool): Whether the feature is enabled.
* - engine (string): The engine type.
* - host (string): The host address.
* - port (int): The port number.
* - username (string|null): The username for authentication.
* - password (string|null): The password for authentication.
* - database (int|null): The database ID.
* - sessions (array): Session-specific settings. Keys include:
* - enabled (bool): Whether sessions are enabled.
* - ttl (int): Session time-to-live in seconds.
* - max (int): Maximum number of concurrent sessions.
*
* @return void
*/
public function __construct(array $data)
{
$this->enabled = (bool)$data['enabled'];
$this->engine = (string)$data['engine'];
$this->host = (string)$data['host'];
$this->port = (int)$data['port'];
$this->username = $data['username'] ? (string)$data['username'] : null;
$this->password = $data['password'] ? (string)$data['password'] : null;
$this->database = $data['database'] ? (int)$data['database'] : null;
$this->sessionsEnabled = (bool)$data['sessions']['enabled'];
$this->sessionsTtl = (int)$data['sessions']['ttl'];
$this->sessionsMax = (int)$data['sessions']['max'];
}
/**
* Checks whether the feature is enabled.
*
* @return bool Returns true if the feature is enabled, false otherwise.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
/**
* Retrieves the engine name.
*
* @return string Returns the name of the engine.
*/
public function getEngine(): string
{
return $this->engine;
}
/**
* Retrieves the host value.
*
* @return string The host as a string.
*/
public function getHost(): string
{
return $this->host;
}
/**
* Retrieves the port value.
*
* @return int The port number.
*/
public function getPort(): int
{
return $this->port;
}
/**
* Retrieves the username value.
*
* @return string|null The username, or null if not set.
*/
public function getUsername(): ?string
{
return $this->username;
}
/**
* Retrieves the password value.
*
* @return string|null The password as a string or null if not set.
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* Retrieves the database identifier.
*
* @return int|null The database identifier or null if not set.
*/
public function getDatabase(): ?int
{
return $this->database;
}
/**
* Checks whether sessions are enabled.
*
* @return bool Returns true if sessions are enabled, otherwise false.
*/
public function isSessionsEnabled(): bool
{
return $this->sessionsEnabled;
}
/**
* Retrieves the time-to-live (TTL) value for sessions.
*
* @return int The TTL value for sessions.
*/
public function getSessionsTtl(): int
{
return $this->sessionsTtl;
}
/**
* Retrieves the maximum number of sessions allowed.
*
* @return int Returns the maximum number of sessions.
*/
public function getSessionsMax(): int
{
return $this->sessionsMax;
}
}

View file

@ -0,0 +1,111 @@
<?php
namespace Socialbox\Classes\Configuration;
class CryptographyConfiguration
{
private ?int $hostKeyPairExpires;
private ?string $hostPublicKey;
private ?string $hostPrivateKey;
private ?array $internalEncryptionKeys;
private int $encryptionKeysCount;
private string $encryptionKeysAlgorithm;
private string $transportEncryptionAlgorithm;
/**
* Constructor to initialize encryption and transport keys from provided data.
*
* @param array $data An associative array containing key-value pairs for encryption keys, algorithms, and expiration settings.
* @return void
*/
public function __construct(array $data)
{
$this->hostKeyPairExpires = $data['host_keypair_expires'] ?? null;
$this->hostPublicKey = $data['host_public_key'] ?? null;
$this->hostPrivateKey = $data['host_private_key'] ?? null;
$this->internalEncryptionKeys = $data['internal_encryption_keys'] ?? null;
$this->encryptionKeysCount = $data['encryption_keys_count'];
$this->encryptionKeysAlgorithm = $data['encryption_keys_algorithm'];
$this->transportEncryptionAlgorithm = $data['transport_encryption_algorithm'];
}
/**
* Retrieves the expiration timestamp of the host key pair.
*
* @return int|null The expiration timestamp of the host key pair, or null if not set.
*/
public function getHostKeyPairExpires(): ?int
{
return $this->hostKeyPairExpires;
}
/**
* Retrieves the host's public key.
*
* @return string|null The host's public key, or null if not set.
*/
public function getHostPublicKey(): ?string
{
return $this->hostPublicKey;
}
/**
* Retrieves the private key associated with the host.
*
* @return string|null The host's private key, or null if not set.
*/
public function getHostPrivateKey(): ?string
{
return $this->hostPrivateKey;
}
/**
* Retrieves the internal encryption keys.
*
* @return array|null Returns an array of internal encryption keys if set, or null if no keys are available.
*/
public function getInternalEncryptionKeys(): ?array
{
return $this->internalEncryptionKeys;
}
/**
* Retrieves a random internal encryption key from the available set of encryption keys.
*
* @return string|null Returns a randomly selected encryption key as a string, or null if no keys are available.
*/
public function getRandomInternalEncryptionKey(): ?string
{
return $this->internalEncryptionKeys[array_rand($this->internalEncryptionKeys)];
}
/**
* Retrieves the total count of encryption keys.
*
* @return int The number of encryption keys.
*/
public function getEncryptionKeysCount(): int
{
return $this->encryptionKeysCount;
}
/**
* Retrieves the algorithm used for the encryption keys.
*
* @return string The encryption keys algorithm.
*/
public function getEncryptionKeysAlgorithm(): string
{
return $this->encryptionKeysAlgorithm;
}
/**
* Retrieves the transport encryption algorithm being used.
*
* @return string The transport encryption algorithm.
*/
public function getTransportEncryptionAlgorithm(): string
{
return $this->transportEncryptionAlgorithm;
}
}

View file

@ -0,0 +1,94 @@
<?php
namespace Socialbox\Classes\Configuration;
class DatabaseConfiguration
{
private string $host;
private int $port;
private string $username;
private ?string $password;
private string $name;
/**
* Constructor method to initialize properties from the provided data array.
*
* @param array $data Associative array containing the keys 'host', 'port', 'username', 'password', and 'name'.
* - 'host' (string): The host of the server.
* - 'port' (int): The port number.
* - 'username' (string): The username for authentication.
* - 'password' (string|null): The password for authentication, optional.
* - 'name' (string): The name associated with the connection or resource.
*
* @return void
*/
public function __construct(array $data)
{
$this->host = (string)$data['host'];
$this->port = (int)$data['port'];
$this->username = (string)$data['username'];
$this->password = $data['password'] ? (string)$data['password'] : null;
$this->name = (string)$data['name'];
}
/**
* Retrieves the host value.
*
* @return string The value of the host.
*/
public function getHost(): string
{
return $this->host;
}
/**
* Retrieves the port value.
*
* @return int The value of the port.
*/
public function getPort(): int
{
return $this->port;
}
/**
* Retrieves the username value.
*
* @return string The value of the username.
*/
public function getUsername(): string
{
return $this->username;
}
/**
* Retrieves the password value.
*
* @return string|null The value of the password, or null if not set.
*/
public function getPassword(): ?string
{
return $this->password;
}
/**
* Retrieves the name value.
*
* @return string The value of the name
*/
public function getName(): string
{
return $this->name;
}
/**
* Constructs and retrieves the Data Source Name (DSN) string.
*
* @return string The DSN string for the database connection.
*/
public function getDsn(): string
{
return sprintf('mysql:host=%s;dbname=%s;port=%s;charset=utf8mb4', $this->host, $this->name, $this->port);
}
}

View file

@ -0,0 +1,68 @@
<?php
namespace Socialbox\Classes\Configuration;
class InstanceConfiguration
{
private bool $enabled;
private string $name;
private ?string $domain;
private ?string $rpcEndpoint;
private array $dnsMocks;
/**
* Constructor that initializes object properties with the provided data.
*
* @param array $data An associative array with keys 'enabled', 'domain', 'private_key', and 'public_key'.
* @return void
*/
public function __construct(array $data)
{
$this->enabled = (bool)$data['enabled'];
$this->name = $data['name'];
$this->domain = $data['domain'];
$this->rpcEndpoint = $data['rpc_endpoint'];
$this->dnsMocks = $data['dns_mocks'];
}
/**
* Checks if the current object is enabled.
*
* @return bool True if the object is enabled, false otherwise.
*/
public function isEnabled(): bool
{
return $this->enabled;
}
public function getName(): string
{
return $this->name;
}
/**
* Retrieves the domain.
*
* @return string|null The domain.
*/
public function getDomain(): ?string
{
return $this->domain;
}
/**
* @return string|null
*/
public function getRpcEndpoint(): ?string
{
return $this->rpcEndpoint;
}
/**
* @return array
*/
public function getDnsMocks(): array
{
return $this->dnsMocks;
}
}

View file

@ -0,0 +1,121 @@
<?php
namespace Socialbox\Classes\Configuration;
use LogLib\Enums\LogLevel;
class LoggingConfiguration
{
private bool $consoleLoggingEnabled;
private string $consoleLoggingLevel;
private bool $fileLoggingEnabled;
private string $fileLoggingLevel;
/**
* Initializes a new instance of the class with the given configuration data.
*
* @param array $data An associative array containing logging configuration settings.
* @return void
*/
public function __construct(array $data)
{
$this->consoleLoggingEnabled = (bool) $data['console_logging_enabled'];
$this->consoleLoggingLevel = $data['console_logging_level'];
$this->fileLoggingEnabled = (bool) $data['file_logging_enabled'];
$this->fileLoggingLevel = $data['file_logging_level'];
}
/**
* Checks if console logging is enabled.
*
* @return bool True if console logging is enabled, otherwise false.
*/
public function isConsoleLoggingEnabled(): bool
{
return $this->consoleLoggingEnabled;
}
/**
* Retrieves the logging level for console output.
*
* @return LogLevel The logging level configured for console output.
*/
public function getConsoleLoggingLevel(): LogLevel
{
return $this->parseLogLevel($this->consoleLoggingLevel);
}
/**
* Checks if file logging is enabled.
*
* @return bool True if file logging is enabled, false otherwise.
*/
public function isFileLoggingEnabled(): bool
{
return $this->fileLoggingEnabled;
}
/**
* Retrieves the logging level for file logging.
*
* @return LogLevel The logging level set for file logging.
*/
public function getFileLoggingLevel(): LogLevel
{
return $this->parseLogLevel($this->fileLoggingLevel);
}
/**
* Parses the given log level from string format to a LogLevel enumeration.
*
* @param string $logLevel The log level as a string.
* @return LogLevel The corresponding LogLevel enumeration.
*/
private function parseLogLevel(string $logLevel): LogLevel
{
switch (strtolower($logLevel))
{
case LogLevel::DEBUG:
case 'debug':
case '6':
case 'dbg':
return LogLevel::DEBUG;
case LogLevel::VERBOSE:
case 'verbose':
case '5':
case 'vrb':
return LogLevel::VERBOSE;
default:
case LogLevel::INFO:
case 'info':
case '4':
case 'inf':
return LogLevel::INFO;
case LogLevel::WARNING:
case 'warning':
case '3':
case 'wrn':
return LogLevel::WARNING;
case LogLevel::ERROR:
case 'error':
case '2':
case 'err':
return LogLevel::ERROR;
case LogLevel::FATAL:
case 'fatal':
case '1':
case 'crt':
return LogLevel::FATAL;
case LogLevel::SILENT:
case 'silent':
case '0':
case 'sil':
return LogLevel::SILENT;
}
}
}

View file

@ -0,0 +1,240 @@
<?php
namespace Socialbox\Classes\Configuration;
use Socialbox\Enums\PrivacyState;
class PoliciesConfiguration
{
private int $maxSigningKeys;
private int $maxContactSigningKeys;
private int $sessionInactivityExpires;
private int $imageCaptchaExpires;
private int $peerSyncInterval;
private int $getContactsLimit;
private int $getEncryptionChannelRequestsLimit;
private int $getEncryptionChannelsLimit;
private int $getEncryptionChannelIncomingLimit;
private int $getEncryptionChannelOutgoingLimit;
private PrivacyState $defaultDisplayPicturePrivacy;
private PrivacyState $defaultFirstNamePrivacy;
private PrivacyState $defaultMiddleNamePrivacy;
private PrivacyState $defaultLastNamePrivacy;
private PrivacyState $defaultEmailAddressPrivacy;
private PrivacyState $defaultPhoneNumberPrivacy;
private PrivacyState $defaultBirthdayPrivacy;
private PrivacyState $defaultUrlPrivacy;
/**
* Constructor method for initializing the policies configuration
*
* @param array $data An associative array containing the following keys:
* 'max_signing_keys', 'session_inactivity_expires',
* 'image_captcha_expires', 'peer_sync_interval',
* 'get_contacts_limit', 'default_display_picture_privacy',
* 'default_first_name_privacy', 'default_middle_name_privacy',
* 'default_last_name_privacy', 'default_email_address_privacy',
* 'default_phone_number_privacy', 'default_birthday_privacy',
* 'default_url_privacy'.
*
* @return void
*/
public function __construct(array $data)
{
$this->maxSigningKeys = $data['max_signing_keys'];
$this->maxContactSigningKeys = $data['max_contact_signing_keys'];
$this->sessionInactivityExpires = $data['session_inactivity_expires'];
$this->imageCaptchaExpires = $data['image_captcha_expires'];
$this->peerSyncInterval = $data['peer_sync_interval'];
$this->getContactsLimit = $data['get_contacts_limit'];
$this->getEncryptionChannelRequestsLimit = $data['get_encryption_channel_requests_limit'];
$this->getEncryptionChannelsLimit = $data['get_encryption_channels_limit'];
$this->getEncryptionChannelIncomingLimit = $data['get_encryption_channel_incoming_limit'];
$this->getEncryptionChannelOutgoingLimit = $data['get_encryption_channel_outgoing_limit'];
$this->defaultDisplayPicturePrivacy = PrivacyState::tryFrom($data['default_display_picture_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultFirstNamePrivacy = PrivacyState::tryFrom($data['default_first_name_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultMiddleNamePrivacy = PrivacyState::tryFrom($data['default_middle_name_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultLastNamePrivacy = PrivacyState::tryFrom($data['default_last_name_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultEmailAddressPrivacy = PrivacyState::tryFrom($data['default_email_address_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultPhoneNumberPrivacy = PrivacyState::tryFrom($data['default_phone_number_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultBirthdayPrivacy = PrivacyState::tryFrom($data['default_birthday_privacy']) ?? PrivacyState::PRIVATE;
$this->defaultUrlPrivacy = PrivacyState::tryFrom($data['default_url_privacy']) ?? PrivacyState::PRIVATE;
}
/**
* Returns the maximum amount of signing keys a peer can register with the server at once
*
* @return int
*/
public function getMaxSigningKeys(): int
{
return $this->maxSigningKeys;
}
public function getMaxContactSigningKeys(): int
{
return $this->maxContactSigningKeys;
}
/**
* Returns the maximum amount of seconds before the session is considered expired due to inactivity
*
* @return int
*/
public function getSessionInactivityExpires(): int
{
return $this->sessionInactivityExpires;
}
/**
* Returns the maximum amount of seconds before a captcha is considered expired due to the amount of time
* that has passed since the answer was generated, if a user fails to answer the captcha during the time
* period then the user must request for a new captcha with a new answer.
*
* @return int
*/
public function getImageCaptchaExpires(): int
{
return $this->imageCaptchaExpires;
}
/**
* Returns the maximum amount of seconds before the external peer resolve cache is considered expired
*
* @return int
*/
public function getPeerSyncInterval(): int
{
return $this->peerSyncInterval;
}
/**
* Returns the maximum amount of contacts that can be retrieved in a single request
*
* @return int
*/
public function getGetContactsLimit(): int
{
return $this->getContactsLimit;
}
/**
* Returns the maximum number of encryption channel requests that can be retrieved in a single request
*
* @return int
*/
public function getEncryptionChannelRequestsLimit(): int
{
return $this->getEncryptionChannelRequestsLimit;
}
/**
* Returns the maximum number of encryption channels that can be retrieved in a single request
*
* @return int
*/
public function getEncryptionChannelsLimit(): int
{
return $this->getEncryptionChannelsLimit;
}
/**
* Returns the maximum number of incoming encryption channels that can be retrieved in a single request
*
* @return int
*/
public function getEncryptionChannelIncomingLimit(): int
{
return $this->getEncryptionChannelIncomingLimit;
}
/**
* Returns the maximum number of outgoing encryption channels that can be retrieved in a single request
*
* @return int
*/
public function getEncryptionChannelOutgoingLimit(): int
{
return $this->getEncryptionChannelOutgoingLimit;
}
/**
* Returns the default privacy state for the display picture
*
* @return PrivacyState
*/
public function getDefaultDisplayPicturePrivacy(): PrivacyState
{
return $this->defaultDisplayPicturePrivacy;
}
/**
* Returns the default privacy state for the first name
*
* @return PrivacyState
*/
public function getDefaultFirstNamePrivacy(): PrivacyState
{
return $this->defaultFirstNamePrivacy;
}
/**
* Returns the default privacy state for the middle name
*
* @return PrivacyState
*/
public function getDefaultMiddleNamePrivacy(): PrivacyState
{
return $this->defaultMiddleNamePrivacy;
}
/**
* Returns the default privacy state for the last name
*
* @return PrivacyState
*/
public function getDefaultLastNamePrivacy(): PrivacyState
{
return $this->defaultLastNamePrivacy;
}
/**
* Returns the default privacy state for the email address
*
* @return PrivacyState
*/
public function getDefaultEmailAddressPrivacy(): PrivacyState
{
return $this->defaultEmailAddressPrivacy;
}
/**
* Returns the default privacy state for the phone number
*
* @return PrivacyState
*/
public function getDefaultPhoneNumberPrivacy(): PrivacyState
{
return $this->defaultPhoneNumberPrivacy;
}
/**
* Returns the default privacy state for the birthday
*
* @return PrivacyState
*/
public function getDefaultBirthdayPrivacy(): PrivacyState
{
return $this->defaultBirthdayPrivacy;
}
/**
* Returns the default privacy state for the URL
*
* @return PrivacyState
*/
public function getDefaultUrlPrivacy(): PrivacyState
{
return $this->defaultUrlPrivacy;
}
}

View file

@ -0,0 +1,286 @@
<?php
namespace Socialbox\Classes\Configuration;
class RegistrationConfiguration
{
private bool $registrationEnabled;
private ?string $privacyPolicyDocument;
private int $privacyPolicyDate;
private bool $acceptPrivacyPolicy;
private ?string $termsOfServiceDocument;
private int $termsOfServiceDate;
private bool $acceptTermsOfService;
private ?string $communityGuidelinesDocument;
private int $communityGuidelinesDate;
private bool $acceptCommunityGuidelines;
private bool $passwordRequired;
private bool $otpRequired;
private bool $displayNameRequired;
private bool $firstNameRequired;
private bool $middleNameRequired;
private bool $lastNameRequired;
private bool $displayPictureRequired;
private bool $emailAddressRequired;
private bool $phoneNumberRequired;
private bool $birthdayRequired;
private bool $urlRequired;
private bool $imageCaptchaVerificationRequired;
/**
* Constructor method for initializing verification requirements.
*
* @param array $data An associative array containing the following keys:
* 'registration_enabled', 'password_required',
* 'otp_required', 'display_name_required',
* 'email_verification_required', 'sms_verification_required',
* 'phone_call_verification_required', 'image_captcha_verification_required'.
*
* @return void
*/
public function __construct(array $data)
{
$this->registrationEnabled = (bool)$data['enabled'];
$this->privacyPolicyDocument = $data['privacy_policy_document'] ?? null;
$this->privacyPolicyDate = $data['privacy_policy_date'] ?? 0;
$this->acceptPrivacyPolicy = $data['accept_privacy_policy'] ?? true;
$this->termsOfServiceDocument = $data['terms_of_service_document'] ?? null;
$this->termsOfServiceDate = $data['terms_of_service_date'] ?? 0;
$this->acceptTermsOfService = $data['accept_terms_of_service'] ?? true;
$this->communityGuidelinesDocument = $data['community_guidelines_document'] ?? null;
$this->communityGuidelinesDate = $data['community_guidelines_date'] ?? 0;
$this->acceptCommunityGuidelines = $data['accept_community_guidelines'] ?? true;
$this->passwordRequired = (bool)$data['password_required'];
$this->otpRequired = (bool)$data['otp_required'];
$this->displayNameRequired = (bool)$data['display_name_required'];
$this->firstNameRequired = (bool)$data['first_name_required'];
$this->middleNameRequired = (bool)$data['middle_name_required'];
$this->lastNameRequired = (bool)$data['last_name_required'];
$this->displayPictureRequired = (bool)$data['display_picture_required'];
$this->emailAddressRequired = (bool)$data['email_address_required'];
$this->phoneNumberRequired = (bool)$data['phone_number_required'];
$this->birthdayRequired = (bool)$data['birthday_required'];
$this->urlRequired = (bool)$data['url_required'];
$this->imageCaptchaVerificationRequired = (bool)$data['image_captcha_verification_required'];
}
/**
* Checks if the registration is enabled.
*
* @return bool True if registration is enabled, false otherwise.
*/
public function isRegistrationEnabled(): bool
{
return $this->registrationEnabled;
}
/**
* Retrieves the privacy policy document.
*
* @return ?string Returns the privacy policy document or null if not set.
*/
public function getPrivacyPolicyDocument(): ?string
{
return $this->privacyPolicyDocument;
}
/**
* Retrieves the date of the privacy policy.
*
* @return int Returns the date of the privacy policy.
*/
public function getPrivacyPolicyDate(): int
{
return $this->privacyPolicyDate;
}
/**
* Checks if accepting the privacy policy is required.
*
* @return bool Returns true if the privacy policy must be accepted, false otherwise.
*/
public function isAcceptPrivacyPolicyRequired(): bool
{
return $this->acceptPrivacyPolicy;
}
/**
* Retrieves the terms of service document.
*
* @return ?string Returns the terms of service document or null if not set.
*/
public function getTermsOfServiceDocument(): ?string
{
return $this->termsOfServiceDocument;
}
/**
* Retrieves the date of the terms of service.
*
* @return int Returns the date of the terms of service.
*/
public function getTermsOfServiceDate(): int
{
return $this->termsOfServiceDate;
}
/**
* Checks if accepting the terms of service is required.
*
* @return bool Returns true if the terms of service must be accepted, false otherwise.
*/
public function isAcceptTermsOfServiceRequired(): bool
{
return $this->acceptTermsOfService;
}
/**
* Retrieves the community guidelines document.
*
* @return ?string Returns the community guidelines document or null if not set.
*/
public function getCommunityGuidelinesDocument(): ?string
{
return $this->communityGuidelinesDocument;
}
/**
* Retrieves the date of the community guidelines.
*
* @return int Returns the date of the community guidelines.
*/
public function getCommunityGuidelinesDate(): int
{
return $this->communityGuidelinesDate;
}
/**
* Checks if accepting the community guidelines is required.
*
* @return bool Returns true if the community guidelines must be accepted, false otherwise.
*/
public function isAcceptCommunityGuidelinesRequired(): bool
{
return $this->acceptCommunityGuidelines;
}
/**
* Determines if a password is required.
*
* @return bool True if a password is required, false otherwise.
*/
public function isPasswordRequired(): bool
{
return $this->passwordRequired;
}
/**
* Determines if OTP (One-Time Password) is required.
*
* @return bool True if OTP is required, false otherwise.
*/
public function isOtpRequired(): bool
{
return $this->otpRequired;
}
/**
* Checks if a display name is required.
*
* @return bool Returns true if a display name is required, false otherwise.
*/
public function isDisplayNameRequired(): bool
{
return $this->displayNameRequired;
}
/**
* Checks if a first name is required.
*
* @return bool Returns true if a first name is required, false otherwise.
*/
public function isFirstNameRequired(): bool
{
return $this->firstNameRequired;
}
/**
* Checks if a middle name is required.
*
* @return bool Returns true if a middle name is required, false otherwise.
*/
public function isMiddleNameRequired(): bool
{
return $this->middleNameRequired;
}
/**
* Checks if a last name is required.
*
* @return bool Returns true if a last name is required, false otherwise.
*/
public function isLastNameRequired(): bool
{
return $this->lastNameRequired;
}
/**
* Checks if a display picture is required.
*
* @return bool Returns true if a display picture is required, false otherwise.
*/
public function isDisplayPictureRequired(): bool
{
return $this->displayPictureRequired;
}
/**
* Determines whether an email address is required.
*
* @return bool Returns true if an email address is required, false otherwise.
*/
public function isEmailAddressRequired(): bool
{
return $this->emailAddressRequired;
}
/**
* Determines if a phone number is required.
*
* @return bool Returns true if a phone number is required, false otherwise.
*/
public function isPhoneNumberRequired(): bool
{
return $this->phoneNumberRequired;
}
/**
* Determines if a birthday is required.
*
* @return bool Returns true if a birthday is required, otherwise false.
*/
public function isBirthdayRequired(): bool
{
return $this->birthdayRequired;
}
/**
* Determines if a URL is required.
*
* @return bool Returns true if a URL is required, false otherwise.
*/
public function isUrlRequired(): bool
{
return $this->urlRequired;
}
/**
* Determines if image CAPTCHA verification is required.
*
* @return bool Returns true if image CAPTCHA verification is required, false otherwise.
*/
public function isImageCaptchaVerificationRequired(): bool
{
return $this->imageCaptchaVerificationRequired;
}
}

View file

@ -0,0 +1,114 @@
<?php
namespace Socialbox\Classes\Configuration;
class SecurityConfiguration
{
private bool $displayInternalExceptions;
private int $resolvedServersTtl;
private int $captchaTtl;
private int $otpSecretKeyLength;
private int $otpTimeStep;
private int $otpDigits;
private string $otpHashAlgorithm;
private int $otpWindow;
/**
* Constructor method for initializing class properties.
*
* @param array $data An associative array containing values for initializing the properties.
*
* @return void
*/
public function __construct(array $data)
{
$this->displayInternalExceptions = $data['display_internal_exceptions'];
$this->resolvedServersTtl = $data['resolved_servers_ttl'];
$this->captchaTtl = $data['captcha_ttl'];
$this->otpSecretKeyLength = $data['otp_secret_key_length'];
$this->otpTimeStep = $data['otp_time_step'];
$this->otpDigits = $data['otp_digits'];
$this->otpHashAlgorithm = $data['otp_hash_algorithm'];
$this->otpWindow = $data['otp_window'];
}
/**
* Determines if the display of internal errors is enabled.
*
* @return bool True if the display of internal errors is enabled, false otherwise.
*/
public function isDisplayInternalExceptions(): bool
{
return $this->displayInternalExceptions;
}
/**
* Retrieves the time-to-live (TTL) value for resolved servers.
*
* @return int The TTL value for resolved servers.
*/
public function getResolvedServersTtl(): int
{
return $this->resolvedServersTtl;
}
/**
* Retrieves the time-to-live (TTL) value for captchas.
*
* @return int The TTL value for captchas.
*/
public function getCaptchaTtl(): int
{
return $this->captchaTtl;
}
/**
* Retrieves the length of the secret key used for OTP generation.
*
* @return int The length of the secret key used for OTP generation.
*/
public function getOtpSecretKeyLength(): int
{
return $this->otpSecretKeyLength;
}
/**
* Retrieves the time step value for OTP generation.
*
* @return int The time step value for OTP generation.
*/
public function getOtpTimeStep(): int
{
return $this->otpTimeStep;
}
/**
* Retrieves the number of digits in the OTP.
*
* @return int The number of digits in the OTP.
*/
public function getOtpDigits(): int
{
return $this->otpDigits;
}
/**
* Retrieves the hash algorithm used for OTP generation.
*
* @return string The hash algorithm used for OTP generation.
*/
public function getOtpHashAlgorithm(): string
{
return $this->otpHashAlgorithm;
}
/**
* Retrieves the window value for OTP generation.
*
* @return int The window value for OTP generation.
*/
public function getOtpWindow(): int
{
return $this->otpWindow;
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Socialbox\Classes\Configuration;
class StorageConfiguration
{
private string $path;
private string $userDisplayImagesPath;
private int $userDisplayImagesMaxSize;
/**
* Constructor method to initialize the class properties with provided data.
*
* @param array $data An associative array containing configuration values
*/
public function __construct(array $data)
{
$this->path = $data['path'];
$this->userDisplayImagesPath = $data['user_display_images_path'];
$this->userDisplayImagesMaxSize = $data['user_display_images_max_size'];
}
/**
* Retrieves the base path value.
*
* @return string The base path.
*/
public function getPath(): string
{
return $this->path;
}
/**
* Retrieves the path for user display images.
*
* @return string The path where user display images are stored.
*/
public function getUserDisplayImagesPath(): string
{
return $this->path . DIRECTORY_SEPARATOR . $this->userDisplayImagesPath;
}
/**
* Retrieves the maximum size allowed for user display images.
*
* @return int The maximum size in bytes.
*/
public function getUserDisplayImagesMaxSize(): int
{
return $this->userDisplayImagesMaxSize;
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,41 +1,41 @@
<?php <?php
namespace Socialbox\Classes; namespace Socialbox\Classes;
use mysqli; use PDO;
use mysqli_sql_exception; use PDOException;
use PDO; use Socialbox\Classes\Configuration\DatabaseConfiguration;
use PDOException; use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\DatabaseOperationException;
class Database class Database
{
private static ?PDO $instance = null;
/**
* @return PDO
* @throws DatabaseOperationException
*/
public static function getConnection(): PDO
{ {
if (self::$instance === null) private static ?PDO $instance = null;
{
try
{
$dsn = 'mysql:host=127.0.0.1;dbname=socialbox;port=3306;charset=utf8mb4';
self::$instance = new PDO($dsn, 'root', 'root');
// Set some common PDO attributes for better error handling /**
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); * @return PDO
self::$instance->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); * @throws DatabaseOperationException
} */
catch (PDOException $e) public static function getConnection(): PDO
{
if (self::$instance === null)
{ {
throw new DatabaseOperationException('Failed to connect to the database', $e); $dsn = Configuration::getDatabaseConfiguration()->getDsn();
try
{
self::$instance = new PDO($dsn, Configuration::getDatabaseConfiguration()->getUsername(), Configuration::getDatabaseConfiguration()->getPassword());
// Set some common PDO attributes for better error handling
self::$instance->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
self::$instance->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
}
catch (PDOException $e)
{
throw new DatabaseOperationException('Failed to connect to the database using ' . $dsn, $e);
}
} }
return self::$instance;
} }
return self::$instance; }
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
use Socialbox\Objects\DnsRecord;
class DnsHelper
{
/**
* Generates a TXT formatted string containing the provided RPC endpoint, public key, and expiration time.
*
* @param string $rpcEndpoint The RPC endpoint to include in the TXT string.
* @param string $publicKey The public key to include in the TXT string.
* @param int $expirationTime The expiration time in seconds to include in the TXT string.
*
* @return string A formatted TXT string containing the input data.
*/
public static function generateTxt(string $rpcEndpoint, string $publicKey, int $expirationTime): string
{
return sprintf('v=socialbox;sb-rpc=%s;sb-key=%s;sb-exp=%d', $rpcEndpoint, $publicKey, $expirationTime);
}
/**
* Parses a TXT record string and extracts its components into a DnsRecord object.
*
* @param string $txtRecord The TXT record string to be parsed.
* @return DnsRecord The extracted DnsRecord object containing the RPC endpoint, public key, and expiration time.
* @throws InvalidArgumentException If the TXT record format is invalid.
*/
public static function parseTxt(string $txtRecord): DnsRecord
{
$pattern = '/v=socialbox;sb-rpc=(?P<rpcEndpoint>https?:\/\/[^;]+);sb-key=(?P<publicSigningKey>[^;]+);sb-exp=(?P<expirationTime>\d+)/';
if (preg_match($pattern, $txtRecord, $matches))
{
return new DnsRecord($matches['rpcEndpoint'], $matches['publicSigningKey'], (int)$matches['expirationTime']);
}
throw new InvalidArgumentException('Invalid TXT record format.');
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Socialbox\Classes;
class Logger
{
private static ?\LogLib2\Logger $logger = null;
/**
* @return \LogLib2\Logger
*/
public static function getLogger(): \LogLib2\Logger
{
if(self::$logger === null)
{
self::$logger = new \LogLib2\Logger(Configuration::getInstanceConfiguration()->getName());
\LogLib2\Logger::registerHandlers();
}
return self::$logger;
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace Socialbox\Classes;
use Random\RandomException;
use Socialbox\Exceptions\CryptographyException;
class OtpCryptography
{
private const string URI_FORMAT = 'otpauth://totp/%s?secret=%s%s&algorithm=%s&digits=%d&period=%d';
/**
* Generates a random secret key of the specified length.
*
* @param int $length The length of the secret key in bytes. Default is 32.
* @return string Returns the generated secret key as a hexadecimal string.
* @throws CryptographyException If the length is less than or equal to 0.
* @throws RandomException If an error occurs while generating random bytes.
*/
public static function generateSecretKey(int $length=32): string
{
if($length <= 0)
{
throw new CryptographyException("Invalid secret key length: must be greater than 0.");
}
return bin2hex(random_bytes($length));
}
/**
* Generates a one-time password (OTP) based on the provided parameters.
*
* @param string $secretKey The secret key used to generate the OTP.
* @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds.
* @param int $digits The number of digits in the OTP. Default is 6.
* @param int|null $counter Optional counter value. If not provided, it is calculated based on the current time and time step.
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
* @return string Returns the generated OTP as a string with the specified number of digits.
* @throws CryptographyException If the generated hash length is less than 20 bytes.
*/
public static function generateOTP(string $secretKey, int $timeStep=30, int $digits=6, int $counter=null, string $hashAlgorithm='sha512'): string
{
if ($counter === null)
{
$counter = floor(time() / $timeStep);
}
$hash = self::hashHmac($hashAlgorithm, pack('J', $counter), $secretKey);
if (strlen($hash) < 20)
{
throw new CryptographyException("Invalid hash length: must be at least 20 bytes.");
}
// Validate the $secretKey
if (!ctype_xdigit($secretKey))
{
throw new CryptographyException("Invalid secret key: must be a hexadecimal string.");
}
$offset = ord($hash[strlen($hash) - 1]) & 0x0F;
$binary = unpack('N', substr($hash, $offset, 4))[1] & 0x7FFFFFFF;
$otp = $binary % (10 ** $digits);
return str_pad((string)$otp, $digits, '0', STR_PAD_LEFT);
}
/**
* Verifies a one-time password (OTP) based on the provided parameters.
*
* @param string $secretKey The secret key used to generate the OTP.
* @param string $otp The one-time password to verify.
* @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds.
* @param int $window The allowed window of time steps for verification. Default is 1.
* @param int $digits The number of digits in the OTP. Default is 6.
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
* @return bool Returns true if the OTP is valid within the provided parameters, otherwise false.
* @throws CryptographyException If the generated hash length is less than 20 bytes.
*/
public static function verifyOTP(string $secretKey, string $otp, int $timeStep=30, int $window=1, int $digits=6, string $hashAlgorithm='sha512'): bool
{
$currentTime = time();
$counter = floor($currentTime / $timeStep);
for ($i = -$window; $i <= $window; $i++)
{
$testCounter = $counter + $i;
$expectedOtp = self::generateOTP($secretKey, $timeStep, $digits, $testCounter, $hashAlgorithm);
if (hash_equals($expectedOtp, $otp))
{
return true;
}
}
return false;
}
/**
* Generates a key URI for use in configuring an authenticator application.
*
* @param string $label A unique label to identify the account (e.g., user or service name).
* @param string $secretKey The secret key used for generating the OTP.
* @param string|null $issuer The name of the organization or service issuing the key. Default is null.
* @param int $timeStep The time step in seconds used for OTP generation. Default is 30 seconds.
* @param int $digits The number of digits in the generated OTP. Default is 6.
* @param string $hashAlgorithm The hash algorithm used for OTP generation. Default is 'sha512'.
* @return string Returns the URI string formatted
*/
public static function generateKeyUri(string $label, string $secretKey, ?string $issuer = null, int $timeStep=30, int $digits=6, string $hashAlgorithm='sha512'): string
{
$issuerPart = $issuer ? "&issuer=" . rawurlencode($issuer) : '';
return sprintf(self::URI_FORMAT, rawurlencode($label), $secretKey, $issuerPart, strtoupper($hashAlgorithm), $digits, $timeStep);
}
/**
* Computes a hash-based message authentication code (HMAC) using the specified algorithm.
*
* @param string $algorithm The hashing algorithm to be used (e.g., 'sha1', 'sha256', 'sha384', 'sha512').
* @param string $data The data to be hashed.
* @param string $key The secret key used for the HMAC generation.
* @return string The generated HMAC as a raw binary string.
* @throws CryptographyException If the algorithm is not supported.
*/
private static function hashHmac(string $algorithm, string $data, string $key): string
{
return match($algorithm)
{
'sha1', 'sha256', 'sha512' => hash_hmac($algorithm, $data, $key, true),
default => throw new CryptographyException('Algorithm not supported')
};
}
}

View file

@ -1,15 +1,84 @@
<?php <?php
namespace Socialbox\Classes; namespace Socialbox\Classes;
use InvalidArgumentException; use Socialbox\Enums\DatabaseObjects;
use Socialbox\Enums\DatabaseObjects;
class Resources class Resources
{
public static function getDatabaseResource(DatabaseObjects $object): string
{ {
$tables_directory = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'database'; /**
return $tables_directory . DIRECTORY_SEPARATOR . $object->value; * Retrieves the full path to a database resource based on the provided DatabaseObjects instance.
} *
} * @param DatabaseObjects $object An instance of DatabaseObjects containing the resource value.
* @return string The full file path to the specified database resource.
*/
public static function getDatabaseResource(DatabaseObjects $object): string
{
$tables_directory = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'database';
return $tables_directory . DIRECTORY_SEPARATOR . $object->value;
}
/**
* Retrieves the file path of a document resource based on the provided name.
*
* @param string $name The name of the document resource to retrieve.
* @return string The file path of the specified document resource.
*/
public static function getDocumentResource(String $name): string
{
$documents_directory = __DIR__ . DIRECTORY_SEPARATOR . 'Resources' . DIRECTORY_SEPARATOR . 'documents';
return $documents_directory . DIRECTORY_SEPARATOR . $name . '.html';
}
/**
* Retrieves the content of the privacy policy document.
*
* @return string The content of the privacy policy document. Attempts to fetch the document
* from a configured location if available and valid; otherwise, retrieves it from a default resource.
*/
public static function getPrivacyPolicy(): string
{
$configuredLocation = Configuration::getRegistrationConfiguration()->getPrivacyPolicyDocument();
if($configuredLocation !== null && file_exists($configuredLocation))
{
return file_get_contents($configuredLocation);
}
return file_get_contents(self::getDocumentResource('privacy'));
}
/**
* Retrieves the content of the Terms of Service document.
*
* @return string The content of the Terms of Service file. The method checks a configured location first,
* and falls back to a default resource if the configured file is unavailable.
*/
public static function getTermsOfService(): string
{
$configuredLocation = Configuration::getRegistrationConfiguration()->getTermsOfServiceDocument();
if($configuredLocation !== null && file_exists($configuredLocation))
{
return file_get_contents($configuredLocation);
}
return file_get_contents(self::getDocumentResource('tos'));
}
/**
* Retrieves the community guidelines document content.
*
* @return string The content of the community guidelines document, either from a configured location
* or a default resource if the configured location is unavailable.
*/
public static function getCommunityGuidelines(): string
{
$configuredLocation = Configuration::getRegistrationConfiguration()->getCommunityGuidelinesDocument();
if($configuredLocation !== null && file_exists($configuredLocation))
{
return file_get_contents($configuredLocation);
}
return file_get_contents(self::getDocumentResource('community'));
}
}

View file

@ -0,0 +1,18 @@
create table authentication_otp
(
peer_uuid varchar(36) not null comment 'The Peer UUID associated with this record'
primary key comment 'The Peer UUID unique Index',
secret mediumtext not null comment 'The encrypted secret for the OTP',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last updated',
constraint authentication_otp_peer_uuid_uindex
unique (peer_uuid) comment 'The Peer UUID unique Index',
constraint authentication_otp_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing encrypted OTP secrets for for verification';
create index authentication_otp_updated_index
on authentication_otp (updated)
comment 'The index for the updated column';

View file

@ -0,0 +1,18 @@
create table authentication_passwords
(
peer_uuid varchar(36) not null comment 'The primary unique index of the peer uuid'
primary key,
hash mediumtext not null comment 'The encrypted hash of the password',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint authentication_passwords_peer_uuid_uindex
unique (peer_uuid) comment 'The primary unique index of the peer uuid',
constraint authentication_passwords_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing authentication passwords for registered peers';
create index authentication_passwords_updated_index
on authentication_passwords (updated)
comment 'The index of the of the updated column of the record';

View file

@ -0,0 +1,24 @@
create table captcha_images
(
uuid varchar(36) default uuid() not null comment 'The Unique index for the UUID column'
primary key,
peer_uuid varchar(36) not null comment 'The UUID of the peer that is associated with this captcha challenge',
status enum ('UNSOLVED', 'SOLVED') default 'UNSOLVED' not null comment 'The status of the current captcha',
answer varchar(8) null comment 'The current answer for the captcha',
answered timestamp null comment 'The Timestamp for when this captcha was answered',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this captcha record was created',
constraint captchas_peer_uuid_uindex
unique (peer_uuid) comment 'The Primary Unique Index for the peer UUID',
constraint captchas_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
);
create index captchas_status_index
on captcha_images (status)
comment 'The Index for the captcha status';
create index captchas_uuid_index
on captcha_images (uuid)
comment 'The Unique index for the UUID column';

View file

@ -0,0 +1,32 @@
create table channel_com
(
uuid varchar(36) default uuid() not null comment 'The UUID of the message',
channel_uuid varchar(36) not null comment 'The UUID of the encryption channel used',
recipient enum ('SENDER', 'RECEIVER') not null comment 'The recipient of the message',
message text not null comment 'The encrypted message content',
signature varchar(64) not null comment 'The signature of the decrypted message',
received tinyint(1) default 0 not null comment 'True if the message was received by the recipient',
timestamp timestamp default current_timestamp() not null comment 'The timestamp of the mssage being sent',
primary key (uuid, channel_uuid) comment 'The Unique Pair Index for the channel UUID and message UUID',
constraint channel_com_uuid_channel_uuid_uindex
unique (uuid, channel_uuid) comment 'The Unique Pair Index for the channel UUID and message UUID',
constraint channel_com_uuid_channel_uuid_uindex_2
unique (uuid, channel_uuid) comment 'The Unique Index Pair for the channel UUID and message UUID',
constraint channel_com_encryption_channels_uuid_fk
foreign key (channel_uuid) references encryption_channels (uuid)
on update cascade on delete cascade
)
comment 'Table for housing communication messages over encryption channels';
create index channel_com_received_index
on channel_com (received)
comment 'The index for the received column';
create index channel_com_recipient_index
on channel_com (recipient)
comment 'The index for the recipient column';
create index channel_com_timestamp_index
on channel_com (timestamp)
comment 'The index for the Timestamp column';

View file

@ -0,0 +1,22 @@
create table contacts_known_keys
(
contact_uuid varchar(36) not null comment 'The Unique Universal Identifier of the personal contact that this record is associated with',
signature_uuid varchar(36) not null comment 'The Unique Universal Identifier for the signature key',
signature_name varchar(64) not null comment 'The name of the signing key',
signature_key varchar(32) not null comment 'The public signing key',
expires timestamp null comment 'The Timestamp for when this key expires, null means never',
created timestamp not null comment 'The Timestamp for when this key was created',
trusted_on timestamp default current_timestamp() not null comment 'The Timestamp for when the peer trusted this key',
primary key (contact_uuid, signature_uuid),
constraint contacts_known_keys_signature_uuid_contact_uuid_uindex
unique (signature_uuid, contact_uuid) comment 'The Unique Signature Index Pair for the contact UUID and key UUID',
constraint contacts_known_keys_contacts_uuid_fk
foreign key (contact_uuid) references contacts (uuid)
on update cascade on delete cascade
)
comment 'Table for housing known keys associated with personal contact records';
create index contacts_known_keys_contact_uuid_index
on contacts_known_keys (contact_uuid)
comment 'The Index of the contact UUID';

View file

@ -0,0 +1,18 @@
create table contacts
(
uuid varchar(36) default uuid() not null comment 'The contact UUID for the record'
primary key comment 'The Primary Unique Universal Identifier for the contact record',
peer_uuid varchar(36) not null comment 'The Peer UUID',
contact_peer_address varchar(256) not null comment 'The contact peer address',
relationship enum ('MUTUAL', 'TRUSTED', 'BLOCKED') default 'MUTUAL' not null comment 'The relationship between the two peers, MUTUAL=The contact peer is recognized',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this contact was created',
constraint contacts_uuid_uindex
unique (uuid) comment 'The Primary Unique Universal Identifier for the contact record',
constraint peer_contacts_peer_uuid_contact_peer_address_uindex
unique (peer_uuid, contact_peer_address) comment 'The Unique Peer UUID & Contact Peer Address combination pair',
constraint peer_contacts_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing personal contacts for peers';

View file

@ -0,0 +1,26 @@
create table encryption_channels_com
(
uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the message for the encryption channel',
channel_uuid varchar(36) not null comment 'The UUID of the channel that the message belongs to',
recipient enum ('CALLER', 'RECEIVER') not null comment 'The recipient of the message',
status enum ('SENT', 'RECEIVED', 'REJECTED') default 'SENT' not null comment 'The status of the message, SENT being the default, RECEIVED is when the recipient receives the message successfully and REJECTED is when the message cannot be decrypted, or the checksum failed.',
checksum varchar(64) not null comment 'The SHA512 hash of the decrypted message contents',
data text not null comment 'The data of the message',
timestamp timestamp default current_timestamp() not null comment 'The Timestamp of the message',
primary key (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message',
constraint encryption_channels_com_uuid_channel_uuid_uindex
unique (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message',
constraint encryption_channels_com_encryption_channels_uuid_fk
foreign key (channel_uuid) references encryption_channels (uuid)
on update cascade on delete cascade
)
comment 'The table for housing communication messages sent over encryption channels';
create index encryption_channels_com_recipient_index
on encryption_channels_com (recipient)
comment 'The index of the recipient column used for indexing';
create index encryption_channels_com_timestamp_index
on encryption_channels_com (timestamp)
comment 'The index of the Timestamp column';

View file

@ -0,0 +1,26 @@
create table encryption_channels_com
(
uuid varchar(36) default uuid() not null comment 'The Unique Universal Identifier of the message for the encryption channel',
channel_uuid varchar(36) not null comment 'The UUID of the channel that the message belongs to',
recipient enum ('CALLER', 'RECEIVER') not null comment 'The recipient of the message',
status enum ('SENT', 'RECEIVED', 'REJECTED') default 'SENT' not null comment 'The status of the message, SENT being the default, RECEIVED is when the recipient receives the message successfully and REJECTED is when the message cannot be decrypted, or the checksum failed.',
checksum varchar(64) not null comment 'The SHA512 hash of the decrypted message contents',
data text not null comment 'The data of the message',
timestamp timestamp default current_timestamp() not null comment 'The Timestamp of the message',
primary key (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message',
constraint encryption_channels_com_uuid_channel_uuid_uindex
unique (uuid, channel_uuid) comment 'The Unique Primary Index Pair for the channel_uuid and uuid of the message',
constraint encryption_channels_com_encryption_channels_uuid_fk
foreign key (channel_uuid) references encryption_channels (uuid)
on update cascade on delete cascade
)
comment 'The table for housing communication messages sent over encryption channels';
create index encryption_channels_com_recipient_index
on encryption_channels_com (recipient)
comment 'The index of the recipient column used for indexing';
create index encryption_channels_com_timestamp_index
on encryption_channels_com (timestamp)
comment 'The index of the Timestamp column';

View file

@ -0,0 +1,22 @@
create table external_sessions
(
domain varchar(256) not null comment 'The unique domain name that this session belongs to'
primary key comment 'The Unique Primary index for the external session',
rpc_endpoint text not null comment 'The RPC endpoint of the external connection',
session_uuid varchar(36) not null comment 'The UUID of the session to the external server',
transport_encryption_algorithm enum ('xchacha20', 'chacha20', 'aes256gcm') default 'xchacha20' not null comment 'The transport encryption algorithm used',
server_keypair_expires int not null comment 'The Timestamp for when the server keypair expires',
server_public_signing_key varchar(64) not null comment 'The public signing key of the server resolved from DNS records',
server_public_encryption_key varchar(64) not null comment 'The public encryption key of the server for this session',
host_public_encryption_key varchar(64) not null comment 'The public encryption key for the host',
host_private_encryption_key varchar(64) not null comment 'The private encryption key for host',
private_shared_secret varchar(64) not null comment 'The private shared secret obtained from the DHE procedure',
host_transport_encryption_key varchar(64) not null comment 'The transport encryption key for the host',
server_transport_encryption_key varchar(64) not null comment 'The transport encryption key for the server',
last_accessed timestamp default current_timestamp() not null comment 'The Timestamp for when the record was last accessed',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created',
constraint external_sessions_domain_uindex
unique (domain) comment 'The Unique Primary index for the external session'
)
comment 'Table for housing external sessions to external servers';

View file

@ -1,18 +0,0 @@
create table password_authentication
(
peer_uuid varchar(36) not null comment 'The Primary unique Index for the peer UUID'
primary key,
value varchar(128) not null comment 'The hash value of the pasword',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint password_authentication_peer_uuid_uindex
unique (peer_uuid) comment 'The Primary unique Index for the peer UUID',
constraint password_authentication_registered_peers_uuid_fk
foreign key (peer_uuid) references registered_peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing password authentications associated with peers';
create index password_authentication_updated_index
on password_authentication (updated)
comment 'The Indefor the updated timestamp';

View file

@ -0,0 +1,19 @@
create table peer_information
(
peer_uuid varchar(36) not null comment 'The Unique Universal Identifier for the peer',
property_name enum ('DISPLAY_NAME', 'DISPLAY_PICTURE', 'FIRST_NAME', 'MIDDLE_NAME', 'LAST_NAME', 'EMAIL_ADDRESS', 'PHONE_NUMBER', 'BIRTHDAY', 'URL') not null comment 'The name of the property',
property_value varchar(256) not null comment 'The value of the property associated with the peer',
privacy_state enum ('PUBLIC', 'PRIVATE', 'CONTACTS', 'TRUSTED') default 'PRIVATE' not null comment 'The privacy setting for the information property',
primary key (property_name, peer_uuid),
constraint peer_information_peer_uuid_property_name_uindex
unique (peer_uuid, property_name) comment 'The Unique Index for the the peer uuid & property name combination',
constraint peer_information_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing peer information';
create index peer_information_peer_uuid_index
on peer_information (peer_uuid)
comment 'The index for the peer uuid';

View file

@ -0,0 +1,37 @@
create table peers
(
uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid'
primary key,
username varchar(255) not null comment 'The Unique username associated with the peer',
server varchar(255) default 'host' not null comment 'The server that this peer is registered to',
flags text null comment 'Comma seprted flags associated with the peer',
enabled tinyint(1) default 0 not null comment 'Boolean column to indicate if this account is Enabled, by default it''s Disabled until the account is verified.',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
created timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network',
constraint registered_peers_server_username_uindex
unique (server, username) comment 'The Unique Username + Server Index Pair',
constraint registered_peers_uuid_uindex
unique (uuid) comment 'The Primary index for the peer uuid'
)
comment 'Table for housing registered peers under this network';
create index registered_peers_enabled_index
on peers (enabled)
comment 'The index of the enabled column for registered peers';
create index registered_peers_registered_index
on peers (created)
comment 'The Index for the reigstered column of the peer';
create index registered_peers_server_index
on peers (server)
comment 'The Index for the peer''s server';
create index registered_peers_updated_index
on peers (updated)
comment 'The Index for the update column';
create index registered_peers_username_index
on peers (username)
comment 'The index for the registered username';

View file

@ -1,20 +0,0 @@
create table registered_peers
(
uuid varchar(36) default uuid() not null comment 'The Primary index for the peer uuid'
primary key,
username varchar(255) not null comment 'The Unique username associated with the peer',
flags text null comment 'Comma seprted flags associated with the peer',
registered timestamp default current_timestamp() not null comment 'The Timestamp for when the peer was registered on the network',
constraint registered_peers_pk_2
unique (username) comment 'The unique username for the peer',
constraint registered_peers_username_uindex
unique (username) comment 'The unique username for the peer',
constraint registered_peers_uuid_uindex
unique (uuid) comment 'The Primary index for the peer uuid'
)
comment 'Table for housing registered peers under this network';
create index registered_peers_registered_index
on registered_peers (registered)
comment 'The Index for the reigstered column of the peer';

View file

@ -0,0 +1,19 @@
create table resolved_dns_records
(
domain varchar(512) not null comment 'Unique Index for the server domain'
primary key,
rpc_endpoint text not null comment 'The endpoint of the RPC server',
public_key text not null comment 'The Public Key of the server',
expires bigint not null comment 'The Unix Timestamp for when the server''s keypair expires',
updated timestamp default current_timestamp() not null comment 'The Timestamp for when this record was last updated',
constraint resolved_dns_records_domain_uindex
unique (domain) comment 'Unique Index for the server domain',
constraint resolved_dns_records_pk
unique (domain) comment 'Unique Index for the server domain'
)
comment 'A table for housing DNS resolutions';
create index resolved_dns_records_updated_index
on resolved_dns_records (updated)
comment 'The index for the updated column';

View file

@ -1,21 +1,31 @@
create table sessions create table sessions
( (
uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID' uuid varchar(36) default uuid() not null comment 'The Unique Primary index for the session UUID'
primary key, primary key,
authenticated_peer_uuid varchar(36) null comment 'The peer the session is authenticated as, null if the session isn''t authenticated', peer_uuid varchar(36) not null comment 'The peer the session is identified as, null if the session isn''t identified as a peer',
public_key blob not null comment 'The client''s public key provided when creating the session', client_name varchar(256) not null comment 'The name of the client that is using this session',
state enum ('ACTIVE', 'EXPIRED', 'CLOSED') default 'ACTIVE' not null comment 'The status of the session', client_version varchar(16) not null comment 'The version of the client',
created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created', authenticated tinyint(1) default 0 not null comment 'Indicates if the session is currently authenticated as the peer',
last_request timestamp null comment 'The Timestamp for when the last request was made using this session', client_public_signing_key varchar(64) not null comment 'The client''s public signing key used for signing decrypted messages',
client_public_encryption_key varchar(64) not null comment 'The Public Key of the client''s encryption key',
server_public_encryption_key varchar(64) not null comment 'The server''s public encryption key for this session',
server_private_encryption_key varchar(64) not null comment 'The server''s private encryption key for this session',
private_shared_secret varchar(64) null comment 'The shared secret encryption key between the Client & Server',
client_transport_encryption_key varchar(64) null comment 'The encryption key for sending messages to the client',
server_transport_encryption_key varchar(64) null comment 'The encryption key for sending messages to the server',
state enum ('AWAITING_DHE', 'ACTIVE', 'CLOSED', 'EXPIRED') default 'AWAITING_DHE' not null comment 'The status of the session',
flags text null comment 'The current flags that is set to the session',
created timestamp default current_timestamp() not null comment 'The Timestamp for when the session was last created',
last_request timestamp null comment 'The Timestamp for when the last request was made using this session',
constraint sessions_uuid_uindex constraint sessions_uuid_uindex
unique (uuid) comment 'The Unique Primary index for the session UUID', unique (uuid) comment 'The Unique Primary index for the session UUID',
constraint sessions_registered_peers_uuid_fk constraint sessions_registered_peers_uuid_fk
foreign key (authenticated_peer_uuid) references registered_peers (uuid) foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade on update cascade on delete cascade
); );
create index sessions_authenticated_peer_index create index sessions_authenticated_peer_index
on sessions (authenticated_peer_uuid) on sessions (peer_uuid)
comment 'The Index for the authenticated peer column'; comment 'The Index for the authenticated peer column';
create index sessions_created_index create index sessions_created_index

View file

@ -0,0 +1,32 @@
create table signing_keys
(
peer_uuid varchar(36) not null comment 'The UUID of the peer',
uuid varchar(36) default uuid() not null comment 'The UUID of the key record',
name varchar(64) not null comment 'Optional. User provided name for the key',
public_key varchar(64) not null comment 'The Public Signature Key',
state enum ('ACTIVE', 'EXPIRED') default 'ACTIVE' not null comment 'The state of the public key',
expires timestamp null comment 'The Timestamp for when this key expires, null = Never expires',
created timestamp default current_timestamp() not null comment 'The Timestamp for when the signing key record was created',
primary key (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer',
constraint signing_keys_peer_uuid_uuid_uindex
unique (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer',
constraint signing_keys_pk
unique (peer_uuid, uuid) comment 'The Unique Index pair for the signing key name and the UUID of the peer',
constraint signing_keys_registered_peers_uuid_fk
foreign key (peer_uuid) references peers (uuid)
on update cascade on delete cascade
)
comment 'Table for housing public signing keys for peers on the network';
create index signing_keys_peer_uuid_index
on signing_keys (peer_uuid)
comment 'The primary index for the peer UUID column';
create index signing_keys_state_index
on signing_keys (state)
comment 'Signing key state index';
create index signing_keys_uuid_index
on signing_keys (uuid)
comment 'The index for the signing key namee';

View file

@ -1,11 +1,12 @@
create table variables create table variables
( (
name varchar(255) not null comment 'The name of the variable' name varchar(255) not null comment 'The unique index for the variable name'
primary key comment 'The unique index for the variable name', primary key,
value text null comment 'The value of the variable', value text null comment 'The value of the variable',
`read_only` tinyint(1) default 0 not null comment 'Boolean indicator if the variable is read only', read_only tinyint(1) default 0 not null comment 'Boolean indicator if the variable is read only',
created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created', created timestamp default current_timestamp() not null comment 'The Timestamp for when this record was created',
updated timestamp null comment 'The Timestamp for when this record was last updated', updated timestamp null comment 'The Timestamp for when this record was last updated',
constraint variables_name_uindex constraint variables_name_uindex
unique (name) comment 'The unique index for the variable name' unique (name) comment 'The unique index for the variable name'
); );

View file

@ -0,0 +1 @@
<h1>Community Guidelines</h1>

View file

@ -0,0 +1,2 @@
<h1>Privacy Policy Document</h1>
<p>This is where the privacy policy document would reside in</p>

View file

@ -0,0 +1,2 @@
<h1>Terms of Service Document</h1>
<p>This is where the Terms of Service document would reside</p>

View file

@ -2,87 +2,915 @@
namespace Socialbox\Classes; namespace Socialbox\Classes;
use Socialbox\Classes\ServerResolver; use InvalidArgumentException;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\StandardHeaders; use Socialbox\Enums\StandardHeaders;
use Socialbox\Enums\Types\RequestType;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\ResolutionException; use Socialbox\Exceptions\ResolutionException;
use Socialclient\Exceptions\RpcRequestException; use Socialbox\Exceptions\RpcException;
use Socialbox\Objects\Client\EncryptionChannelSecret;
use Socialbox\Objects\Client\ExportedSession;
use Socialbox\Objects\Client\SignatureKeyPair;
use Socialbox\Objects\KeyPair;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Objects\RpcResult;
use Socialbox\Objects\Standard\ServerInformation;
class RpcClient class RpcClient
{ {
private \LogLib2\Logger $logger;
private const string CLIENT_NAME = 'Socialbox PHP'; private const string CLIENT_NAME = 'Socialbox PHP';
private const string CLIENT_VERSION = '1.0'; private const string CLIENT_VERSION = '1.0';
private const string CONTENT_TYPE = 'application/json';
private string $domain;
private string $endpoint;
private string $serverPublicKey;
private PeerAddress $identifiedAs;
private string $serverPublicSigningKey;
private string $serverPublicEncryptionKey;
private KeyPair $clientSigningKeyPair;
private KeyPair $clientEncryptionKeyPair;
private string $privateSharedSecret;
private string $clientTransportEncryptionKey;
private string $serverTransportEncryptionKey;
private ServerInformation $serverInformation;
private string $rpcEndpoint;
private string $remoteServer;
private string $sessionUuid;
private ?string $defaultSigningKey;
private array $signingKeys;
/**
* @var EncryptionChannelSecret[]
*/
private array $encryptionChannelSecrets;
/** /**
* @throws ResolutionException * Constructs a new instance with the specified peer address.
*
* @param string|PeerAddress $identifiedAs The peer address to be used for the instance (eg; johndoe@example.com)
* @param string|null $server Optional. The domain of the server to connect to if different from the identified
* @param ExportedSession|null $exportedSession Optional. An exported session to be used to re-connect.
* @throws CryptographyException If there is an error in the cryptographic operations.
* @throws ResolutionException If there is an error in the resolution process.
* @throws RpcException If there is an error in the RPC request or if no response is received.
* @throws DatabaseOperationException If there is an error in the database operation.
*/ */
public function __construct(string $domain) public function __construct(string|PeerAddress $identifiedAs, ?string $server=null, ?ExportedSession $exportedSession=null)
{ {
$resolved = ServerResolver::resolveDomain($domain); $this->logger = new \LogLib2\Logger('net.nosial.socialbox');
$this->domain = $domain; // If an exported session is provided, no need to re-connect.
$this->endpoint = $resolved->getEndpoint(); // Just use the session details provided.
$this->serverPublicKey = $resolved->getPublicKey(); if($exportedSession !== null)
$this->clientPrivateKey = null; {
// Check if the server keypair has expired from the exported session
if($exportedSession->getServerKeypairExpires() > 0 && time() > $exportedSession->getServerKeypairExpires())
{
throw new RpcException('The server keypair has expired, a new session must be created');
}
$this->identifiedAs = PeerAddress::fromAddress($exportedSession->getPeerAddress());
$this->rpcEndpoint = $exportedSession->getRpcEndpoint();
$this->remoteServer = $exportedSession->getRemoteServer();
$this->sessionUuid = $exportedSession->getSessionUuid();
$this->serverPublicSigningKey = $exportedSession->getServerPublicSigningKey();
$this->serverPublicEncryptionKey = $exportedSession->getServerPublicEncryptionKey();
$this->clientSigningKeyPair = new KeyPair($exportedSession->getClientPublicSigningKey(), $exportedSession->getClientPrivateSigningKey());
$this->clientEncryptionKeyPair = new KeyPair($exportedSession->getClientPublicEncryptionKey(), $exportedSession->getClientPrivateEncryptionKey());
$this->privateSharedSecret = $exportedSession->getPrivateSharedSecret();
$this->clientTransportEncryptionKey = $exportedSession->getClientTransportEncryptionKey();
$this->serverTransportEncryptionKey = $exportedSession->getServerTransportEncryptionKey();
$this->signingKeys = $exportedSession->getSigningKeys();
$this->defaultSigningKey = $exportedSession->getDefaultSigningKey();
$this->encryptionChannelSecrets = $exportedSession->getEncryptionChannelSecrets();
// Still solve the server information
$this->serverInformation = self::getServerInformation();
// Check if the active keypair has expired
if($this->serverInformation->getServerKeypairExpires() > 0 && time() > $this->serverInformation->getServerKeypairExpires())
{
// TODO: Could be auto-resolved
throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator');
}
// Check if the transport encryption algorithm has changed
if($this->serverInformation->getTransportEncryptionAlgorithm() !== $exportedSession->getTransportEncryptionAlgorithm())
{
// TODO: Could be auto-resolved
throw new RpcException('The server has changed its transport encryption algorithm, a new session must be created, old algorithm: ' . $exportedSession->getTransportEncryptionAlgorithm() . ', new algorithm: ' . $this->serverInformation->getTransportEncryptionAlgorithm());
}
return;
}
// If the peer address is a string, we need to convert it to a PeerAddress object
if(is_string($identifiedAs))
{
$identifiedAs = PeerAddress::fromAddress($identifiedAs);
}
// Set the initial properties
$this->signingKeys = [];
$this->encryptionChannelSecrets = [];
$this->defaultSigningKey = null;
$this->identifiedAs = $identifiedAs;
$this->remoteServer = $server ?? $identifiedAs->getDomain();
// Resolve the domain and get the server's Public Key & RPC Endpoint
$resolvedServer = ServerResolver::resolveDomain($this->remoteServer, false);
// Import the RPC Endpoint & the server's public key.
$this->serverPublicSigningKey = $resolvedServer->getPublicSigningKey();
$this->rpcEndpoint = $resolvedServer->getRpcEndpoint();
if(empty($this->serverPublicSigningKey))
{
throw new ResolutionException('Failed to resolve domain: No public key found for the server');
}
// Resolve basic server information
$this->serverInformation = self::getServerInformation();
// Check if the server keypair has expired
if($this->serverInformation->getServerKeypairExpires() > 0 && time() > $this->serverInformation->getServerKeypairExpires())
{
throw new RpcException('The server keypair has expired but the server has not provided a new keypair, contact the server administrator');
}
// If the username is `host` and the domain is the same as this server's domain, we use our signing keypair
// Essentially this is a special case for the server to contact another server
if($this->identifiedAs->isHost())
{
$this->clientSigningKeyPair = new KeyPair(Configuration::getCryptographyConfiguration()->getHostPublicKey(), Configuration::getCryptographyConfiguration()->getHostPrivateKey());
}
// Otherwise we generate a random signing keypair
else
{
$this->clientSigningKeyPair = Cryptography::generateSigningKeyPair();
}
// Always use a random encryption keypair
$this->clientEncryptionKeyPair = Cryptography::generateEncryptionKeyPair();
// Create a session with the server, with the method we obtain the Session UUID
// And the server's public encryption key.
$this->createSession();
// Generate a transport encryption key on our end using the server's preferred algorithm
$this->clientTransportEncryptionKey = Cryptography::generateEncryptionKey($this->serverInformation->getTransportEncryptionAlgorithm());
// Preform the DHE so that transport encryption keys can be exchanged
$this->sendDheExchange();
} }
public function getDomain(): string /**
* Initiates a new session with the server and retrieves the session UUID.
*
* @throws RpcException If the session cannot be created, if the server does not provide a valid response,
* or critical headers like encryption public key are missing in the server's response.
* @return void
*/
private function createSession(): void
{ {
return $this->domain; $ch = curl_init();
} $headers = [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INITIATE_SESSION->value,
StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME,
StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION,
StandardHeaders::IDENTIFY_AS->value . ': ' . $this->identifiedAs->getAddress(),
// Always provide our generated encrypted public key
StandardHeaders::ENCRYPTION_PUBLIC_KEY->value . ': ' . $this->clientEncryptionKeyPair->getPublicKey()
];
public function getEndpoint(): string // If we're not connecting as the host, we need to provide our public key
{ // Otherwise, the server will obtain the public key itself from DNS records rather than trusting the client
return $this->endpoint; if(!$this->identifiedAs->isHost())
} {
$headers[] = StandardHeaders::SIGNING_PUBLIC_KEY->value . ': ' . $this->clientSigningKeyPair->getPublicKey();
public function getServerPublicKey(): string }
{
return $this->serverPublicKey;
}
public function sendRequest(array $data)
{
$ch = curl_init($this->endpoint);
$responseHeaders = [];
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true); // Capture the response headers to get the encryption public key
curl_setopt($ch, CURLOPT_POSTFIELDS, Utilities::jsonEncode($data)); curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders)
curl_setopt($ch, CURLOPT_HTTPHEADER, [ {
Utilities::generateHeader(StandardHeaders::CLIENT_NAME, self::CLIENT_NAME), $len = strlen($header);
Utilities::generateHeader(StandardHeaders::CLIENT_VERSION, self::CLIENT_VERSION), $header = explode(':', $header, 2);
Utilities::generateHeader(StandardHeaders::CONTENT_TYPE, self::CONTENT_TYPE) if (count($header) < 2) // ignore invalid headers
]); {
curl_setopt($ch, CURLOPT_HEADER, true); return $len;
}
$responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$this->logger->debug(sprintf('Creating session with %s', $this->rpcEndpoint));
$this->logger->debug(sprintf('Headers: %s', json_encode($headers)));
$this->logger->debug(sprintf('Client Encryption Public Key: %s', $this->clientEncryptionKeyPair->getPublicKey()));
$this->logger->debug(sprintf('Client Signing Public Key: %s', $this->clientSigningKeyPair->getPublicKey()));
$this->logger->debug(sprintf('Identified As: %s', $this->identifiedAs->getAddress()));
$response = curl_exec($ch); $response = curl_exec($ch);
if (curl_errno($ch)) // If the response is false, the request failed
if($response === false)
{ {
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
// Separate headers and body
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$response_body = substr($response, $header_size);
curl_close($ch); curl_close($ch);
throw new RpcException(sprintf('Failed to create the session at %s, no response received', $this->rpcEndpoint));
// Throw exception with response body as message and status code as code
throw new RpcRequestException($response_body, $statusCode);
} }
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); // If the response code is not 201, the request failed
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 201)
{
curl_close($ch);
// Separate headers and body if(empty($response))
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); {
$response_headers = substr($response, 0, $header_size); throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode, $this->rpcEndpoint));
$response_body = substr($response, $header_size); }
throw new RpcException(sprintf('Failed to create the session at %s, server responded with ' . $responseCode . ': ' . $response, $this->rpcEndpoint));
}
// If the response is empty, the server did not provide a session UUID
if(empty($response))
{
curl_close($ch);
throw new RpcException(sprintf('Failed to create the session at %s, server did not return a session UUID', $this->rpcEndpoint));
}
// Get the Encryption Public Key from the server's response headers
$serverPublicEncryptionKey = $responseHeaders[strtolower(StandardHeaders::ENCRYPTION_PUBLIC_KEY->value)][0] ?? null;
// null check
if($serverPublicEncryptionKey === null)
{
curl_close($ch);
throw new RpcException('Failed to create session at %s, the server did not return a public encryption key');
}
$this->logger->debug(sprintf('Server Encryption Public Key: %s', $serverPublicEncryptionKey));
// Validate the server's encryption public key
if(!Cryptography::validatePublicEncryptionKey($serverPublicEncryptionKey))
{
curl_close($ch);
throw new RpcException('The server did not provide a valid encryption public key');
}
// If the server did not provide an encryption public key, the response is invalid
// We can't preform the DHE without the server's encryption key.
if ($serverPublicEncryptionKey === null)
{
curl_close($ch);
throw new RpcException('The server did not provide a signature for the response');
}
curl_close($ch); curl_close($ch);
$this->logger->debug(sprintf('Session UUID: %s', $response));
// Set the server's encryption key
$this->serverPublicEncryptionKey = $serverPublicEncryptionKey;
// Set the session UUID
$this->sessionUuid = $response;
}
/**
* Sends a Diffie-Hellman Ephemeral (DHE) exchange request to the server.
*
* @throws RpcException If the encryption or the request fails.
*/
private function sendDheExchange(): void
{
// First preform the DHE
try
{
$this->privateSharedSecret = Cryptography::performDHE($this->serverPublicEncryptionKey, $this->clientEncryptionKeyPair->getPrivateKey());
}
catch(CryptographyException $e)
{
throw new RpcException('Failed to preform DHE: ' . $e->getMessage(), StandardError::CRYPTOGRAPHIC_ERROR->value, $e);
}
// Request body should contain the encrypted key, the client's public key, and the session UUID
// Upon success the server should return 204 without a body
try
{
$encryptedKey = Cryptography::encryptShared($this->clientTransportEncryptionKey, $this->privateSharedSecret);
$signature = Cryptography::signMessage($this->clientTransportEncryptionKey, $this->clientSigningKeyPair->getPrivateKey());
}
catch (CryptographyException $e)
{
throw new RpcException('Failed to encrypt DHE exchange data', StandardError::CRYPTOGRAPHIC_ERROR->value, $e);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::DHE_EXCHANGE->value,
StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid,
StandardHeaders::SIGNATURE->value . ': ' . $signature
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedKey);
$response = curl_exec($ch);
if($response === false)
{
curl_close($ch);
throw new RpcException('Failed to send DHE exchange, no response received', StandardError::CRYPTOGRAPHIC_ERROR->value);
}
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 200)
{
curl_close($ch);
throw new RpcException('Failed to send DHE exchange, server responded with ' . $responseCode . ': ' . $response, StandardError::CRYPTOGRAPHIC_ERROR->value);
}
try
{
$this->serverTransportEncryptionKey = Cryptography::decryptShared($response, $this->privateSharedSecret);
}
catch(CryptographyException $e)
{
throw new RpcException('Failed to decrypt DHE exchange data', 0, $e);
}
finally
{
curl_close($ch);
}
}
/**
* Sends an RPC request with the given JSON data.
*
* @param string $jsonData The JSON data to be sent in the request.
* @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication.
* @return RpcResult[] An array of RpcResult objects.
* @throws RpcException If the request fails, the response is invalid, or the decryption/signature verification fails.
*/
public function sendRawRequest(string $jsonData, ?string $identifiedAs=null): array
{
try
{
$encryptedData = Cryptography::encryptMessage(
message: $jsonData,
encryptionKey: $this->serverTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
$signature = Cryptography::signMessage(
message: $jsonData,
privateKey: $this->clientSigningKeyPair->getPrivateKey(),
);
}
catch (CryptographyException $e)
{
throw new RpcException('Failed to encrypt request data: ' . $e->getMessage(), 0, $e);
}
$ch = curl_init();
$returnHeaders = [];
$headers = [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::RPC->value,
StandardHeaders::SESSION_UUID->value . ': ' . $this->sessionUuid,
StandardHeaders::SIGNATURE->value . ': ' . $signature
];
if($identifiedAs)
{
$headers[] = StandardHeaders::IDENTIFY_AS->value . ': ' . $identifiedAs;
}
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$returnHeaders)
{
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) // ignore invalid returnHeaders
{
return $len;
}
$returnHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
});
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, $encryptedData);
$this->logger->debug(sprintf('Sending RPC request to %s', $this->rpcEndpoint));
$this->logger->debug(sprintf('Headers: %s', json_encode($headers)));
$this->logger->debug(sprintf('Encrypted Data Size: %d', strlen($encryptedData)));
$this->logger->debug(sprintf('Request Signature: %s', $signature));
$response = curl_exec($ch);
if ($response === false)
{
curl_close($ch);
throw new RpcException('Failed to send request, no response received');
}
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$responseString = $response;
if (!Utilities::isSuccessCodes($responseCode))
{
curl_close($ch);
if (!empty($responseString))
{
throw new RpcException($responseString);
}
throw new RpcException('Failed to send request (Empty Response): ' . $responseCode);
}
if ($responseCode == 204)
{
curl_close($ch);
return [];
}
if (empty($responseString))
{
curl_close($ch);
throw new RpcException('The request was successful but the server did not indicate an empty response');
}
curl_close($ch);
$this->logger->debug(sprintf('Encrypted Response Size: %d', strlen($responseString)));
try
{
$decryptedResponse = Cryptography::decryptMessage(
encryptedMessage: $responseString,
encryptionKey: $this->clientTransportEncryptionKey,
algorithm: $this->serverInformation->getTransportEncryptionAlgorithm()
);
$this->logger->debug(sprintf('Decrypted Response: %s', $decryptedResponse));
}
catch (CryptographyException $e)
{
throw new RpcException('Failed to decrypt response: ' . $e->getMessage(), 0, $e);
}
$signature = $returnHeaders[strtolower(StandardHeaders::SIGNATURE->value)][0] ?? null;
$this->logger->debug(sprintf('Response Signature: %s', $signature));
if ($signature === null)
{
throw new RpcException('The server did not provide a signature for the response');
}
try
{
if(!Cryptography::verifyMessage(
message: $decryptedResponse,
signature: $signature,
publicKey: $this->serverPublicSigningKey,
))
{
throw new RpcException('Failed to verify the response signature');
}
}
catch (CryptographyException $e)
{
throw new RpcException('Failed to verify the response signature: ' . $e->getMessage(), 0, $e);
}
$decoded = json_decode($decryptedResponse, true);
if(isset($decoded['id']))
{
return [new RpcResult($decoded)];
}
else
{
$results = [];
foreach ($decoded as $responseMap)
{
$results[] = new RpcResult($responseMap);
}
return $results;
}
}
/**
* Retrieves server information by making an RPC request.
*
* @return ServerInformation The parsed server information received in the response.
* @throws RpcException If the request fails, no response is received, or the server returns an error status code or invalid data.
*/
public function getServerInformation(): ServerInformation
{
$ch = curl_init();
$headers = [
StandardHeaders::REQUEST_TYPE->value . ': ' . RequestType::INFO->value,
StandardHeaders::CLIENT_NAME->value . ': ' . self::CLIENT_NAME,
StandardHeaders::CLIENT_VERSION->value . ': ' . self::CLIENT_VERSION,
];
curl_setopt($ch, CURLOPT_URL, $this->rpcEndpoint);
curl_setopt($ch, CURLOPT_HTTPGET, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$this->logger->debug(sprintf('Getting server information from %s', $this->rpcEndpoint));
$this->logger->debug(sprintf('Headers: %s', json_encode($headers)));
$response = curl_exec($ch);
if($response === false)
{
curl_close($ch);
throw new RpcException(sprintf('Failed to get server information at %s, no response received', $this->rpcEndpoint));
}
$responseCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
if($responseCode !== 200)
{
curl_close($ch);
if(empty($response))
{
throw new RpcException(sprintf('Failed to get server information at %s, server responded with ' . $responseCode, $this->rpcEndpoint));
}
}
curl_close($ch);
return ServerInformation::fromArray(json_decode($response, true));
}
/**
* Sends an RPC request and retrieves the corresponding RPC response.
*
* @param RpcRequest $request The RPC request to be sent.
* @param bool $throwException Optional. Whether to throw an exception if the response contains an error.
* @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication.
* @return RpcResult The received RPC response.
* @throws RpcException If no response is received from the request.
*/
public function sendRequest(RpcRequest $request, bool $throwException=true, ?string $identifiedAs=null): RpcResult
{
$response = $this->sendRawRequest(json_encode($request->toArray()), $identifiedAs);
if (count($response) === 0)
{
throw new RpcException('Failed to send request, no response received');
}
if($throwException)
{
if($response[0]->getError() !== null)
{
throw $response[0]->getError()->toRpcException();
}
}
return $response[0];
}
/**
* Sends a batch of requests to the server, processes them into an appropriate format,
* and handles the response.
*
* @param RpcRequest[] $requests An array of RpcRequest objects to be sent to the server.
* @param string|null $identifiedAs Optional. The username to identify as, usually the requesting peer. Required for server-to-server communication.
* @return RpcResult[] An array of RpcResult objects received from the server.
* @throws RpcException If no response is received from the server.
*/
public function sendRequests(array $requests, ?string $identifiedAs=null): array
{
$parsedRequests = [];
foreach ($requests as $request)
{
$parsedRequests[] = $request->toArray();
}
$responses = $this->sendRawRequest(json_encode($parsedRequests), $identifiedAs);
if (count($responses) === 0)
{
return [];
}
return $responses;
}
/**
* Retrieves the peer address identified for the instance.
*
* @return PeerAddress The identified peer address.
*/
public function getIdentifiedAs(): PeerAddress
{
return $this->identifiedAs;
}
/**
* Retrieves the server's public signing key.
*
* @return string The server's public signing key.
*/
public function getServerPublicSigningKey(): string
{
return $this->serverPublicSigningKey;
}
/**
* Retrieves the server's public encryption key.
*
* @return string The public encryption key of the server.
*/
public function getServerPublicEncryptionKey(): string
{
return $this->serverPublicEncryptionKey;
}
/**
* Retrieves the client's signing key pair.
*
* @return KeyPair The client's signing key pair.
*/
public function getClientSigningKeyPair(): KeyPair
{
return $this->clientSigningKeyPair;
}
/**
* Retrieves the client encryption key pair configured for the instance.
*
* @return KeyPair The client encryption key pair.
*/
public function getClientEncryptionKeyPair(): KeyPair
{
return $this->clientEncryptionKeyPair;
}
/**
* Retrieves the private shared secret configured for the instance.
*
* @return string The private shared secret value.
*/
public function getPrivateSharedSecret(): string
{
return $this->privateSharedSecret;
}
/**
* Retrieves the client transport encryption key configured for the instance.
*
* @return string The client transport encryption key value.
*/
public function getClientTransportEncryptionKey(): string
{
return $this->clientTransportEncryptionKey;
}
/**
* Retrieves the server transport encryption key.
*
* @return string The server transport encryption key.
*/
public function getServerTransportEncryptionKey(): string
{
return $this->serverTransportEncryptionKey;
}
/**
* Retrieves the RPC endpoint configured for the instance.
*
* @return string The RPC endpoint value.
*/
public function getRpcEndpoint(): string
{
return $this->rpcEndpoint;
}
/**
* Retrieves the remote server address configured for the instance.
*
* @return string The remote server address.
*/
public function getRemoteServer(): string
{
return $this->remoteServer;
}
/**
* Retrieves the session UUID associated with the instance.
*
* @return string The session UUID value.
*/
public function getSessionUuid(): string
{
return $this->sessionUuid;
}
/**
* Returns the signing keys associated with the current instance.
*
* @return SignatureKeyPair[] The signing keys.
*/
public function getSigningKeys(): array
{
return $this->signingKeys;
}
/**
* Adds a new signing key to the current instance.
*
* @param SignatureKeyPair $key The signing key to be added.
* @return void
*/
protected function addSigningKey(SignatureKeyPair $key): void
{
$this->signingKeys[$key->getUuid()] = $key;
if($this->defaultSigningKey === null)
{
$this->defaultSigningKey = $key->getUuid();
}
}
/**
* @param string $uuid
* @return bool
*/
public function signingKeyExists(string $uuid): bool
{
return isset($this->signingKeys[$uuid]);
}
/**
* Removes a signing key from the current instance.
*
* @param string $uuid The UUID of the signing key to be removed.
* @return void
*/
protected function removeSigningKey(string $uuid): void
{
unset($this->signingKeys[$uuid]);
if($this->defaultSigningKey === $uuid)
{
$this->defaultSigningKey = null;
}
}
/**
* Retrieves the signing key associated with the specified UUID.
*
* @param string $uuid The UUID of the signing key to be retrieved.
* @return SignatureKeyPair|null The signing key associated with the UUID, or null if not found.
*/
public function getSigningKey(string $uuid): ?SignatureKeyPair
{
return $this->signingKeys[$uuid] ?? null;
}
/**
* Deletes a signing key from the current instance.
*
* @param string $uuid The UUID of the signing key to be deleted.
* @return void
*/
public function deleteSigningKey(string $uuid): void
{
unset($this->signingKeys[$uuid]);
}
/**
* Retrieves the default signing key associated with the current instance.
*
* @return SignatureKeyPair|null The default signing key.
*/
public function getDefaultSigningKey(): ?SignatureKeyPair
{
return $this->signingKeys[$this->defaultSigningKey] ?? null;
}
/**
* Sets the default signing key for the current instance.
*
* @param string $uuid The UUID of the signing key to be set as default.
* @return void
*/
public function setDefaultSigningKey(string $uuid): void
{
if(!isset($this->signingKeys[$uuid]))
{
throw new InvalidArgumentException('The specified signing key does not exist');
}
$this->defaultSigningKey = $uuid;
}
/**
* Retrieves the encryption channel keys associated with the current instance.
*
* @return EncryptionChannelSecret[] The encryption channel keys.
*/
public function getEncryptionChannelSecrets(): array
{
return $this->encryptionChannelSecrets;
}
public function getEncryptionChannelSecret(string $channelUuid): ?EncryptionChannelSecret
{
return $this->encryptionChannelSecrets[$channelUuid] ?? null;
}
/**
* Adds a new encryption channel key to the current instance.
*
* @param EncryptionChannelSecret $key The encryption channel key to be added.
* @return void
*/
public function addEncryptionChannelSecret(EncryptionChannelSecret $key): void
{
$this->encryptionChannelSecrets[$key->getChannelUuid()] = $key;
}
/**
* Removes an encryption channel key from the current instance.
*
* @param string $channelUuid The UUID of the encryption channel key to be removed.
* @return void
*/
public function removeEncryptionChannelKey(string $channelUuid): void
{
unset($this->encryptionChannelSecrets[$channelUuid]);
}
/**
* Retrieves the encryption channel key associated with the specified UUID.
*
* @param string $channelUuid The UUID of the encryption channel key to be retrieved.
* @return EncryptionChannelSecret|null The encryption channel key associated with the UUID, or null if not found.
*/
public function getEncryptionChannelKey(string $channelUuid): ?EncryptionChannelSecret
{
return $this->encryptionChannelSecrets[$channelUuid] ?? null;
}
/**
* Checks if an encryption channel key exists with the specified UUID.
*
* @param string $channelUuid The UUID of the encryption channel key to check.
* @return bool True if the encryption channel key exists, false otherwise.
*/
public function encryptionChannelKeyExists(string $channelUuid): bool
{
return isset($this->encryptionChannelSecrets[$channelUuid]);
}
/**
* Deletes an encryption channel key from the current instance.
*
* @param string $channelUuid The UUID of the encryption channel key to be deleted.
* @return void
*/
public function deleteEncryptionChannelKey(string $channelUuid): void
{
unset($this->encryptionChannelSecrets[$channelUuid]);
}
/**
* Exports the current session details into an ExportedSession object.
*
* @return ExportedSession The exported session containing session-specific details.
*/
public function exportSession(): ExportedSession
{
return new ExportedSession([
'peer_address' => $this->identifiedAs->getAddress(),
'rpc_endpoint' => $this->rpcEndpoint,
'remote_server' => $this->remoteServer,
'session_uuid' => $this->sessionUuid,
'transport_encryption_algorithm' => $this->serverInformation->getTransportEncryptionAlgorithm(),
'server_keypair_expires' => $this->serverInformation->getServerKeypairExpires(),
'server_public_signing_key' => $this->serverPublicSigningKey,
'server_public_encryption_key' => $this->serverPublicEncryptionKey,
'client_public_signing_key' => $this->clientSigningKeyPair->getPublicKey(),
'client_private_signing_key' => $this->clientSigningKeyPair->getPrivateKey(),
'client_public_encryption_key' => $this->clientEncryptionKeyPair->getPublicKey(),
'client_private_encryption_key' => $this->clientEncryptionKeyPair->getPrivateKey(),
'private_shared_secret' => $this->privateSharedSecret,
'client_transport_encryption_key' => $this->clientTransportEncryptionKey,
'server_transport_encryption_key' => $this->serverTransportEncryptionKey,
'default_signing_key' => $this->defaultSigningKey,
'signing_keys' => array_map(fn(SignatureKeyPair $key) => $key->toArray(), $this->signingKeys),
'encryption_channel_secrets' => array_map(fn(EncryptionChannelSecret $key) => $key->toArray(), $this->encryptionChannelSecrets)
]);
} }
} }

View file

@ -1,195 +0,0 @@
<?php
namespace Socialbox\Classes;
use InvalidArgumentException;
use RuntimeException;
use Socialbox\Enums\StandardHeaders;
use Socialbox\Exceptions\CryptographyException;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\StandardException;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class RpcHandler
{
/**
* Gets the incoming ClientRequest object, validates if the request is valid & if a session UUID is provided
* checks if the request signature matches the client's provided public key.
*
* @return ClientRequest The parsed ClientRequest object
* @throws RpcException Thrown if the request is invalid
*/
public static function getClientRequest(): ClientRequest
{
if($_SERVER['REQUEST_METHOD'] !== 'POST')
{
throw new RpcException('Invalid Request Method, expected POST', 400);
}
try
{
$headers = Utilities::getRequestHeaders();
foreach(StandardHeaders::getRequiredHeaders() as $header)
{
if (!isset($headers[$header]))
{
throw new RpcException("Missing required header: $header", 400);
}
// Validate the headers
switch(StandardHeaders::tryFrom($header))
{
case StandardHeaders::CLIENT_VERSION:
if($headers[$header] !== '1.0')
{
throw new RpcException(sprintf("Unsupported Client Version: %s", $headers[$header]));
}
break;
case StandardHeaders::CONTENT_TYPE:
if(!str_contains($headers[$header], 'application/json'))
{
throw new RpcException(sprintf("Invalid Content-Type header: Expected application/json, got %s", $headers[$header]), 400);
}
break;
case StandardHeaders::FROM_PEER:
if(!Validator::validatePeerAddress($headers[$header]))
{
throw new RpcException("Invalid From-Peer header: " . $headers[$header], 400);
}
break;
default:
break;
}
}
}
catch(RuntimeException $e)
{
throw new RpcException("Failed to parse request: " . $e->getMessage(), 400, $e);
}
$clientRequest = new ClientRequest($headers, self::getRpcRequests(), self::getRequestHash());
// Verify the session & request signature
if($clientRequest->getSessionUuid() !== null)
{
// If no signature is provided, it must be required if the client is providing a Session UUID
if($clientRequest->getSignature() === null)
{
throw new RpcException(sprintf('Unauthorized request, signature required for session based requests'), 401);
}
try
{
$session = SessionManager::getSession($clientRequest->getSessionUuid());
// Verify the signature of the request
if(!Cryptography::verifyContent($clientRequest->getHash(), $clientRequest->getSignature(), $session->getPublicKey()))
{
throw new RpcException('Request signature check failed', 400);
}
}
catch(StandardException $e)
{
throw new RpcException($e->getMessage(), 400);
}
catch(CryptographyException $e)
{
throw new RpcException('Request signature check failed (Cryptography Error)', 400, $e);
}
catch(DatabaseOperationException $e)
{
throw new RpcException('Failed to verify session', 500, $e);
}
}
return $clientRequest;
}
/**
* Returns the request hash by hashing the request body using SHA256
*
* @return string Returns the request hash in SHA256 representation
*/
private static function getRequestHash(): string
{
return hash('sha1', file_get_contents('php://input'));
}
/**
* Handles a POST request, returning an array of RpcRequest objects
* expects a JSON encoded body with either a single RpcRequest object or an array of RpcRequest objects
*
* @return RpcRequest[] The parsed RpcRequest objects
* @throws RpcException Thrown if the request is invalid
*/
private static function getRpcRequests(): array
{
try
{
// Decode the request body
$body = Utilities::jsonDecode(file_get_contents('php://input'));
}
catch(InvalidArgumentException $e)
{
throw new RpcException("Invalid JSON in request body: " . $e->getMessage(), 400, $e);
}
if(isset($body['method']))
{
// If it only contains a method, we assume it's a single request
return [self::parseRequest($body)];
}
// Otherwise, we assume it's an array of requests
return array_map(fn($request) => self::parseRequest($request), $body);
}
/**
* Parses the raw request data into an RpcRequest object
*
* @param array $data The raw request data
* @return RpcRequest The parsed RpcRequest object
* @throws RpcException If the request is invalid
*/
private static function parseRequest(array $data): RpcRequest
{
if(!isset($data['method']))
{
throw new RpcException("Missing 'method' key in request", 400);
}
if(isset($data['id']))
{
if(!is_string($data['id']))
{
throw new RpcException("Invalid 'id' key in request: Expected string", 400);
}
if(strlen($data['id']) === 0)
{
throw new RpcException("Invalid 'id' key in request: Expected non-empty string", 400);
}
if(strlen($data['id']) > 8)
{
throw new RpcException("Invalid 'id' key in request: Expected string of length <= 8", 400);
}
}
if(isset($data['parameters']))
{
if(!is_array($data['parameters']))
{
throw new RpcException("Invalid 'parameters' key in request: Expected array", 400);
}
}
return new RpcRequest($data['method'], $data['id'] ?? null, $data['parameters'] ?? null);
}
}

View file

@ -1,52 +1,154 @@
<?php <?php
namespace Socialbox\Classes; namespace Socialbox\Classes;
use Socialbox\Exceptions\ResolutionException; use InvalidArgumentException;
use Socialbox\Objects\ResolvedServer; use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\ResolutionException;
use Socialbox\Managers\ResolvedDnsRecordsManager;
use Socialbox\Objects\DnsRecord;
class ServerResolver class ServerResolver
{
/**
* Resolves a given domain to fetch the RPC endpoint and public key from its DNS TXT records.
*
* @param string $domain The domain to be resolved.
* @return ResolvedServer An instance of ResolvedServer containing the endpoint and public key.
* @throws ResolutionException If the DNS TXT records cannot be resolved or if required information is missing.
*/
public static function resolveDomain(string $domain): ResolvedServer
{ {
$txtRecords = dns_get_record($domain, DNS_TXT); private static array $mockedRecords = [];
if ($txtRecords === false) /**
* Resolves a domain by retrieving and parsing its DNS TXT records.
* Optionally checks a database for cached resolution data before performing a DNS query.
*
* @param string $domain The domain name to resolve.
* @param bool $useDatabase Whether to check the database for cached resolution data; defaults to true.
* @return DnsRecord The parsed DNS record for the given domain.
* @throws ResolutionException If the DNS TXT records cannot be retrieved or parsed.
* @throws DatabaseOperationException If an error occurs while interacting with the database. (Only if $useDatabase is true)
*/
public static function resolveDomain(string $domain, bool $useDatabase=true): DnsRecord
{ {
throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain)); // Return the mocked record if the mocking record is set
} if(isset(self::$mockedRecords[$domain]))
$endpoint = null;
$publicKey = null;
foreach ($txtRecords as $txt)
{
if (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox='))
{ {
$endpoint = substr($txt['txt'], strlen('socialbox=')); return self::$mockedRecords[$domain];
} }
elseif (isset($txt['txt']) && str_starts_with($txt['txt'], 'socialbox-key='))
// Return the mocked record from the configuration if one is set
if(isset(Configuration::getInstanceConfiguration()->getDnsMocks()[$domain]))
{ {
$publicKey = substr($txt['txt'], strlen('socialbox-key=')); return DnsHelper::parseTxt(Configuration::getInstanceConfiguration()->getDnsMocks()[$domain]);
}
// Check the database if enabled
if ($useDatabase)
{
// Return from the database cache if one exists
// TODO: Implement renewal here
$resolvedServer = ResolvedDnsRecordsManager::getDnsRecord($domain);
if ($resolvedServer !== null)
{
return $resolvedServer;
}
}
// Resolve DNS & Records
$txtRecords = self::dnsGetTxtRecords($domain);
if ($txtRecords === false)
{
throw new ResolutionException(sprintf("Failed to resolve DNS TXT records for %s", $domain));
}
$fullRecord = self::concatenateTxtRecords($txtRecords);
try
{
// Parse the TXT record using DnsHelper
$record = DnsHelper::parseTxt($fullRecord);
// Cache the resolved server record in the database
if($useDatabase)
{
ResolvedDnsRecordsManager::addResolvedServer($domain, $record);
}
return $record;
}
catch (InvalidArgumentException $e)
{
throw new ResolutionException(sprintf("Failed to find valid SocialBox record for %s: %s", $domain, $e->getMessage()));
} }
} }
if ($endpoint === null) /**
* Retrieves the TXT records for a given domain using the dns_get_record function.
*
* @param string $domain The domain name to fetch TXT records for.
* @return array|false An array of DNS TXT records on success, or false on failure.
*/
private static function dnsGetTxtRecords(string $domain): array|false
{ {
throw new ResolutionException(sprintf("Failed to resolve RPC endpoint for %s", $domain)); return @dns_get_record($domain, DNS_TXT);
} }
if ($publicKey === null) /**
* Concatenates an array of TXT records into a single string, filtering for SocialBox records.
*
* @param array $txtRecords An array of TXT records, where each record is expected to have a 'txt' key.
* @return string A concatenated string of all relevant TXT records.
*/
private static function concatenateTxtRecords(array $txtRecords): string
{ {
throw new ResolutionException(sprintf("Failed to resolve public key for %s", $domain)); $fullRecordBuilder = '';
foreach ($txtRecords as $txt)
{
if (isset($txt['txt']))
{
$record = trim($txt['txt'], '" ');
// Only include records that start with v=socialbox
if (stripos($record, 'v=socialbox') === 0)
{
$fullRecordBuilder .= $record;
}
}
}
return $fullRecordBuilder;
} }
return new ResolvedServer($endpoint, $publicKey); /**
} * Retrieves the mocked records.
} *
* @return array The list of mocked records.
*/
public static function getMockedRecords(): array
{
return self::$mockedRecords;
}
/**
* Adds a mock DNS record for a specific domain.
*
* @param string $domain The domain name for which the DNS record is being mocked.
* @param DnsRecord $record The DNS record to be associated with the specified domain.
* @return void
*/
public static function addMock(string $domain, DnsRecord|string $record): void
{
if(isset(self::$mockedRecords[$domain]))
{
return;
}
if(is_string($record))
{
$record = DnsHelper::parseTxt($record);
}
self::$mockedRecords[$domain] = $record;
}
/**
* Clears all mocked records by resetting the mocked records array.
*
* @return void
*/
public static function clearMockedRecords(): void
{
self::$mockedRecords = [];
}
}

View file

@ -0,0 +1,85 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Types\ContactRelationshipType;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class AddressBookAddContact extends Method
{
/**
* Adds a contact to the authenticated peer's address book, returns True if the contact was added
* false if the contact already exists.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
if($rpcRequest->containsParameter('relationship') && $rpcRequest->getParameter('relationship') !== null)
{
$relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship')));
if($relationship === null)
{
throw new InvalidRpcArgumentException('relationship');
}
}
else
{
$relationship = ContactRelationshipType::MUTUAL;
}
try
{
$peer = $request->getPeer();
if($peer->getAddress() == $address)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Cannot add self as contact');
}
// Resolve the peer, this would throw a StandardException if something goes wrong
Socialbox::resolvePeer($address);
// Check if the contact already exists
if(ContactManager::isContact($peer, $address))
{
return $rpcRequest->produceResponse(false);
}
// Create the contact
ContactManager::createContact($peer, $address, $relationship);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to add contact', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
class AddressBookContactExists extends Method
{
/**
* Returns True if the contact exists in the address book, False otherwise.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
try
{
$peer = $request->getPeer();
return $rpcRequest->produceResponse(ContactManager::isContact($peer, $address));
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to check if the contact exists', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,62 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
class AddressBookDeleteContact extends Method
{
/**
* Deletes a contact from the authenticated peer's address book, returns True if the contact was deleted
* false if the contact does not exist.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
try
{
// Check if the contact already exists
$peer = $request->getPeer();
if(!ContactManager::isContact($peer, $address))
{
return $rpcRequest->produceResponse(false);
}
// Create the contact
ContactManager::deleteContact($peer, $address);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to remove contact', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
class AddressBookGetContact extends Method
{
/**
* Returns the contact information for the given peer address.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
try
{
if(!ContactManager::isContact($request->getPeer(), $address))
{
// Return empty response if the contact does not exist
return $rpcRequest->produceResponse();
}
$rpcRequest->produceResponse(ContactManager::getStandardContact($request->getPeer(), $address));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to get contact', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Configuration;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class AddressBookGetContacts extends Method
{
/**
* Returns the contacts in the address book.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
$limit = Configuration::getPoliciesConfiguration()->getGetContactsLimit();
if($rpcRequest->containsParameter('limit', true))
{
$limit = (int)$rpcRequest->getParameter('limit');
if($limit <= 0)
{
throw new InvalidRpcArgumentException('limit', 'Invalid limit, must be greater than 0');
}
$limit = min($limit, Configuration::getPoliciesConfiguration()->getGetContactsLimit());
}
$page = 0;
if($rpcRequest->containsParameter('page', true))
{
$page = (int)$rpcRequest->getParameter('page');
if($page < 0)
{
throw new InvalidRpcArgumentException('page', 'Invalid page, must be greater than or equal to 0');
}
$page = max($page, 0);
}
try
{
return $rpcRequest->produceResponse(ContactManager::getStandardContacts($request->getPeer(), $limit, $page));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to get contacts', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Symfony\Component\Uid\Uuid;
class AddressBookRevokeSignature extends Method
{
/**
* @inheritDoc
* @noinspection DuplicatedCode
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
if(!$rpcRequest->containsParameter('signature_uuid'))
{
throw new MissingRpcArgumentException('signature_uuid');
}
try
{
$signatureUuid = Uuid::fromString($rpcRequest->getParameter('signature_uuid'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('signature_uuid', $e);
}
try
{
// Check if the contact already exists
$peer = $request->getPeer();
$contact = ContactManager::getContact($peer, $address);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to check contact state with calling peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($contact === null)
{
return $rpcRequest->produceResponse(false);
}
try
{
if(!ContactManager::contactSigningKeyUuidExists($contact, $signatureUuid))
{
return $rpcRequest->produceResponse(false);
}
ContactManager::removeContactSigningKey($contact, $signatureUuid);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to remove contact signature', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,105 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Configuration;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
use Symfony\Component\Uid\Uuid;
class AddressBookTrustSignature extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
if(!$rpcRequest->containsParameter('signature_uuid'))
{
throw new MissingRpcArgumentException('signature_uuid');
}
try
{
$signatureUuid = Uuid::fromString($rpcRequest->getParameter('signature_uuid'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('signature_uuid', $e);
}
$signingKey = Socialbox::resolvePeerSignature($address, $signatureUuid);
try
{
// Check if the contact already exists
$peer = $request->getPeer();
if(!ContactManager::isContact($peer, $address))
{
ContactManager::createContact($peer, $address);
}
$contact = ContactManager::getContact($peer, $address);
if(ContactManager::contactGetSigningKeysCount($contact) > Configuration::getPoliciesConfiguration()->getMaxContactSigningKeys())
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The contact has exceeded the maximum amount of trusted signatures');
}
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to check contact state with calling peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($signingKey === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested signature key was not found');
}
try
{
if(ContactManager::contactSigningKeyUuidExists($contact, $signingKey->getUuid()))
{
return $rpcRequest->produceResponse(false);
}
if(ContactManager::contactSigningKeyExists($contact, $signingKey->getPublicKey()))
{
return $rpcRequest->produceResponse(false);
}
ContactManager::addContactSigningKey($contact, $signingKey);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to trust contact signature', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Socialbox\Classes\StandardMethods\AddressBook;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Types\ContactRelationshipType;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\ContactManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
class AddressBookUpdateRelationship extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
try
{
$address = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
if(!$rpcRequest->containsParameter('relationship'))
{
throw new MissingRpcArgumentException('relationship');
}
$relationship = ContactRelationshipType::tryFrom(strtoupper($rpcRequest->getParameter('relationship')));
if($relationship === null)
{
throw new InvalidRpcArgumentException('relationship');
}
try
{
// Check if the contact already exists
$peer = $request->getPeer();
if(!ContactManager::isContact($peer, $address))
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'Contact does not exist');
}
// Create the contact
ContactManager::updateContactRelationship($peer, $address, $relationship);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to update contact relationship', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Return success
return $rpcRequest->produceResponse(true);
}
}

View file

@ -1,50 +0,0 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\FirstLevelAuthentication;
use Socialbox\Enums\StandardError;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Objects\RpcResponse;
class Authenticate extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!isset($rpcRequest->getParameters()['type']))
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing required parameter \'type\'');
}
if(strlen($rpcRequest->getParameters()['type']) == 0)
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Parameter \'type\' cannot be empty');
}
return match (FirstLevelAuthentication::tryFrom($rpcRequest->getParameters()['type']))
{
FirstLevelAuthentication::PASSWORD => self::handlePassword($request),
default => $rpcRequest->produceError(StandardError::UNSUPPORTED_AUTHENTICATION_TYPE,
sprintf('Unsupported authentication type: %s', $rpcRequest->getParameters()['type'])
),
};
}
/**
* Handles the password authentication phase for the peer
*
* @param ClientRequest $request
* @return SerializableInterface
*/
private static function handlePassword(ClientRequest $request): SerializableInterface
{
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\StandardMethods;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class GetAllowedMethods extends Method
{
/**
* Returns a list of allowed methods for the current session.
*
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
$allowedMethods = [];
try
{
foreach(StandardMethods::getAllowedMethods($request) as $method)
{
$allowedMethods[] = $method->value;
}
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve allowed methods due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse($allowedMethods);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\PeerInformationManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class GetSelf extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
$selfPeer = $request->getPeer();
return $rpcRequest->produceResponse($selfPeer->toStandardPeer(PeerInformationManager::getFields($selfPeer)));
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Unable to resolve self peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class GetSessionState extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
return $rpcRequest->produceResponse($request->getSession()->toStandardSessionState());
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve session state due to an internal exception', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use Socialbox\Abstracts\Method;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class Ping extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\ReservedUsernames;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class ResolvePeer extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
// Check if the required 'peer' parameter is set.
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
// Parse the peer address
try
{
$peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new StandardRpcException('Peer Address Error: ' . $e->getMessage(), StandardError::RPC_INVALID_ARGUMENTS, $e);
}
// Check if host is making the request & the identifier is not empty
try
{
$identifyAs = null;
if ($request->getPeer()->getUsername() == ReservedUsernames::HOST && $request->getIdentifyAs() != null)
{
$identifyAs = $request->getIdentifyAs();
}
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve peer information', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Resolve the peer using the server's peer resolver, this will resolve both internal peers and external peers
return $rpcRequest->produceResponse(Socialbox::resolvePeer($peerAddress, $identifyAs));
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use Exception;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
use Symfony\Component\Uid\Uuid;
class ResolveSignature extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
// Check if the required 'peer' parameter is set.
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
if(!$rpcRequest->containsParameter('signature_uuid'))
{
throw new MissingRpcArgumentException('signature_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('signature_uuid')))
{
throw new InvalidRpcArgumentException('signature_uuid', 'Invalid UUID V4');
}
return $rpcRequest->produceResponse(Socialbox::resolvePeerSignature(
$rpcRequest->getParameter('peer'), $rpcRequest->getParameter('signature_uuid')
));
}
}

View file

@ -0,0 +1,87 @@
<?php
namespace Socialbox\Classes\StandardMethods\Core;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Validator;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class VerifySignature extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
// Check if the required 'peer' parameter is set.
if(!$rpcRequest->containsParameter('peer'))
{
throw new MissingRpcArgumentException('peer');
}
if(!$rpcRequest->containsParameter('signature_uuid'))
{
throw new MissingRpcArgumentException('signature_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('signature_uuid')))
{
throw new InvalidRpcArgumentException('signature_uuid', 'Invalid UUID V4');
}
if(!$rpcRequest->containsParameter('signature'))
{
throw new MissingRpcArgumentException('signature');
}
if(!$rpcRequest->containsParameter('sha512'))
{
throw new MissingRpcArgumentException('sha512');
}
elseif(!Cryptography::validateSha512($rpcRequest->getParameter('sha512')))
{
throw new InvalidRpcArgumentException('sha512', 'Invalid SHA512');
}
// Parse the peer address
try
{
$peerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('peer'));
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException('peer', $e);
}
if($rpcRequest->containsParameter('time'))
{
if(!is_numeric($rpcRequest->getParameter('time')))
{
throw new InvalidRpcArgumentException('time', 'Invalid timestamp, must be a Unix Timestamp');
}
return $rpcRequest->produceResponse(Socialbox::verifyTimedSignature(
signingPeer: $peerAddress,
signatureUuid: $rpcRequest->getParameter('signature_uuid'),
signature: $rpcRequest->getParameter('signature'),
messageHash: $rpcRequest->getParameter('sha512'),
signatureTime: (int)$rpcRequest->getParameter('time')
)->value);
}
return $rpcRequest->produceResponse(Socialbox::verifySignature(
signingPeer: $peerAddress,
signatureUuid: $rpcRequest->getParameter('signature_uuid'),
signature: $rpcRequest->getParameter('signature'),
messageHash: $rpcRequest->getParameter('sha512'),
)->value);
}
}

View file

@ -1,47 +0,0 @@
<?php
namespace Socialbox\Classes\StandardMethods;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\SessionManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class CreateSession extends Method
{
/**
* Executes the session creation process based on the provided public key.
*
* @param ClientRequest $request The client request object.
* @param RpcRequest $rpcRequest The RPC request containing parameters for execution.
* @return SerializableInterface|null Returns a response with the session UUID or an error.
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('public_key'))
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, 'Missing parameter \'public_key\'');
}
try
{
$uuid = SessionManager::createSession($rpcRequest->getParameter('public_key'));
}
catch(DatabaseOperationException $e)
{
return $rpcRequest->produceError(StandardError::INTERNAL_SERVER_ERROR, 'There was an error while trying to create a new session: ' . $e->getMessage());
}
catch(InvalidArgumentException $e)
{
return $rpcRequest->produceError(StandardError::RPC_INVALID_ARGUMENTS, $e->getMessage());
}
return $rpcRequest->produceResponse([
'uuid' => $uuid
]);
}
}

View file

@ -0,0 +1,209 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionAcceptChannel extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
if ($request->isExternal())
{
return self::handleExternal($request, $rpcRequest);
}
return self::handleInternal($request, $rpcRequest);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
if(!$rpcRequest->containsParameter('public_encryption_key'))
{
throw new MissingRpcArgumentException('public_encryption_key');
}
elseif(!Cryptography::validatePublicEncryptionKey('public_encryption_key'))
{
throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid');
}
try
{
$receivingPeer = $request->getPeer();
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif($encryptionChannel->getReceivingPeerAddress()->getAddress() !== $receivingPeer->getAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver');
}
if($encryptionChannel->getCallingPeerAddress()->isExternal())
{
try
{
$rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain());
$rpcClient->encryptionAcceptChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'),
identifiedAs: $receivingPeer->getAddress()
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('Error declining channel as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
try
{
EncryptionChannelManager::acceptChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key')
);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to accept the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing required header IdentifyAs');
}
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
if(!$rpcRequest->containsParameter('public_encryption_key'))
{
throw new MissingRpcArgumentException('public_encryption_key');
}
elseif(!Cryptography::validatePublicEncryptionKey('public_encryption_key'))
{
throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid');
}
try
{
$receivingPeer = Socialbox::resolvePeer($request->getIdentifyAs());
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getPeerAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver');
}
try
{
EncryptionChannelManager::acceptChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key')
);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to accept the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,221 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\Database\EncryptionChannelRecord;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionChannelAcknowledgeMessage extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
if(!$rpcRequest->containsParameter('message_uuid'))
{
throw new MissingRpcArgumentException('message_uuid');
}
elseif(!is_string($rpcRequest->getParameter('message_uuid')))
{
throw new InvalidRpcArgumentException('message_uuid', 'Must be type string');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('message_uuid')))
{
throw new InvalidRpcArgumentException('message_uuid', 'Invalid message UUID V4');
}
try
{
$channelUuid = $rpcRequest->getParameter('channel_uuid');
$encryptionChannel = EncryptionChannelManager::getChannel($channelUuid);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist');
}
try
{
if ($request->isExternal())
{
return self::handleExternal($request, $rpcRequest, $encryptionChannel);
}
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return self::handleInternal($request, $rpcRequest, $encryptionChannel);
}
/**
* Handles the external execution of the method.
*
* @param ClientRequest $request The client request instance.
* @param RpcRequest $rpcRequest The RPC request instance.
* @param EncryptionChannelRecord $encryptionChannel The encryption channel record.
* @return SerializableInterface|null The response to the request.
* @throws StandardRpcException If an error occurs.
*/
public static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is missing');
}
$requestingPeerAddress = $request->getIdentifyAs();
if(!$encryptionChannel->isParticipant($requestingPeerAddress))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
try
{
$message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'));
if($message === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist');
}
if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeerAddress)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer');
}
EncryptionChannelManager::acknowledgeMessage(
$rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
/**
* Handles the internal execution of the method.
*
* @param ClientRequest $request The client request instance.
* @param RpcRequest $rpcRequest The RPC request instance.
* @param EncryptionChannelRecord $encryptionChannel The encryption channel record.
* @return SerializableInterface|null The response to the request.
* @throws StandardRpcException If an error occurs.
*/
public static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface
{
try
{
$requestingPeer = $request->getPeer();
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($requestingPeer === null)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized');
}
if(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened');
}
try
{
$message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'));
if($message === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist');
}
if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeer->getAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer');
}
EncryptionChannelManager::acknowledgeMessage(
$rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($message->getOwner($encryptionChannel)->isExternal())
{
try
{
$rpcClient = Socialbox::getExternalSession($message->getOwner($encryptionChannel)->getDomain());
$rpcClient->encryptionChannelAcknowledgeMessage(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
messageUuid: $rpcRequest->getParameter('message_uuid'),
identifiedAs: $requestingPeer->getAddress()
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::rejectMessage($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'), true);
}
catch (DatabaseOperationException $e)
{
Logger::getLogger()->error('Error rejecting message as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('Failed to acknowledge the message with the external server', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Socialbox\Abstracts\Method;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class EncryptionChannelExists extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
try
{
return $rpcRequest->produceResponse(EncryptionChannelManager::channelUuidExists($rpcRequest->getParameter('channel_uuid')));
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while checking if the channel exists', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class EncryptionChannelReceive extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
try
{
$channelUuid = $rpcRequest->getParameter('channel_uuid');
$encryptionChannel = EncryptionChannelManager::getChannel($channelUuid);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist');
}
try
{
$requestingPeer = $request->getPeer();
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($requestingPeer === null)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized');
}
if(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened');
}
$acknowledge = false;
if($rpcRequest->containsParameter('acknowledge') && is_bool($rpcRequest->getParameter('acknowledge')) && $rpcRequest->getParameter('acknowledge'))
{
$acknowledge = true;
}
try
{
$messages = EncryptionChannelManager::receiveData(
$rpcRequest->getParameter('channel_uuid'), $encryptionChannel->determineRecipient($requestingPeer->getAddress(), true)
);
if($acknowledge)
{
EncryptionChannelManager::acknowledgeMessagesBatch($rpcRequest->getParameter('channel_uuid'), array_map(fn($message) => $message->getUuid(), $messages));
}
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the messages', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(array_map(fn($message) => $message->toStandard(), $messages));
}
}

View file

@ -0,0 +1,221 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\Database\EncryptionChannelRecord;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionChannelRejectMessage extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
if(!$rpcRequest->containsParameter('message_uuid'))
{
throw new MissingRpcArgumentException('message_uuid');
}
elseif(!is_string($rpcRequest->getParameter('message_uuid')))
{
throw new InvalidRpcArgumentException('message_uuid', 'Must be type string');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('message_uuid')))
{
throw new InvalidRpcArgumentException('message_uuid', 'Invalid message UUID V4');
}
try
{
$channelUuid = $rpcRequest->getParameter('channel_uuid');
$encryptionChannel = EncryptionChannelManager::getChannel($channelUuid);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist');
}
try
{
if ($request->isExternal())
{
return self::handleExternal($request, $rpcRequest, $encryptionChannel);
}
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to reject the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return self::handleInternal($request, $rpcRequest, $encryptionChannel);
}
/**
* Handles the external execution of the method.
*
* @param ClientRequest $request The client request instance.
* @param RpcRequest $rpcRequest The RPC request instance.
* @param EncryptionChannelRecord $encryptionChannel The encryption channel record.
* @return SerializableInterface|null The response to the request.
* @throws StandardRpcException If an error occurs.
*/
public static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is missing');
}
$requestingPeerAddress = $request->getIdentifyAs();
if(!$encryptionChannel->isParticipant($requestingPeerAddress))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
try
{
$message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'));
if($message === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist');
}
if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeerAddress)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer');
}
EncryptionChannelManager::rejectMessage(
$rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to reject the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
/**
* Handles the internal execution of the method.
*
* @param ClientRequest $request The client request instance.
* @param RpcRequest $rpcRequest The RPC request instance.
* @param EncryptionChannelRecord $encryptionChannel The encryption channel record.
* @return SerializableInterface|null The response to the request.
* @throws StandardRpcException If an error occurs.
*/
public static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest, EncryptionChannelRecord $encryptionChannel): ?SerializableInterface
{
try
{
$requestingPeer = $request->getPeer();
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($requestingPeer === null)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized');
}
if(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened');
}
try
{
$message = EncryptionChannelManager::getMessageRecord($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'));
if($message === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The message does not exist');
}
if($message->getReceiver($encryptionChannel)->getAddress() !== $requestingPeer->getAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The message is not for the requesting peer');
}
EncryptionChannelManager::acknowledgeMessage(
$rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to acknowledge the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($message->getOwner($encryptionChannel)->isExternal())
{
try
{
$rpcClient = Socialbox::getExternalSession($message->getOwner($encryptionChannel)->getDomain());
$rpcClient->encryptionChannelRejectMessage(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
messageUuid: $rpcRequest->getParameter('message_uuid'),
identifiedAs: $requestingPeer->getAddress()
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::rejectMessage($rpcRequest->getParameter('channel_uuid'), $rpcRequest->getParameter('message_uuid'), true);
}
catch (DatabaseOperationException $e)
{
Logger::getLogger()->error('Error rejecting message as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('Failed to acknowledge the message with the external server', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,237 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
use Symfony\Component\Uid\Uuid;
class EncryptionChannelSend extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
try
{
if ($request->isExternal())
{
return self::executeExternal($request, $rpcRequest);
}
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return self::executeInternal($request, $rpcRequest);
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface
* @throws StandardRpcException
*/
private static function executeInternal(ClientRequest $request, RpcRequest $rpcRequest): SerializableInterface
{
try
{
$channelUuid = $rpcRequest->getParameter('channel_uuid');
$encryptionChannel = EncryptionChannelManager::getChannel($channelUuid);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist');
}
try
{
$requestingPeer = $request->getPeer();
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($requestingPeer === null)
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The peer is not authorized');
}
if(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::OPENED)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not opened');
}
if(!$rpcRequest->containsParameter('checksum'))
{
throw new MissingRpcArgumentException('checksum');
}
elseif(!Cryptography::validateSha512($rpcRequest->getParameter('checksum')))
{
throw new InvalidRpcArgumentException('checksum', 'The given checksum is not a valid SHA-512 checksum');
}
if(!$rpcRequest->containsParameter('data'))
{
throw new MissingRpcArgumentException('data');
}
try
{
$messageUuid = Uuid::v4()->toRfc4122();
$messageTimestamp = time();
EncryptionChannelManager::sendMessage(
channelUuid: $channelUuid,
recipient: $encryptionChannel->determineRecipient($requestingPeer->getAddress()),
checksum: $rpcRequest->getParameter('checksum'),
data: $rpcRequest->getParameter('data'),
messageUuid: $messageUuid,
messageTimestamp: $messageTimestamp
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to send the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel->determineReceiver($requestingPeer->getAddress())->isExternal())
{
try
{
$rpcClient = Socialbox::getExternalSession($encryptionChannel->determineReceiver($requestingPeer->getAddress())->getDomain());
$rpcClient->encryptionChannelSend(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
checksum: $rpcRequest->getParameter('checksum'),
data: $rpcRequest->getParameter('data'),
identifiedAs: $requestingPeer->getAddress(),
messageUuid: $messageUuid,
timestamp: $messageTimestamp
);
}
catch(Exception $e)
{
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
return $rpcRequest->produceResponse();
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface
* @throws StandardRpcException
*/
private static function executeExternal(ClientRequest $request, RpcRequest $rpcRequest): SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'The IdentifyAs header is required');
}
try
{
$channelUuid = $rpcRequest->getParameter('channel_uuid');
$encryptionChannel = EncryptionChannelManager::getChannel($channelUuid);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to retrieve the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The encryption channel does not exist');
}
elseif(!$encryptionChannel->isParticipant($request->getIdentifyAs()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The encryption channel is not accessible');
}
if(!$rpcRequest->containsParameter('checksum'))
{
throw new MissingRpcArgumentException('checksum');
}
if(!$rpcRequest->containsParameter('data'))
{
throw new MissingRpcArgumentException('data');
}
if(!$rpcRequest->containsParameter('message_uuid'))
{
throw new MissingRpcArgumentException('message_uuid');
}
if(!$rpcRequest->containsParameter('timestamp'))
{
throw new MissingRpcArgumentException('timestamp');
}
elseif(!is_int($rpcRequest->getParameter('timestamp')))
{
throw new InvalidRpcArgumentException('timestamp', 'The given timestamp must be type integer');
}
try
{
EncryptionChannelManager::sendMessage(
channelUuid: $channelUuid,
recipient: $encryptionChannel->determineRecipient($request->getIdentifyAs()),
checksum: $rpcRequest->getParameter('checksum'),
data: $rpcRequest->getParameter('data'),
messageUuid: $rpcRequest->getParameter('message_uuid'),
messageTimestamp: (int)$rpcRequest->getParameter('timestamp')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('Failed to send the message', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,170 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionCloseChannel extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
if($request->isExternal())
{
return self::handleExternal($request, $rpcRequest);
}
return self::handleInternal($request, $rpcRequest);
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
$requestingPeer = $request->getPeer();
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() === EncryptionChannelStatus::CLOSED)
{
return $rpcRequest->produceResponse(false);
}
try
{
EncryptionChannelManager::closeChannel($encryptionChannel->getUuid());
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while trying to close the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
$externalPeer = $encryptionChannel->getExternalPeer();
if($externalPeer !== null)
{
try
{
$rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain());
$rpcClient->encryptionCloseChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
identifiedAs: $requestingPeer->getAddress()
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('Error declining channel as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
return $rpcRequest->produceResponse(true);
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
throw new StandardRpcException('The IdentifyAs field is required for external requests', StandardError::UNAUTHORIZED);
}
try
{
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
if(!$encryptionChannel->isParticipant($request->getIdentifyAs()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
if($encryptionChannel->getStatus() === EncryptionChannelStatus::CLOSED)
{
return $rpcRequest->produceResponse(false);
}
try
{
EncryptionChannelManager::closeChannel($encryptionChannel->getUuid());
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while trying to close the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,263 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use InvalidArgumentException;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Cryptography;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Managers\RegisteredPeerManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\PeerAddress;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionCreateChannel extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
if ($request->isExternal())
{
return self::handleExternal($request, $rpcRequest);
}
return self::handleInternal($request, $rpcRequest);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('receiving_peer'))
{
throw new MissingRpcArgumentException('receiving_peer');
}
elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer')))
{
throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address');
}
if(!$rpcRequest->containsParameter('public_encryption_key'))
{
throw new MissingRpcArgumentException('public_encryption_key');
}
elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key')))
{
throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid');
}
$receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer'));
Socialbox::resolvePeer($receivingPeerAddress);
try
{
$callingPeer = $request->getPeer();
$callingPeerAddress = PeerAddress::fromAddress($callingPeer->getAddress());
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the calling peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
try
{
$uuid = EncryptionChannelManager::createChannel(
callingPeer: $callingPeerAddress,
receivingPeer: $receivingPeerAddress,
callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_ke')
);
}
catch(InvalidArgumentException $e)
{
throw new InvalidRpcArgumentException(null, $e);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to create a new encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// If the receiver is in an external server, we must notify the external server as a client
if($receivingPeerAddress->isExternal())
{
// Obtain the RPC Client, if for any reason it fails; we set the encryption channel as declined.
try
{
$rpcClient = Socialbox::getExternalSession($receivingPeerAddress->getDomain());
$externalUuid = $rpcClient->encryptionCreateChannel(
receivingPeer: $receivingPeerAddress,
publicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'),
channelUuid: $uuid,
identifiedAs: $callingPeerAddress
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::declineChannel($uuid, true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('Error declining channel as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
// Check for sanity reasons
if($externalUuid !== $uuid)
{
try
{
EncryptionChannelManager::declineChannel($uuid, true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('Error declining channel as server', $e);
}
throw new StandardRpcException('The external server did not return the correct UUID', StandardError::UUID_MISMATCH);
}
}
return null;
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing IdentifyAs request header');
}
$callingPeer = $request->getIdentifyAs();
Socialbox::resolvePeer($callingPeer);
if(!$rpcRequest->containsParameter('receiving_peer'))
{
throw new MissingRpcArgumentException('receiving_peer');
}
elseif(!Validator::validatePeerAddress($rpcRequest->getParameter('receiving_peer')))
{
throw new InvalidRpcArgumentException('receiving_peer', 'Invalid Receiving Peer Address');
}
if(!$rpcRequest->containsParameter('public_encryption_key'))
{
throw new MissingRpcArgumentException('public_encryption_key');
}
elseif(!Cryptography::validatePublicEncryptionKey($rpcRequest->getParameter('public_encryption_key')))
{
throw new InvalidRpcArgumentException('public_encryption_key', 'The given public encryption key is invalid');
}
// Check for an additional required parameter 'channel_uuid'
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given UUID is not a valid UUID v4 format');
}
// Check if the UUID already is used on this server
try
{
if(EncryptionChannelManager::channelUuidExists($rpcRequest->getParameter('channel_uuid')))
{
return $rpcRequest->produceError(StandardError::UUID_CONFLICT, 'The given UUID already exists with another existing encryption channel on this server');
}
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while checking the existence of the channel UUID', StandardError::INTERNAL_SERVER_ERROR, $e);
}
$receivingPeerAddress = PeerAddress::fromAddress($rpcRequest->getParameter('receiving_peer'));
if($receivingPeerAddress->isExternal())
{
return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not belong to this server');
}
try
{
$receivingPeer = RegisteredPeerManager::getPeerByAddress($rpcRequest->getParameter('receiving_peer'));
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the receiving peer', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($receivingPeer === null)
{
return $rpcRequest->produceError(StandardError::PEER_NOT_FOUND, 'The receiving peer does not exist on this server');
}
try
{
$uuid = EncryptionChannelManager::createChannel(
callingPeer: $callingPeer,
receivingPeer: $receivingPeerAddress,
callingPublicEncryptionKey: $rpcRequest->getParameter('public_encryption_key'),
channelUUid: $rpcRequest->getParameter('channel_uuid')
);
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to create the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($uuid !== $rpcRequest->getParameter('channel_uuid'))
{
try
{
EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('There was an error while trying to decline the encryption channel as a server', $e);
}
return $rpcRequest->produceError(StandardError::UUID_MISMATCH, 'The created UUID in the server does not match the UUID that was received');
}
return $rpcRequest->produceResponse($uuid);
}
}

View file

@ -0,0 +1,187 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Exception;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Logger;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Enums\Status\EncryptionChannelStatus;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\RpcException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
use Socialbox\Socialbox;
class EncryptionDeclineChannel extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
try
{
if ($request->isExternal())
{
return self::handleExternal($request, $rpcRequest);
}
return self::handleInternal($request, $rpcRequest);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('An error occurred while checking the request type', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleInternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
try
{
$receivingPeer = $request->getPeer();
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif($encryptionChannel->getReceivingPeerAddress()->getAddress() !== $receivingPeer->getAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver');
}
if($encryptionChannel->getCallingPeerAddress()->isExternal())
{
try
{
$rpcClient = Socialbox::getExternalSession($encryptionChannel->getCallingPeerAddress()->getDomain());
$rpcClient->encryptionDeclineChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid'),
identifiedAs: $receivingPeer->getAddress()
);
}
catch(Exception $e)
{
try
{
EncryptionChannelManager::declineChannel($rpcRequest->getParameter('channel_uuid'), true);
}
catch(DatabaseOperationException $e)
{
Logger::getLogger()->error('Error declining channel as server', $e);
}
if($e instanceof RpcException)
{
throw StandardRpcException::fromRpcException($e);
}
throw new StandardRpcException('There was an error while trying to notify the external server of the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
}
try
{
EncryptionChannelManager::declineChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid')
);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to decline the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
/**
* @param ClientRequest $request
* @param RpcRequest $rpcRequest
* @return SerializableInterface|null
* @throws StandardRpcException
*/
private static function handleExternal(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if($request->getIdentifyAs() === null)
{
return $rpcRequest->produceError(StandardError::BAD_REQUEST, 'Missing required header IdentifyAs');
}
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
try
{
$receivingPeer = Socialbox::resolvePeer($request->getIdentifyAs());
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif($encryptionChannel->getReceivingPeerAddress() !== $receivingPeer->getPeerAddress())
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
elseif($encryptionChannel->getStatus() !== EncryptionChannelStatus::AWAITING_RECEIVER)
{
return $rpcRequest->produceError(StandardError::FORBIDDEN, 'The encryption channel is not awaiting the receiver');
}
try
{
EncryptionChannelManager::declineChannel(
channelUuid: $rpcRequest->getParameter('channel_uuid')
);
}
catch (DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to decline the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
return $rpcRequest->produceResponse(true);
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace Socialbox\Classes\StandardMethods\EncryptionChannel;
use Socialbox\Abstracts\Method;
use Socialbox\Classes\Validator;
use Socialbox\Enums\StandardError;
use Socialbox\Exceptions\DatabaseOperationException;
use Socialbox\Exceptions\Standard\InvalidRpcArgumentException;
use Socialbox\Exceptions\Standard\MissingRpcArgumentException;
use Socialbox\Exceptions\Standard\StandardRpcException;
use Socialbox\Interfaces\SerializableInterface;
use Socialbox\Managers\EncryptionChannelManager;
use Socialbox\Objects\ClientRequest;
use Socialbox\Objects\RpcRequest;
class EncryptionGetChannel extends Method
{
/**
* @inheritDoc
*/
public static function execute(ClientRequest $request, RpcRequest $rpcRequest): ?SerializableInterface
{
if(!$rpcRequest->containsParameter('channel_uuid'))
{
throw new MissingRpcArgumentException('channel_uuid');
}
elseif(!Validator::validateUuid($rpcRequest->getParameter('channel_uuid')))
{
throw new InvalidRpcArgumentException('channel_uuid', 'The given channel uuid is not a valid UUID V4');
}
try
{
$requestingPeer = $request->getPeer();
$encryptionChannel = EncryptionChannelManager::getChannel($rpcRequest->getParameter('channel_uuid'));
}
catch(DatabaseOperationException $e)
{
throw new StandardRpcException('There was an error while trying to obtain the encryption channel', StandardError::INTERNAL_SERVER_ERROR, $e);
}
if($encryptionChannel === null)
{
return $rpcRequest->produceError(StandardError::NOT_FOUND, 'The requested encryption channel was not found');
}
elseif(!$encryptionChannel->isParticipant($requestingPeer->getAddress()))
{
return $rpcRequest->produceError(StandardError::UNAUTHORIZED, 'The requested encryption channel is not accessible');
}
return $rpcRequest->produceResponse($encryptionChannel->toStandard());
}
}

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