From 75bb525d70f5966aab01692136add13a00a2353a Mon Sep 17 00:00:00 2001 From: Stefan Date: Tue, 3 Dec 2024 10:35:31 +0100 Subject: [PATCH] Auto-commit on 2024-12-03 10:35:31 by pcx3 --- configuration/.github/scripts/get_cf_ips.sh | 22 - .../.github/workflows/trigger-sync.yml | 21 - .../.github/workflows/update_cf_ips.yml | 36 - .../.github/workflows/update_dns_date.yml | 35 - configuration/.gitignore | 3 - configuration/bind9/zone_template.txt | 2 +- .../docker/compose/new-docker-compose.yml | 2 + configuration/docker/compose/ttyd-compose.yml | 30 + .../vhosts/docker_varnish_apache_domain.conf | 82 ++ configuration/openadmin/cluster/.gitkeep | 1 + configuration/openadmin/config/admin.ini | 10 + .../config/{terms => terms_temporary_off} | 0 configuration/openpanel/conf/openpanel.config | 6 +- configuration/services/sentinel/messages.yaml | 73 ++ configuration/services/varnish.service | 31 + configuration/ssh/admin_welcome.sh | 4 +- configuration/varnish/default.vcl | 2 +- .../varnish/docker_varnish_nginx_domain.conf | 114 +++ integrations/digitalocean/.github/CODEOWNERS | 1 - .../digitalocean/.github/CONTRIBUTING.md | 27 - integrations/digitalocean/.gitignore | 43 - integrations/whmcs/README.md | 36 + integrations/whmcs/openpanel.php | 805 ++++++++++++++++++ integrations/whmcs/whmcs.json | 22 + 24 files changed, 1212 insertions(+), 196 deletions(-) delete mode 100644 configuration/.github/scripts/get_cf_ips.sh delete mode 100644 configuration/.github/workflows/trigger-sync.yml delete mode 100644 configuration/.github/workflows/update_cf_ips.yml delete mode 100644 configuration/.github/workflows/update_dns_date.yml delete mode 100644 configuration/.gitignore create mode 100644 configuration/docker/compose/ttyd-compose.yml create mode 100644 configuration/nginx/vhosts/docker_varnish_apache_domain.conf create mode 100644 configuration/openadmin/cluster/.gitkeep rename configuration/openadmin/config/{terms => terms_temporary_off} (100%) create mode 100644 configuration/services/sentinel/messages.yaml create mode 100644 configuration/services/varnish.service create mode 100644 configuration/varnish/docker_varnish_nginx_domain.conf delete mode 100644 integrations/digitalocean/.github/CODEOWNERS delete mode 100644 integrations/digitalocean/.github/CONTRIBUTING.md delete mode 100644 integrations/digitalocean/.gitignore create mode 100644 integrations/whmcs/README.md create mode 100644 integrations/whmcs/openpanel.php create mode 100644 integrations/whmcs/whmcs.json diff --git a/configuration/.github/scripts/get_cf_ips.sh b/configuration/.github/scripts/get_cf_ips.sh deleted file mode 100644 index ef3c07c9..00000000 --- a/configuration/.github/scripts/get_cf_ips.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -cf_ips="$(curl -fsLm5 --retry 3 https://api.cloudflare.com/client/v4/ips)" - -if [ -n "$cf_ips" ] && [ "$(echo "$cf_ips" | jq -r '.success')" = "true" ]; then - cf_inc="nginx/cloudflare.inc" - - echo "[ * ] Updating Cloudflare IP Ranges for Nginx..." - echo "# Cloudflare IP Ranges" > $cf_inc - echo "" >> $cf_inc - echo "# IPv4" >> $cf_inc - for ipv4 in $(echo "$cf_ips" | jq -r '.result.ipv4_cidrs[]' | sort); do - echo "set_real_ip_from $ipv4;" >> $cf_inc - done - echo "" >> $cf_inc - echo "# IPv6" >> $cf_inc - for ipv6 in $(echo "$cf_ips" | jq -r '.result.ipv6_cidrs[]' | sort); do - echo "set_real_ip_from $ipv6;" >> $cf_inc - done - echo "" >> $cf_inc - echo "real_ip_header CF-Connecting-IP;" >> $cf_inc -fi diff --git a/configuration/.github/workflows/trigger-sync.yml b/configuration/.github/workflows/trigger-sync.yml deleted file mode 100644 index 93305770..00000000 --- a/configuration/.github/workflows/trigger-sync.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Trigger sync of conf - -on: - push: - branches: - - main #always - -jobs: - trigger: - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - repository-projects: write - steps: - - name: Trigger Sync - run: | - curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ - -H "Accept: application/vnd.github.v3+json" \ - https://api.github.com/repos/stefanpejcic/OpenPanel/dispatches \ - -d '{"event_type":"sync-configuration"}' diff --git a/configuration/.github/workflows/update_cf_ips.yml b/configuration/.github/workflows/update_cf_ips.yml deleted file mode 100644 index 7a2f6be4..00000000 --- a/configuration/.github/workflows/update_cf_ips.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Update Cloudflare IPs - -on: - schedule: - - cron: '0 10 * * *' - workflow_dispatch: # manual trigger - -permissions: - contents: write # to push - -jobs: - update_cf_ips: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Ensure get_cf_ips.sh is executable - run: chmod +x .github/scripts/get_cf_ips.sh - - - name: Install jq - run: sudo apt-get install -y jq - - - name: Run get_cf_ips.sh - run: .github/scripts/get_cf_ips.sh - - - name: Commit and push changes - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - git add nginx/cloudflare.inc - git commit -m "Update Cloudflare IPs" || echo "No changes to commit" - git push - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/configuration/.github/workflows/update_dns_date.yml b/configuration/.github/workflows/update_dns_date.yml deleted file mode 100644 index f0f85cfa..00000000 --- a/configuration/.github/workflows/update_dns_date.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Update Serial Number Daily - -on: - schedule: - - cron: '0 0 * * *' # Runs every day at midnight UTC - workflow_dispatch: # Allows manual triggering - -jobs: - update-serial: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT_TOKEN }} - - - name: Update serial number - run: | - # Get today's date in YYYYMMDD format - DATE=$(date -u +"%Y%m%d") - - FILE="bind9/zone_template.txt" - - sed -i "s/[0-9]\{10\}/$(date +%Y%m%d)01/" $FILE - - - name: Commit changes - uses: EndBug/add-and-commit@v9 - with: - add: 'bind9/zone_template.txt' - message: 'Update serial number with current date' - author_name: 'GitHub Actions' - author_email: 'actions@github.com' - token: ${{ secrets.PAT_TOKEN }} - diff --git a/configuration/.gitignore b/configuration/.gitignore deleted file mode 100644 index 8e8f33d3..00000000 --- a/configuration/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -#.github/ -#.github/workflows/ -README.md diff --git a/configuration/bind9/zone_template.txt b/configuration/bind9/zone_template.txt index b4b88322..0f1d4ed6 100644 --- a/configuration/bind9/zone_template.txt +++ b/configuration/bind9/zone_template.txt @@ -1,6 +1,6 @@ $TTL 1h @ IN SOA {ns1}. {ns2}. ( - 2024110501 ; Serial number + 2024120301 ; Serial number 1h ; Refresh interval 15m ; Retry interval 1w ; Expire interval diff --git a/configuration/docker/compose/new-docker-compose.yml b/configuration/docker/compose/new-docker-compose.yml index 0e628251..f3151b59 100644 --- a/configuration/docker/compose/new-docker-compose.yml +++ b/configuration/docker/compose/new-docker-compose.yml @@ -70,6 +70,8 @@ services: - /etc/openpanel/openpanel/conf/knowledge_base_articles.json:/etc/openpanel/openpanel/conf/knowledge_base_articles.json # localization - /etc/openpanel/openpanel/translations/:/etc/openpanel/openpanel/translations/ + # emails + - /usr/local/mail/openmail/:/usr/local/mail/openmail/ network_mode: host mem_limit: 1g cpus: 1.0 diff --git a/configuration/docker/compose/ttyd-compose.yml b/configuration/docker/compose/ttyd-compose.yml new file mode 100644 index 00000000..3cd877d7 --- /dev/null +++ b/configuration/docker/compose/ttyd-compose.yml @@ -0,0 +1,30 @@ +services: + # WEB TERMINAL FOR OPENADMIN + ttyd: + image: tsl0922/ttyd + container_name: ttyd + #ports: + # - "7681:7681" + network_mode: "host" + command: ttyd -o -W bash + restart: no + stdin_open: true + tty: true + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup + - /var/run/docker.sock:/var/run/docker.sock + - openadmin_mysql:/var/lib/mysql + - /etc/:/etc/ + - /home/:/home/ + - /var/:/var/ + - /root/:/root/ + - /usr/local/bin/opencli:/usr/local/bin/opencli + - /usr/local/csf:/usr/local/csf + - /usr/sbin/:/usr/sbin/ + - /usr/bin/docker:/usr/bin/docker + - /usr/bin/awk:/usr/bin/awk + - /usr/local/admin/:/usr/local/admin/ + mem_limit: 1g + cpus: 1.0 + oom_kill_disable: true + privileged: true diff --git a/configuration/nginx/vhosts/docker_varnish_apache_domain.conf b/configuration/nginx/vhosts/docker_varnish_apache_domain.conf new file mode 100644 index 00000000..a7ba6977 --- /dev/null +++ b/configuration/nginx/vhosts/docker_varnish_apache_domain.conf @@ -0,0 +1,82 @@ + + ServerName + ServerAlias www. + DocumentRoot /home// + CustomLog /var/log/apache2/domlogs/.log combined + + /> + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + DirectoryIndex index.php index.html default_page.html + + # Alias for default_page.html + Alias /default_page.html /etc/apache2/default_page.html + + + Require all granted + Options -Indexes + SetEnvIf Request_URI ^/default_page.html allow_default_page + + + + + SetHandler "proxy:unix:/run/php/-fpm.sock|fcgi://localhost" + + + # Allow access to .well-known for Certbot + //.well-known"> + Options Indexes FollowSymLinks + AllowOverride None + Require all granted + + + Alias /.well-known /home///.well-known + + + + + + ServerName + ServerAlias www. + DocumentRoot /home// + CustomLog /var/log/apache2/domlogs/.log combined + + SSLEngine on + SSLCertificateFile /etc/apache2/ssl/cert.crt + SSLCertificateKeyFile /etc/apache2/ssl/cert.key + + # TLS configuration + SSLProtocol TLSv1.2 TLSv1.3 + SSLCipherSuite HIGH:!aNULL:!MD5 + + # Proxy configuration for Varnish + ProxyPass "/" "http://127.0.0.1:80/" + ProxyPassReverse "/" "http://127.0.0.1:80/" + ProxyPreserveHost On + + /> + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + DirectoryIndex index.php index.html default_page.html + + # Alias for default_page.html + Alias /default_page.html /etc/apache2/default_page.html + + + Require all granted + Options -Indexes + SetEnvIf Request_URI ^/default_page.html allow_default_page + + + + + SetHandler "proxy:unix:/run/php/-fpm.sock|fcgi://localhost" + + + diff --git a/configuration/openadmin/cluster/.gitkeep b/configuration/openadmin/cluster/.gitkeep new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/configuration/openadmin/cluster/.gitkeep @@ -0,0 +1 @@ + diff --git a/configuration/openadmin/config/admin.ini b/configuration/openadmin/config/admin.ini index 5795bf62..3585e934 100644 --- a/configuration/openadmin/config/admin.ini +++ b/configuration/openadmin/config/admin.ini @@ -1,7 +1,17 @@ [USERS] +reseller=no impersonate=no +[RESELLERS] +api_access=no + [PANEL] login_ratelimit=5 login_blocklimit=20 + + +[SECURITY] +basic_auth=no +basic_auth_username= +basic_auth_password= diff --git a/configuration/openadmin/config/terms b/configuration/openadmin/config/terms_temporary_off similarity index 100% rename from configuration/openadmin/config/terms rename to configuration/openadmin/config/terms_temporary_off diff --git a/configuration/openpanel/conf/openpanel.config b/configuration/openpanel/conf/openpanel.config index 1678bc77..b9a35b35 100644 --- a/configuration/openpanel/conf/openpanel.config +++ b/configuration/openpanel/conf/openpanel.config @@ -31,9 +31,10 @@ login_blocklimit=20 session_duration=30 session_lifetime=300 password_reset=no +weakpass=yes twofa_nag=yes how_to_guides=yes -sidebar_color=dark +color_mode=dark avatar_type=gravatar max_login_records=20 activity_items_per_page=25 @@ -66,9 +67,6 @@ api=off dev_mode=off template= admin_template= -basic_auth=no -basic_auth_username= -basic_auth_password= screenshots=http://screenshots-api.openpanel.com/screenshot temporary_links=https://preview.openpanel.org/index.php diff --git a/configuration/services/sentinel/messages.yaml b/configuration/services/sentinel/messages.yaml new file mode 100644 index 00000000..0f11c3a2 --- /dev/null +++ b/configuration/services/sentinel/messages.yaml @@ -0,0 +1,73 @@ +DAILY_REPORT: + title: "Daily Usage Report" + message: "Daily usage report." + +REBOOT: + title: "🚨 URGENT - SYSTEM REBOOT" + message: "System was rebooted. Uptime: " + +SWAP_USAGE: + title: "⚠️ SWAP usage alert!" + message: "" + +RAM_USAGE: + title: "🚨 High Memory Usage!" + message: "" + +LOAD_USAGE: + title: "🚨 High System Load!" + message: "" + +CPU_USAGE: + title: "🚨 High CPU Usage!" + message: "" + +DISK_USAGE: + title: "⚠️ Running out of Disk Space!" + message: "" + +SSH_LOGIN: + title: "Suspicious SSH login detected" + message: "SSH users currently logged-in that have not accessed OpenAdmin interface. IP addresses: " + +ADMIN_LOGIN: + title: "Admin account accessed from new IP address" + message: "Admin account was accessed from a new IP address: " + +MYSQL_DOWN: + title: "⚠️ WARNING - MySQL service is not active. Users are unable to log into OpenPanel!" + message: "" + +NGINX_DOWN: + title: "Nginx service is not active. Users' websites are not working!" + message: "" + +CSF_DOWN: + title: "ConfigService Firewall (CSF) is not active. Server and websites are not protected!" + message: "" + +UFW_DOWN: + title: "Uncomplicated Firewall (UFW) is not active. Server and websites are not protected!" + message: "" + +ADMIN_DOWN: + title: "Admin service is not active. OpenAdmin service is not accessible!" + message: "" + +DOCKER_DOWN: + title: "Docker service is not active. User websites are down!" + message: "" + +PANEL_DOWN: + title: "OpenPanel docker container is not running. Users are unable to access the OpenPanel interface!" + message: "" + +CERTBOT_DOWN: + title: "Certbot service is not running. SSL certificates will not renew automatically!" + message: "" + +BIND_DOWN: + title: "Named (BIND9) service is not active. DNS resolving of domains is not working!" + message: "" + + diff --git a/configuration/services/varnish.service b/configuration/services/varnish.service new file mode 100644 index 00000000..6dbc90ce --- /dev/null +++ b/configuration/services/varnish.service @@ -0,0 +1,31 @@ +[Unit] +Description=Varnish Cache, a high-performance HTTP accelerator +Documentation=https://www.varnish-cache.org/docs/ man:varnishd + +[Service] +Type=simple + +# Maximum number of open files (for ulimit -n) +LimitNOFILE=131072 + +# Locked shared memory - should suffice to lock the shared memory log +# (varnishd -l argument) +# Default log size is 80MB vsl + 1M vsm + header -> 82MB +# unit is bytes +LimitMEMLOCK=85983232 +ExecStart=/usr/sbin/varnishd \ + -j unix,user=vcache \ + -F \ + -a :80 \ + -T localhost:6082 \ + -f /etc/varnish/default.vcl \ + -S /etc/varnish/secret \ + -s malloc,256m +ExecReload=/usr/share/varnish/varnishreload +ProtectSystem=full +ProtectHome=true +PrivateTmp=true +PrivateDevices=true + +[Install] +WantedBy=multi-user.target diff --git a/configuration/ssh/admin_welcome.sh b/configuration/ssh/admin_welcome.sh index 116652fa..5269f903 100644 --- a/configuration/ssh/admin_welcome.sh +++ b/configuration/ssh/admin_welcome.sh @@ -106,8 +106,8 @@ echo -e "OPENADMIN LINK: ${GREEN}${admin_url}${RESET}" echo -e "" echo -e "Need assistance or looking to learn more? We've got you covered:" echo -e " - 📚 Admin Docs: https://openpanel.com/docs/admin/intro/" -echo -e " - 💬 Forums: https://community.openpanel.com/" -echo -e " - 👉 Discord: https://discord.openpanel.com/" +echo -e " - 💬 Forums: https://community.openpanel.org/" +echo -e " - 👉 Discord: https://discord.openpanel.com/" echo -e "" echo -e "================================================================" diff --git a/configuration/varnish/default.vcl b/configuration/varnish/default.vcl index 18e2d722..fcb4baca 100644 --- a/configuration/varnish/default.vcl +++ b/configuration/varnish/default.vcl @@ -1,7 +1,7 @@ vcl 4.1; backend default { - .host = "127.0.0.1"; + .host = "localhost"; .port = "8080"; } diff --git a/configuration/varnish/docker_varnish_nginx_domain.conf b/configuration/varnish/docker_varnish_nginx_domain.conf new file mode 100644 index 00000000..0d9405c9 --- /dev/null +++ b/configuration/varnish/docker_varnish_nginx_domain.conf @@ -0,0 +1,114 @@ +# content +server { + listen 80; + server_name www.; + access_log /var/log/nginx/domlogs/.log; + + # + location ~* ^/(\.git|composer\.(json|lock)|auth\.json|config\.php|wp-config\.php|vendor) { + deny all; + return 403; + } + # + + root /home//; + + location / { + real_ip_header X-Forwarded-For; + set_real_ip_from 172.17.0.1; + try_files $uri $uri/ /index.php$is_args$args /default_page.html =404; + index index.php index.html default_page.html; + autoindex on; + } + + location = /default_page.html { + alias /etc/nginx/default_page.html; + default_type text/html; + access_log off; + log_not_found off; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/-fpm.sock; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires max; + log_not_found off; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + +} + + +server { + listen 443 ssl http2; + server_name www.; + access_log /var/log/nginx/domlogs/.log; + + root /home//; + + # SSL configuration + ssl_certificate /etc/nginx/ssl/cert.crt; + ssl_certificate_key /etc/nginx/ssl/cert.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers on; + ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; + + # Proxy HTTPS traffic to Varnish on port 80 + location / { + proxy_pass http://127.0.0.1:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } + + + location = /default_page.html { + alias /etc/nginx/default_page.html; + default_type text/html; + access_log off; + log_not_found off; + } + + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/run/php/-fpm.sock; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires max; + log_not_found off; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + # Allow access to .well-known for Certbot + location ^~ /.well-known { + default_type "text/plain"; + root /home///; + } + +} diff --git a/integrations/digitalocean/.github/CODEOWNERS b/integrations/digitalocean/.github/CODEOWNERS deleted file mode 100644 index c226b66c..00000000 --- a/integrations/digitalocean/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -@marketplace-eng diff --git a/integrations/digitalocean/.github/CONTRIBUTING.md b/integrations/digitalocean/.github/CONTRIBUTING.md deleted file mode 100644 index 1962a365..00000000 --- a/integrations/digitalocean/.github/CONTRIBUTING.md +++ /dev/null @@ -1,27 +0,0 @@ -# Contributing - -We enthusiastically encourage contributions of all sorts to our repository, from correcting typos, to improving checks or adding new ones. - -### Reporting Issues - -This section guides you through submitting an issue for the Marketplace Partners. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behavior :computer: :computer:, and find related reports :mag_right:. - -When you are reporting an issue, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). - -> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. - -#### Before Submitting An Issue -* **Check the [current issues](https://github.com/digitalocean/marketplace-partners/issues)**. - -#### How Do I Submit A (Good) Issue? - -Issues are tracked as [GitHub issues](https://guides.github.com/features/issues/). Create an issue and provide the following information listed below. - -Explain the problem and include additional details to help maintainers reproduce the problem: - -* **Use a clear and descriptive title** for the issue to identify the problem. -* **Describe the exact steps which reproduce the problem** in as many details as possible. When listing steps, **don't just say what you did, but explain how you did it**. -* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). -* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. -* **Explain which behavior you expected to see instead and why.** -* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. diff --git a/integrations/digitalocean/.gitignore b/integrations/digitalocean/.gitignore deleted file mode 100644 index 6a557f52..00000000 --- a/integrations/digitalocean/.gitignore +++ /dev/null @@ -1,43 +0,0 @@ -# Compiled source # -################### -*.com -*.class -*.dll -*.exe -*.o -*.so - -# Packages # -############ -# it's better to unpack these files and commit the raw source -# git has its own built in compression methods -*.7z -*.dmg -*.gz -*.iso -*.jar -*.rar -*.tar -*.zip - -# Logs and databases # -###################### -*.log -*.sql -*.sqlite - -# OS generated files # -###################### -.DS_Store -.DS_Store? -._* -.Spotlight-V100 -.Trashes -ehthumbs.db -Thumbs.db - -# App generated files # -###################### -.vscode - -.Rproj.user diff --git a/integrations/whmcs/README.md b/integrations/whmcs/README.md new file mode 100644 index 00000000..9cc49b08 --- /dev/null +++ b/integrations/whmcs/README.md @@ -0,0 +1,36 @@ +# OpenPanel WHMCS Module 😎 +WHMCS module for [OpenPanel](https://openpanel.com) + + +## Requirements + +- Server with OpenPanel Enterprise license +- WHMCS + +## Installation + +1. Login to SSH for WHMCS server +2. Navigate to `path_to_whmcs/modules/servers` +3. Run this command to create a new folder and in it download the module: + ```bash + git clone https://github.com/stefanpejcic/openpanel-whmcs-module.git openpanel + ``` + +## Configuration + +How to setup WHMCS and OpenPanel: https://openpanel.com/docs/articles/extensions/openpanel-and-whmcs/ + + +## Update + +1. Login to SSH for WHMCS server +2. Navigate to `path_to_whmcs/modules/servers/openpanel` +3. Run this command to download newer files: + ```bash + git pull + ``` + +## Bug Reports + +Report [new issue on github](https://github.com/stefanpejcic/openpanel-whmcs-module/issues/new/choose) + diff --git a/integrations/whmcs/openpanel.php b/integrations/whmcs/openpanel.php new file mode 100644 index 00000000..841b71cf --- /dev/null +++ b/integrations/whmcs/openpanel.php @@ -0,0 +1,805 @@ + $authEndpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(array( + 'username' => $params["serverusername"], + 'password' => $params["serverpassword"] + )), + CURLOPT_HTTPHEADER => array( + "Content-Type: application/json" + ), + )); + + // Execute cURL request to authenticate + $response = curl_exec($curl); + + // Check for errors + if (curl_errno($curl)) { + $token = false; + $error = "cURL Error: " . curl_error($curl); + } else { + // Decode the response JSON to get the token + $responseData = json_decode($response, true); + $token = isset($responseData['access_token']) ? $responseData['access_token'] : false; + $error = $token ? null : "Token not found in response"; + } + + // Close cURL session + curl_close($curl); + + return array($token, $error); +} + + +function apiRequest($endpoint, $token, $data = null, $method = 'POST') { + // Prepare cURL request + $curl = curl_init(); + + // Set default cURL options + $options = array( + CURLOPT_URL => $endpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array( + "Authorization: Bearer " . $token, + "Content-Type: application/json" + ), + ); + + // Handle different HTTP methods + switch ($method) { + case 'POST': + if ($data !== null) { + $options[CURLOPT_POST] = true; + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + break; + + case 'GET': + $options[CURLOPT_CUSTOMREQUEST] = 'GET'; + break; + + case 'PUT': + $options[CURLOPT_CUSTOMREQUEST] = 'PUT'; + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + break; + + case 'CONNECT': + $options[CURLOPT_CUSTOMREQUEST] = 'CONNECT'; + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + break; + + case 'PATCH': + $options[CURLOPT_CUSTOMREQUEST] = 'PATCH'; + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + break; + + case 'DELETE': + $options[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + if ($data !== null) { + $options[CURLOPT_POSTFIELDS] = json_encode($data); + } + break; + + default: + // Handle unsupported methods + throw new InvalidArgumentException("Unsupported method: $method"); + } + + // Set the options for the cURL request + curl_setopt_array($curl, $options); + + // Execute cURL request + $response = curl_exec($curl); + + // Decode the response JSON + $responseData = json_decode($response, true); + + // Close cURL session + curl_close($curl); + + return $responseData; +} + + + + + + + +############### USER ACTIONS ################ +# CREATE ACCOUNT +function openpanel_CreateAccount($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return $error; // Return the error message as a plain string + } + + try { + $apiProtocol = getApiProtocol($params["serverhostname"]); + $createUserEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users'; + $packageId = $params['pid']; // Get the Product ID (Package ID) + + // Query the database to get the package name + $result = select_query("tblproducts", "name", array("id" => $packageId)); + $data = mysql_fetch_array($result); + $packageName = $data['name']; // This is the package name + + // Prepare data for user creation + $userData = array( + 'username' => $params["username"], + 'password' => $params["password"], + 'email' => $params["clientsdetails"]["email"], + 'plan_name' => $packageName + ); + + // Make API request to create user + $response = apiRequest($createUserEndpoint, $jwtToken, $userData); + + if (isset($response['success']) && $response['success'] === true) { + return 'success'; + } else { + return isset($response['error']) ? $response['error'] : 'An unknown error occurred.'; + } + + } catch (Exception $e) { + logModuleCall( + 'openpanel', + __FUNCTION__, + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + return $e->getMessage(); + } +} + + +# TERMINATE ACCOUNT +function openpanel_TerminateAccount($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return $error; // Return the error message as a plain string + } + + try { + $apiProtocol = getApiProtocol($params["serverhostname"]); + $userEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Step 1: Unsuspend the account if it's suspended + try { + $unsuspendData = array('action' => 'unsuspend'); + $unsuspendResponse = apiRequest($userEndpoint, $jwtToken, $unsuspendData, 'PATCH'); + + + } catch (Exception $e) { + // If unsuspend fails, check if the account doesn't exist + $errorMessage = $e->getMessage(); + if (strpos($errorMessage, 'not found') !== false || strpos($errorMessage, 'User') !== false) { + // Account does not exist, return an error message + return 'Error: Account "' . $params["username"] . '" does not exist and could not be deleted.'; + } else { + return 'Failed to unsuspend account before termination: ' . $errorMessage; + } + } + + // Step 2: Now attempt to delete the account + try { + $response = apiRequest($userEndpoint, $jwtToken, null, 'DELETE'); + + + if (isset($response['success']) && $response['success'] === true) { + return 'success'; + } else { + return isset($response['error']) ? $response['error'] : 'An unknown error occurred during termination.'; + } + + } catch (Exception $e) { + // Log the exception for the delete action + logModuleCall( + 'openpanel', + 'TerminateAccount - Delete Exception', + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + // Handle exception during the delete action + return 'Error during account termination: ' . $e->getMessage(); + } + + } catch (Exception $e) { + logModuleCall( + 'openpanel', + __FUNCTION__, + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + return $e->getMessage(); + } +} + + + +# CHANGE PASSWORD FOR ACCOUNT +function openpanel_ChangePassword($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return $error; // Return the error message as a plain string + } + + try { + $apiProtocol = getApiProtocol($params["serverhostname"]); + $changePasswordEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Prepare data for password change + $passwordData = array('password' => $params["password"]); + + // Make API request to change password for user + $response = apiRequest($changePasswordEndpoint, $jwtToken, $passwordData, 'PATCH'); + + // Log the API request and response + logModuleCall( + 'openpanel', + 'ChangePassword', + $passwordData, + $response + ); + + // Check for success in the response + if (isset($response['success']) && $response['success'] === true) { + return 'success'; + } else { + // Return the error message from the response or a default message + return isset($response['error']) ? $response['error'] : 'An unknown error occurred during password change.'; + } + + } catch (Exception $e) { + // Log the exception + logModuleCall( + 'openpanel', + 'ChangePassword Exception', + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + // Return the exception message + return 'Error: ' . $e->getMessage(); + } +} + + + +# SUSPEND ACCOUNT +function openpanel_SuspendAccount($params) { + list($jwtToken, $error) = getAuthToken($params); + + // If JWT token is not received, return error message + if (!$jwtToken) { + return json_encode(array("success" => false, "message" => $error)); + } + + try { + // Prepare the API endpoint for suspending the account + $apiProtocol = getApiProtocol($params["serverhostname"]); + $suspendAccountEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Prepare data for account suspension + $suspendData = array('action' => 'suspend'); + + // Make the API request to suspend the account + $response = apiRequest($suspendAccountEndpoint, $jwtToken, $suspendData, 'PATCH'); + + // Check the API response for success or failure + if (isset($response['success']) && $response['success'] === true) { + return 'success'; + } else { + // Return the error message from the API response + return isset($response['error']) ? $response['error'] : 'An unknown error occurred.'; + } + + } catch (Exception $e) { + // Log the exception details + logModuleCall( + 'openpanel', + 'SuspendAccount Exception', + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + // Return the exception message + return 'Error: ' . $e->getMessage(); + } +} + + + + + + +# UNSUSPEND ACCOUNT +function openpanel_UnsuspendAccount($params) { + list($jwtToken, $error) = getAuthToken($params); + + // If JWT token is not received, return error message + if (!$jwtToken) { + return json_encode(array("success" => false, "message" => $error)); + } + + try { + // Prepare the API endpoint to unsuspend the account + $apiProtocol = getApiProtocol($params["serverhostname"]); + $unsuspendAccountEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Prepare data for account unsuspension (if any) + $unsuspendData = array('action' => 'unsuspend'); + + // Make the API request to unsuspend the account + $response = apiRequest($unsuspendAccountEndpoint, $jwtToken, $unsuspendData, 'PATCH'); + + + // Check the API response for success or failure + if (isset($response['success']) && $response['success'] === true) { + return 'success'; + } else { + // Return the error message from the API response + return isset($response['error']) ? $response['error'] : 'An unknown error occurred.'; + } + + } catch (Exception $e) { + // Log the exception details + logModuleCall( + 'openpanel', + 'UnsuspendAccount Exception', + $params, + $e->getMessage(), + $e->getTraceAsString() + ); + + // Return the exception message + return 'Error: ' . $e->getMessage(); + } +} + + + + + + +# CHANGE PACKAGE (PLAN) +function openpanel_ChangePackage($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + // Only log token issues as these are critical. + logModuleCall('openpanel', 'ChangePackage', $params, "Error fetching token: $error"); + return $error; + } + + try { + $apiProtocol = getApiProtocol($params["serverhostname"]); + $changePlanEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Fetch the stored plan ID + $storedPlanId = $params['configoption1']; + + // Retrieve available plans from the server + $plans = getAvailablePlans($params); + + if (is_string($plans)) { + logModuleCall('openpanel', 'ChangePackage', $params, "Error retrieving plans: $plans"); + return "Error retrieving plans: $plans"; + } + + // Find the actual plan name using the stored plan ID + $planName = null; + foreach ($plans as $plan) { + if ($plan['id'] == $storedPlanId) { + $planName = $plan['name']; + break; + } + } + + if (!$planName) { + logModuleCall('openpanel', 'ChangePackage', $params, "No matching plan name found for stored plan ID: $storedPlanId"); + return "Error: No matching plan name found for stored plan ID: $storedPlanId"; + } + + // Prepare data for changing plan + $planData = array('plan_name' => $planName); + + // Make API request to change plan + $response = apiRequest($changePlanEndpoint, $jwtToken, $planData, 'PUT'); + + // Log only on failure + if (!(isset($response['success']) && $response['success'] === true)) { + logModuleCall('openpanel', 'ChangePackage', array('command' => "opencli user-change_plan {$params['username']} $planName"), $response); + return isset($response['error']) ? $response['error'] : 'An unknown error occurred during package change.'; + } + + return 'success'; + + } catch (Exception $e) { + logModuleCall('openpanel', 'ChangePackage Exception', $params, $e->getMessage(), $e->getTraceAsString()); + return 'Error: ' . $e->getMessage(); + } +} + + + +############### AUTOLOGIN LINKS ############## + +# LOGIN FOR USERS ON FRONT +function openpanel_ClientArea($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return '

Error: ' . $error . '

'; + } + + $apiProtocol = getApiProtocol($params["serverhostname"]); + $getLoginLinkEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Prepare data for login link generation + $loginData = array(); + + // Make API request to get login link + $response = apiRequest($getLoginLinkEndpoint, $jwtToken, $loginData, 'CONNECT'); + + if (isset($response["link"])) { + $code = ''; + $code .= ' +   Login to OpenPanel + '; + $code .= ''; + } else { + $code = '

Error: Unable to generate login link for OpenPanel. Please try again later.

'; + if (isset($response["message"])) { + $code .= '

Server Response: ' . htmlentities($response["message"]) . '

'; + } + } + + return $code; +} + + + +# LOGIN FROM admin/configservers.php +function openpanel_AdminLink($params) { + $apiProtocol = getApiProtocol($params["serverhostname"]); + $adminLoginEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/login'; + + $code = '
+ + + +
'; + return $code; +} + + +# LOGIN FOR ADMINS FROM BACKEND +function openpanel_LoginLink($params) { + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return '

Error: ' . $error . '

'; + } + + $apiProtocol = getApiProtocol($params["serverhostname"]); + $getLoginLinkEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"]; + + // Prepare data for login link generation + $loginData = array(); + + // Make API request to get login link + $response = apiRequest($getLoginLinkEndpoint, $jwtToken, $loginData, 'CONNECT'); + + if (isset($response["link"])) { + $code = ''; + $code .= ' +   Login to OpenPanel + '; + $code .= ''; + } else { + // Log or print the response in case of error + $code = '

Error: Unable to generate the login link. Please try again later.

'; + if (isset($response["message"])) { + $code .= '

Server Response: ' . htmlentities($response["message"]) . '

'; + } + } + + return $code; +} + +function getAvailablePlans($params) { + // Use WHMCS server parameters for OpenPanel + $username = $params['serverusername']; // OpenPanel username + $password = $params['serverpassword']; // OpenPanel password + $hostname = $params['serverhostname']; // OpenPanel hostname + + $apiProtocol = getApiProtocol($hostname); + $plansEndpoint = $apiProtocol . $hostname . ':2087/api/plans'; // Correct endpoint for OpenPanel API + + // Get the JWT token using the server credentials + list($jwtToken, $error) = getAuthToken($params); + if (!$jwtToken) { + return "Error fetching token: $error"; // Return error if token cannot be fetched + } + + // Prepare cURL request with Bearer token + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => $plansEndpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => array( + "Authorization: Bearer " . $jwtToken, + "Content-Type: application/json" + ), + CURLOPT_CUSTOMREQUEST => 'GET', + // Enable SSL verification for production + CURLOPT_SSL_VERIFYHOST => 2, // Verify the SSL certificate's host + CURLOPT_SSL_VERIFYPEER => true, // Verify the SSL certificate + )); + + // Execute the request + $response = curl_exec($curl); + + // Capture any errors + if (curl_errno($curl)) { + return "cURL Error: " . curl_error($curl); // Return cURL error + } + + // Close cURL session + curl_close($curl); + + // Decode the response + $plansResponse = json_decode($response, true); + + // Check if the plans were retrieved + if (isset($plansResponse['plans']) && is_array($plansResponse['plans'])) { + return $plansResponse['plans']; // Return the plans array + } else { + return "Error fetching plans: " . json_encode($plansResponse); // Return error + } +} + +function openpanel_ConfigOptions() { + // Get the product ID from the request, if available + $productId = isset($_REQUEST['id']) ? (int)$_REQUEST['id'] : 0; + + if (!$productId) { + // If no product ID exists yet, prompt the user to save first + return array( + 'Note' => array( + 'Description' => 'Please save the product first to configure options.', + ), + ); + } + + // Fetch the server group assigned to this product + $result = select_query('tblproducts', 'servergroup', array('id' => $productId)); + $data = mysql_fetch_array($result); + $serverGroupId = $data['servergroup']; + + if (!$serverGroupId) { + // If no server group is selected yet, do not show the plans field + return array( + 'Note' => array( + 'Description' => 'Please assign a server group to this product to fetch available plans.', + ), + ); + } + + // Fetch servers in the selected server group + $serversResult = select_query('tblservers', '*', array('disabled' => 0)); + $servers = array(); + while ($serverData = mysql_fetch_array($serversResult)) { + // Check if the server belongs to the selected server group + $serverGroupRelResult = select_query('tblservergroupsrel', 'groupid', array('serverid' => $serverData['id'])); + while ($groupRel = mysql_fetch_array($serverGroupRelResult)) { + if ($groupRel['groupid'] == $serverGroupId) { + $servers[] = $serverData; + break; + } + } + } + + if (count($servers) == 0) { + // No servers found in the group, show a message and don't load the plans field + return array( + 'Note' => array( + 'Description' => 'No servers found in the assigned server group.', + ), + ); + } + + // Use the first server in the group + $server = $servers[0]; + $params = array( + 'serverhostname' => $server['hostname'], + 'serverusername' => $server['username'], + 'serverpassword' => decrypt($server['password']), + ); + + // Fetch available plans from OpenPanel + $plans = getAvailablePlans($params); + + // Handle errors in fetching plans + if (is_string($plans)) { + // Error message + return array( + 'Note' => array( + 'Description' => 'Error fetching plans: ' . $plans, + ), + ); + } + + // Populate plans in the dropdown + $planOptions = array(); + if ($plans && is_array($plans)) { + foreach ($plans as $plan) { + $planOptions[$plan['id']] = $plan['name']; + } + } + + return array( + 'Plan' => array( + 'Type' => 'dropdown', + 'Options' => $planOptions, + 'Description' => 'Select a plan from OpenPanel', + ), + ); +} + +############### MAINTENANCE ################ + + +# TODO: GET USAGE FOR USERS!!!!!!!! +function openpanel_UsageUpdate($params) { + + # resposne should be formated like this: + #{ + # "disk_usage": "1024 MB", + # "disk_limit": "2048 MB", + # "bandwidth_usage": "512 MB", + # "bandwidth_limit": "1024 MB" + #} + + $apiProtocol = getApiProtocol($params["serverhostname"]); + $authEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/'; + + // Authenticate and get JWT token + list($jwtToken, $error) = getAuthToken($params); + + if (!$jwtToken) { + return json_encode(array( + "success" => false, + "message" => $error + )); + } + + // Prepare API endpoint for getting usage + $getUsageEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/usage/'; + + // Prepare cURL request for getting usage + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => $getUsageEndpoint, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CUSTOMREQUEST => 'PATCH', + CURLOPT_HTTPHEADER => array( + "Authorization: Bearer " . $jwtToken, + "Content-Type: application/json" + ), + )); + + // Execute cURL request for getting usage + $response = curl_exec($curl); + + // Check for errors + if (curl_errno($curl)) { + $result = json_encode(array( + "success" => false, + "message" => "cURL Error: " . curl_error($curl) + )); + } else { + // Decode the response JSON + $usageData = json_decode($response, true); + + // Loop through results and update database + foreach ($usageData as $user => $values) { + update_query("tblhosting", array( + "diskusage" => $values['disk_usage'], + "disklimit" => $values['disk_limit'], + "lastupdate" => "now()" + ), array("server" => $params['serverid'], "username" => $user)); + } + + $result = json_encode(array( + "success" => true, + "message" => "Usage updated successfully" + )); + } + + // Close cURL session + curl_close($curl); + + return $result; +} + +?> \ No newline at end of file diff --git a/integrations/whmcs/whmcs.json b/integrations/whmcs/whmcs.json new file mode 100644 index 00000000..e5c388b5 --- /dev/null +++ b/integrations/whmcs/whmcs.json @@ -0,0 +1,22 @@ +{ + "schema": "1.0", + "type": "whmcs-servers", + "name": "OpenPanel", + "license": "proprietary", + "category": "provisioning", + "description": { + "name": "OpenPanel", + "tagline": "A Linux hosting control panel based on Docker.", + "long": "" + }, + "support": { + "homepage": "https://openpanel.co/", + "docs_url": "https://dev.openpanel.co/api" + }, + "authors": [ + { + "name": "OPENPANEL", + "homepage": "https:\/\/openpanel.co\/" + } + ] +}