Auto-commit on 2024-10-15 10:31:15 by pcx3

This commit is contained in:
Stefan
2024-10-15 10:31:18 +02:00
parent 865c4d23d6
commit e2463de5a9
66 changed files with 36671 additions and 273 deletions

View File

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

View File

@@ -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"}'

View File

@@ -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 }}

View File

@@ -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 }}

View File

@@ -1,3 +0,0 @@
#.github/
#.github/workflows/
README.md

4
configuration/README.md Normal file
View File

@@ -0,0 +1,4 @@
# openpanel-configuration
Configuration files for `/etc/openpanel/`
# ✌️

View File

@@ -1,6 +1,6 @@
$TTL 1h
@ IN SOA {ns1}. {ns2}. (
2024091801 ; Serial number
2024101501 ; Serial number
1h ; Refresh interval
15m ; Retry interval
1w ; Expire interval

View File

@@ -14,9 +14,9 @@ services:
volumes:
- openadmin_mysql:/var/lib/mysql
- /etc/openpanel/:/etc/openpanel/
- /etc/openpanel/docker/compose/initialize.sql:/docker-entrypoint-initdb.d/initialize.sql
mem_limit: 1g
cpus: 1
- /root/initialize.sql:/docker-entrypoint-initdb.d/initialize.sql
mem_limit: 0.5g
cpus: 1.0
oom_kill_disable: true
# OpenPanel service running on port 2083
@@ -37,80 +37,137 @@ services:
- /sys:/host/sys:ro
- /:/hostfs:ro
- /home:/home
- /etc/ufw:/etc/ufw
- /usr/local/admin:/usr/local/admin
- /usr/local/admin/scripts:/usr/local/admin/scripts
- /var/log:/var/log
- /etc/ufw:/etc/ufw
- /etc/letsencrypt:/etc/letsencrypt
- /etc/my.cnf:/etc/my.cnf
- /etc/openpanel/:/etc/openpanel/
- /var/run/docker.sock:/var/run/docker.sock
- openadmin_mysql:/var/lib/mysql
- /usr/bin/docker:/usr/bin/docker
- /root/:/root/
# https://dev.openpanel.com/customize.html#Custom-Code
- /etc/openpanel/openpanel/custom_code/:/usr/local/panel/templates/custom_code/
- /etc/openpanel/openpanel/custom_code/custom.css:/usr/local/panel/static/css/custom.css
- /etc/openpanel/openpanel/custom_code/custom.js:/usr/local/panel/static/js/custom.js
- /etc/openpanel/openpanel/conf/knowledge_base_articles.json:/etc/openpanel/openpanel/conf/knowledge_base_articles.json
network_mode: host
deploy:
resources:
limits:
memory: 1g
cpus: '1.0'
mem_limit: 1g
cpus: 1.0
restart: always
privileged: true
#mailserver:
# image: ghcr.io/docker-mailserver/docker-mailserver:latest
# container_name: openadmin_mailserver
# hostname: mail.openpanel.site
# env_file: mailserver.env
# ports:
# - "25:25"
# - "143:143"
# - "465:465"
# - "587:587"
# - "993:993"
# volumes:
# - ./docker-data/dms/mail-data/:/var/mail/
# - ./docker-data/dms/mail-state/:/var/mail-state/
# - ./docker-data/dms/mail-logs/:/var/log/mail/
# - ./docker-data/dms/config/:/tmp/docker-mailserver/
# - /etc/localtime:/etc/localtime:ro
# restart: always
# stop_grace_period: 1m
# healthcheck:
# test: "ss --listening --tcp | grep -P 'LISTEN.+:smtp' || exit 1"
# timeout: 3s
# retries: 0
# deploy:
# resources:
# limits:
# memory: 1g
# cpus: '1.0'
# networks:
# - openadmin_mail_network
# Webserver from 0.2.5+
nginx:
image: openpanel/waf # openpanel/waf for modsecurity or nginx:alpine
container_name: nginx
network_mode: "host"
volumes:
- /etc/openpanel/nginx/nginx.conf:/usr/local/nginx/conf/nginx.conf # for modsecurity
# - /etc/openpanel/nginx/nginx.conf:/etc/nginx/nginx.conf # for standalone nginx
- /etc/openpanel/nginx/vhosts/default.conf:/etc/nginx/conf.d/default.conf
- /etc/openpanel/nginx/vhosts/openpanel_proxy.conf:/etc/openpanel/nginx/vhosts/openpanel_proxy.conf
- /etc/nginx/sites-available/:/etc/nginx/sites-available/
- /etc/nginx/sites-enabled/:/etc/nginx/sites-enabled/
- /etc/nginx/ssl/:/etc/nginx/ssl/ # for custom ssl from 0.2.6
- /etc/openpanel/nginx/error_pages/snippets/:/usr/local/nginx/conf/snippets/
- /etc/openpanel/nginx/error_pages/:/srv/http/default/
- /var/log/nginx/:/var/log/nginx/
- /etc/letsencrypt/options-ssl-nginx.conf:/etc/letsencrypt/options-ssl-nginx.conf
- /etc/letsencrypt/ssl-dhparams.pem:/etc/letsencrypt/ssl-dhparams.pem
- /etc/letsencrypt/:/etc/letsencrypt/
- /etc/openpanel/openpanel/core/users/:/etc/openpanel/openpanel/core/users/
- /etc/hosts:/etc/hosts
- /usr/share/nginx/html/:/usr/share/nginx/html/
- /home/:/home/:ro
# start modsecurity #
- /etc/openpanel/nginx/modsecurity/modsec_includes.conf:/usr/local/nginx/conf/modsec_includes.conf
- /etc/openpanel/nginx/modsecurity/modsecurity.conf:/usr/local/nginx/conf/modsecurity.conf
- /etc/openpanel/nginx/modsecurity/crs-setup.conf:/usr/local/nginx/conf/rules/crs-setup.conf
- /etc/openpanel/nginx/modsecurity/rules/:/usr/local/nginx/conf/rules/
- /etc/openpanel/nginx/modsecurity/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf:/usr/local/nginx/conf/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf
- /etc/openpanel/nginx/modsecurity/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:/usr/local/nginx/conf/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
# end modsecurity #
restart: unless-stopped
mem_limit: 1g
cpus: 1.0
oom_kill_disable: true
# SSL status and renewals
certbot:
image: certbot/certbot:latest
container_name: certbot
network_mode: "host"
volumes:
- /etc/letsencrypt:/etc/letsencrypt # Let's Encrypt certificates
- /var/lib/letsencrypt:/var/lib/letsencrypt # Working directory for Certbot
- /etc/nginx/sites-available:/etc/nginx/sites-available # Access to Nginx config for authentication challenges
- /etc/nginx/sites-enabled:/etc/nginx/sites-enabled # Enabled sites for reloading after cert issuance
entrypoint: /bin/sh -c 'trap exit TERM; while :; do sleep 6h & wait $${!}; certbot renew; nginx -s reload; done'
restart: unless-stopped
mem_limit: 0.1g
cpus: 0.1
oom_kill_disable: true
#roundcube:
# image: roundcube/roundcubemail
# container_name: openadmin_roundcube
# restart: always
# environment:
# - ROUNDCUBEMAIL_DEFAULT_HOST=openadmin_mailserver
# - ROUNDCUBEMAIL_SMTP_SERVER=openadmin_mailserver
# ports:
# - "8080:80"
# networks:
# - openadmin_mail_network
# deploy:
# resources:
# limits:
# memory: 1g
# cpus: '1.0'
# DNS
bind9:
container_name: openpanel_dns
image: ubuntu/bind9:latest
environment:
- BIND9_USER=root
- TZ=America/New_York
ports:
- "53:53/tcp"
- "53:53/udp"
volumes:
- /etc/bind/:/etc/bind/
restart: unless-stopped
mem_limit: 0.1g
cpus: 0.1
oom_kill_disable: true
# FTP
ftp_env_generator:
image: alpine:latest
container_name: ftp_env_generator
volumes:
- /etc/openpanel/ftp/:/etc/openpanel/ftp/
- /usr/local/admin/scripts/ftp/users:/usr/local/admin/scripts/ftp/users
entrypoint: /bin/sh -c "/usr/local/admin/scripts/ftp/users"
restart: "no" # Do not restart, we just want it to run once
#networks:
# openadmin_mail_network:
# driver: bridge
openadmin_ftp:
#OLD# image: delfer/alpine-ftp-server
build:
context: /etc/openpanel/ftp/
container_name: openadmin_ftp
restart: always
ports:
- "21:21"
- "21000-21010:21000-21010"
volumes:
- /home/:/home/
- /etc/openpanel/ftp/vsftpd.conf:/etc/vsftpd/vsftpd.conf
- /etc/openpanel/ftp/start_vsftpd.sh:/bin/start_vsftpd.sh
- /etc/openpanel/ftp/vsftpd.chroot_list:/etc/vsftpd.chroot_list
- /etc/openpanel/users/:/etc/openpanel/ftp/users/
# uncomment for ssl # - /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- ftp_env_generator
env_file:
- /etc/openpanel/ftp/all.users
# uncomment the following lines for SSL and replace ftp.YOUR_DOMAIN_HERE.com with your domain
# environment:
# - ADDRESS=ftp.YOUR_DOMAIN_HERE.com
# - TLS_CERT="/etc/letsencrypt/live/ftp.YOUR_DOMAIN_HERE.com/fullchain.pem"
# - TLS_KEY="/etc/letsencrypt/live/ftp.YOUR_DOMAIN_HERE.com/privkey.pem"
mem_limit: 0.5g
cpus: 0.5
# make the data persistent
# make the mysql data persistent
volumes:
openadmin_mysql:

View File

@@ -127,10 +127,9 @@ services:
- /etc/openpanel/ftp/:/etc/openpanel/ftp/
- /usr/local/admin/scripts/ftp/users:/usr/local/admin/scripts/ftp/users
entrypoint: /bin/sh -c "/usr/local/admin/scripts/ftp/users"
restart: "no" # Do not restart, we just want it to run once
restart: "no" # only run once
openadmin_ftp:
#OLD# image: delfer/alpine-ftp-server
build:
context: /etc/openpanel/ftp/
container_name: openadmin_ftp
@@ -144,16 +143,10 @@ services:
- /etc/openpanel/ftp/start_vsftpd.sh:/bin/start_vsftpd.sh
- /etc/openpanel/ftp/vsftpd.chroot_list:/etc/vsftpd.chroot_list
- /etc/openpanel/users/:/etc/openpanel/ftp/users/
# uncomment for ssl # - /etc/letsencrypt:/etc/letsencrypt:ro
depends_on:
- ftp_env_generator
env_file:
- /etc/openpanel/ftp/all.users
# uncomment the following lines for SSL and replace ftp.YOUR_DOMAIN_HERE.com with your domain
# environment:
# - ADDRESS=ftp.YOUR_DOMAIN_HERE.com
# - TLS_CERT="/etc/letsencrypt/live/ftp.YOUR_DOMAIN_HERE.com/fullchain.pem"
# - TLS_KEY="/etc/letsencrypt/live/ftp.YOUR_DOMAIN_HERE.com/privkey.pem"
mem_limit: 0.5g
cpus: 0.5

View File

@@ -102,6 +102,7 @@ CREATE TABLE `users` (
`otp_secret` varchar(255) DEFAULT NULL,
`plan` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`registered_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`server` varchar(255) DEFAULT 'default',
`plan_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

View File

@@ -102,6 +102,7 @@ CREATE TABLE `users` (
`otp_secret` varchar(255) DEFAULT NULL,
`plan` varchar(255) CHARACTER SET latin1 COLLATE latin1_swedish_ci DEFAULT NULL,
`registered_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`server` varchar(255) DEFAULT 'default',
`plan_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

View File

@@ -0,0 +1,24 @@
include rules/crs-setup.conf
include rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
include rules/REQUEST-901-INITIALIZATION.conf
include rules/REQUEST-905-COMMON-EXCEPTIONS.conf
include rules/REQUEST-911-METHOD-ENFORCEMENT.conf
include rules/REQUEST-913-SCANNER-DETECTION.conf
include rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
include rules/REQUEST-921-PROTOCOL-ATTACK.conf
include rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
include rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
include rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
include rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
include rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
include rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
include rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
include rules/REQUEST-949-BLOCKING-EVALUATION.conf
include rules/RESPONSE-950-DATA-LEAKAGES.conf
include rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
include rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
include rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
include rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
include rules/RESPONSE-959-BLOCKING-EVALUATION.conf
include rules/RESPONSE-980-CORRELATION.conf
include rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

View File

@@ -0,0 +1,228 @@
# -- Rule engine initialization ----------------------------------------------
# Enable ModSecurity, attaching it to every transaction. Use detection
# only to start with, because that minimises the chances of post-installation
# disruption.
#
SecRuleEngine DetectionOnly
# -- Request body handling ---------------------------------------------------
# Allow ModSecurity to access request bodies. If you don't, ModSecurity
# won't be able to see any POST parameters, which opens a large security
# hole for attackers to exploit.
#
SecRequestBodyAccess On
# Enable XML request body parser.
# Initiate XML Processor in case of xml content-type
#
SecRule REQUEST_HEADERS:Content-Type "(?:text|application)/xml" \
"id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML"
# Enable JSON request body parser.
# Initiate JSON Processor in case of JSON content-type; change accordingly
# if your application does not use 'application/json'
#
SecRule REQUEST_HEADERS:Content-Type "application/json" \
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
# Maximum request body size we will accept for buffering. If you support
# file uploads then the value given on the first line has to be as large
# as the largest file you are willing to accept. The second value refers
# to the size of data, with files excluded. You want to keep that value as
# low as practical.
#
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Store up to 128 KB of request body data in memory. When the multipart
# parser reachers this limit, it will start using your hard disk for
# storage. That is slow, but unavoidable.
# Obsolete
#SecRequestBodyInMemoryLimit 131072
# What do do if the request body size is above our configured limit.
# Keep in mind that this setting will automatically be set to ProcessPartial
# when SecRuleEngine is set to DetectionOnly mode in order to minimize
# disruptions when initially deploying ModSecurity.
#
SecRequestBodyLimitAction Reject
# Verify that we've correctly processed the request body.
# As a rule of thumb, when failing to process a request body
# you should reject the request (when deployed in blocking mode)
# or log a high-severity alert (when deployed in detection-only mode).
#
SecRule REQBODY_ERROR "!@eq 0" \
"id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2"
# By default be strict with what we accept in the multipart/form-data
# request body. If the rule below proves to be too strict for your
# environment consider changing it to detection-only. You are encouraged
# _not_ to remove it altogether.
#
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:'200003',phase:2,t:none,log,deny,status:400, \
msg:'Multipart request body failed strict validation: \
PE %{REQBODY_PROCESSOR_ERROR}, \
BQ %{MULTIPART_BOUNDARY_QUOTED}, \
BW %{MULTIPART_BOUNDARY_WHITESPACE}, \
DB %{MULTIPART_DATA_BEFORE}, \
DA %{MULTIPART_DATA_AFTER}, \
HF %{MULTIPART_HEADER_FOLDING}, \
LF %{MULTIPART_LF_LINE}, \
SM %{MULTIPART_MISSING_SEMICOLON}, \
IQ %{MULTIPART_INVALID_QUOTING}, \
IP %{MULTIPART_INVALID_PART}, \
IH %{MULTIPART_INVALID_HEADER_FOLDING}, \
FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'"
# Did we see anything that might be a boundary?
#
SecRule MULTIPART_UNMATCHED_BOUNDARY "!@eq 0" \
"id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'"
# PCRE Tuning
# We want to avoid a potential RegEx DoS condition
#
SecPcreMatchLimit 1000
SecPcreMatchLimitRecursion 1000
# Some internal errors will set flags in TX and we will need to look for these.
# All of these are prefixed with "MSC_". The following flags currently exist:
#
# MSC_PCRE_LIMITS_EXCEEDED: PCRE match limits were exceeded.
#
SecRule TX:/^MSC_/ "!@streq 0" \
"id:'200005',phase:2,t:none,deny,msg:'ModSecurity internal error flagged: %{MATCHED_VAR_NAME}'"
# -- Response body handling --------------------------------------------------
# Allow ModSecurity to access response bodies.
# You should have this directive enabled in order to identify errors
# and data leakage issues.
#
# Do keep in mind that enabling this directive does increases both
# memory consumption and response latency.
#
SecResponseBodyAccess On
# Which response MIME types do you want to inspect? You should adjust the
# configuration below to catch documents but avoid static files
# (e.g., images and archives).
#
SecResponseBodyMimeType text/plain text/html text/xml
# Buffer response bodies of up to 512 KB in length.
SecResponseBodyLimit 524288
# What happens when we encounter a response body larger than the configured
# limit? By default, we process what we have and let the rest through.
# That's somewhat less secure, but does not break any legitimate pages.
#
SecResponseBodyLimitAction ProcessPartial
# -- Filesystem configuration ------------------------------------------------
# The location where ModSecurity stores temporary files (for example, when
# it needs to handle a file upload that is larger than the configured limit).
#
# This default setting is chosen due to all systems have /tmp available however,
# this is less than ideal. It is recommended that you specify a location that's private.
#
SecTmpDir /tmp/
# The location where ModSecurity will keep its persistent data. This default setting
# is chosen due to all systems have /tmp available however, it
# too should be updated to a place that other users can't access.
#
SecDataDir /tmp/
# -- File uploads handling configuration -------------------------------------
# The location where ModSecurity stores intercepted uploaded files. This
# location must be private to ModSecurity. You don't want other users on
# the server to access the files, do you?
#
#SecUploadDir /opt/modsecurity/var/upload/
# By default, only keep the files that were determined to be unusual
# in some way (by an external inspection script). For this to work you
# will also need at least one file inspection rule.
#
#SecUploadKeepFiles RelevantOnly
# Uploaded files are by default created with permissions that do not allow
# any other user to access them. You may need to relax that if you want to
# interface ModSecurity to an external program (e.g., an anti-virus).
#
#SecUploadFileMode 0600
# -- Debug log configuration -------------------------------------------------
# The default debug log configuration is to duplicate the error, warning
# and notice messages from the error log.
#
#SecDebugLog /opt/modsecurity/var/log/debug.log
#SecDebugLogLevel 3
# -- Audit log configuration -------------------------------------------------
# Log the transactions that are marked by a rule, as well as those that
# trigger a server error (determined by a 5xx or 4xx, excluding 404,
# level response status codes).
#
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
# Log everything we know about a transaction.
SecAuditLogParts ABIJDEFHZ
# Use a single file for logging. This is much easier to look at, but
# assumes that you will use the audit log only ocassionally.
#
SecAuditLogType Serial
SecAuditLog /var/log/modsec_audit.log
# Specify the path for concurrent audit logging.
SecAuditLogStorageDir /opt/modsecurity/var/audit/
# -- Miscellaneous -----------------------------------------------------------
# Use the most commonly used application/x-www-form-urlencoded parameter
# separator. There's probably only one application somewhere that uses
# something else so don't expect to change this value.
#
SecArgumentSeparator &
# Settle on version 0 (zero) cookies, as that is what most applications
# use. Using an incorrect cookie version may open your installation to
# evasion attacks (against the rules that examine named cookies).
#
SecCookieFormat 0
# Specify your Unicode Code Point.
# This mapping is used by the t:urlDecodeUni transformation function
# to properly map encoded data to your language. Properly setting
# these directives helps to reduce false positives and negatives.
#
SecUnicodeMapFile /ModSecurity/unicode.mapping 20127
# Improve the quality of ModSecurity by sharing information about your
# current ModSecurity version and dependencies versions.
# The following information will be shared: ModSecurity version,
# Web Server version, APR version, PCRE version, Lua version, Libxml2
# version, Anonymous unique id for host.
SecStatusEngine On
#Load all Rule
Include modsec_includes.conf

View File

@@ -12,21 +12,6 @@
</Directory>
# <!-- END EXPOSED RESOURCES PROTECTION -->
##### /phmyadmin on #####
ProxyRequests Off
ProxyPreserveHost On
<Proxy>
Require all granted
</Proxy>
ProxyPass /phpmyadmin http://localhost:8080/
ProxyPassReverse /phpmyadmin http://localhost:8080/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Ssl on
##### /phpmyadmin end #####
<Directory /home/<USER>/<DOMAIN_NAME>>
Options Indexes FollowSymLinks
AllowOverride All
@@ -61,21 +46,6 @@
SSLCertificateFile /etc/apache2/ssl/cert.crt
SSLCertificateKeyFile /etc/apache2/ssl/cert.key
##### /phmyadmin on #####
ProxyRequests Off
ProxyPreserveHost On
<Proxy>
Require all granted
</Proxy>
ProxyPass /phpmyadmin http://localhost:8080/
ProxyPassReverse /phpmyadmin http://localhost:8080/
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Ssl on
##### /phpmyadmin end #####
<Directory /home/<USER>/<DOMAIN_NAME>>
Options Indexes FollowSymLinks
AllowOverride All

View File

@@ -10,20 +10,7 @@ server {
return 403;
}
# <!-- END EXPOSED RESOURCES PROTECTION -->
##### /phmyadmin on #####
location /phpmyadmin {
proxy_pass http://localhost:8080/;
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 $scheme;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
}
##### /phmyadmin off #####
root /home/<USER>/<DOMAIN_NAME>;
location / {
@@ -62,19 +49,6 @@ server {
listen 443 ssl http2;
server_name <DOMAIN_NAME> www.<DOMAIN_NAME>;
access_log /var/log/nginx/domlogs/<DOMAIN_NAME>.log;
##### /phmyadmin on #####
location /phpmyadmin {
proxy_pass http://localhost:8080/;
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 $scheme;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
}
##### /phmyadmin off #####
root /home/<USER>/<DOMAIN_NAME>;

View File

@@ -17,6 +17,8 @@ server {
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 $scheme;
proxy_pass_header Server;
proxy_hide_header Server;
}
# openpanel

View File

@@ -2,7 +2,7 @@ server {
listen <LISTEN_IP>;
server_name <DOMAIN_NAME> www.<DOMAIN_NAME>;
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/main.conf;
modsecurity_rules_file /usr/local/nginx/conf/modsecurity.conf;
include snippets/error_pages.conf;
access_log /var/log/nginx/domlogs/<DOMAIN_NAME>.log;
include /etc/openpanel/openpanel/core/users/<USERNAME>/domains/<DOMAIN_NAME>-block_ips.conf;
@@ -19,6 +19,8 @@ server {
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 $scheme;
proxy_pass_header Server;
proxy_hide_header Server;
}
# openpanel

View File

@@ -1,35 +1,14 @@
# openpanel
location /openpanel {
proxy_pass https://127.0.0.1:2083;
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 $scheme;
return 301 https://localhost:2083;
}
# openadmin
location /openadmin {
proxy_pass https://127.0.0.1:2087;
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 $scheme;
return 301 https://localhost:2087;
}
# webmail
# roundcube
location /webmail {
proxy_pass http://127.0.0.1:8080;
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 $scheme;
}
# Autodiscover for Outlook & Thunderbird
# https://github.com/stefanpejcic/Autodiscover/tree/main
location /AutoDiscover {
proxy_pass http://127.0.0.1:8000/autodiscover/autodiscover.xml;
}
location /mail {
proxy_pass http://127.0.0.1:8000/mail/config-v1.1.xml;
return 301 https://webmail.localhost/;
}

View File

@@ -6,6 +6,6 @@
"schedule": "weekly",
"retention": "4",
"status": "off",
"filters": ["files", "entrypoint", "apache-conf", "mysql-data", "mysql-conf", "php-versions", "crontab", "user-data", "core-users", "stats-users", "apache-ssl-conf", "domain-access-reports", "ssh", "timzeone"],
"filters": ["files", "entrypoint", "apache-conf", "mysql-data", "mysql-conf", "php-versions", "crontab", "user-data", "core-users", "stats-users", "apache-ssl-conf", "domain-access-reports", "ssh", "timezone"],
"id": "3"
}

View File

@@ -12,6 +12,7 @@
"OpenCLI Logs": "/var/log/openpanel/admin/opencli.log",
"Certbot SSL Logs": "/var/log/letsencrypt/letsencrypt.log",
"MySQL Logs": "/var/log/mysql.log",
"FTP Logs": "/var/log/ftp.log",
"CSF Deny Log": "/etc/csf/csf.deny",
"AuthLog": "/var/log/auth.log",
"DPKG Log": "/var/log/dpkg.log",

View File

@@ -53,6 +53,12 @@
"on_dashboard": true,
"real_name": "openpanel_dns"
},
{
"name": "SSL",
"type": "docker",
"on_dashboard": false,
"real_name": "certbot"
},
{
"name": "ConfigServer Firewall",
"type": "system",

View File

@@ -23,7 +23,7 @@ ns3=
ns4=
email=
logout_url=
enabled_modules=dns,favorites,phpmyadmin,ssh,crons,backups,wordpress,pm2,disk_usage,inodes,usage,terminal,services,webserver,fix_permissions,process_manager,ip_blocker,redis,memcached,login_history,activity,twofa,domains_visitors
enabled_modules=dns,favorites,phpmyadmin,temporary_links,ssh,crons,backups,wordpress,pm2,disk_usage,inodes,usage,terminal,services,webserver,fix_permissions,process_manager,ip_blocker,redis,memcached,login_history,activity,twofa,domains_visitors
available_modules=malware_scan, elasticsearch
@@ -68,6 +68,7 @@ basic_auth=no
basic_auth_username=
basic_auth_password=
screenshots=http://screenshots-api.openpanel.com/screenshot
temporary_links=https://preview.openpanel.org/index.php
[SMTP]
mail_server=
@@ -84,9 +85,6 @@ max_ram=90
max_cpu=95
[PHPMYADMIN]
pma_url=
[SECURITY]
backups_encryption_key=

View File

@@ -0,0 +1 @@
https://downloads.ioncube.com/loader_downloads/ioncube_loaders_lin_x86-64.tar.gz

View File

@@ -0,0 +1,86 @@
# cPanel 2 OpenPanel user import
Free OpenPanel module to import cPanel backup in OpenPanel
Maintained by [CodeWithJuber](https://github.com/CodeWithJuber)
## Features
Currently suported for import:
- files and folders
- mysql databases, users and grants
- domains
- dns zones
- php version
- wp sites
- cronjobs
todo:
- ftp accounts
- email accounts
- nodejs/python apps
- postgresql
- ssh keys
- ssl certificates
Steps:
# cPanel to OpenPanel Migration Script
This script automates the process of migrating a cPanel backup to OpenPanel server. It handles various cPanel backup formats and restores essential components of the user's account.
## Features
- Supports multiple cPanel backup formats (cpmove, full backup, tar.gz, tgz, tar, zip)
- Restores user account details, domains, and hosting plan settings
- Migrates websites, databases, domains, SSL certificates, and DNS zones
- Handles PHP version settings and cron jobs
- Restores SSH access and file permissions
## Usage
1. Run the script with sudo privileges:
```
git clone https://github.com/stefanpejcic/cPanel-to-OpenPanel
```
```
bash cPanel-to-OpenPanel/cp-import.sh --backup-location /path/to/cpanel_backup.file --plan-name "default_plan_nginx"
```
## Parameters
- `--backup-location`: Path to the cPanel backup file (required)
- `--plan-name`: Name of the hosting plan in OpenPanel (required)
## Important Notes
- This script should be run on the OpenPanel server where you want to import the cPanel backup.
- The script requires internet access to install dependencies if they are not already present.
- Large backups may take a considerable amount of time to process.
- Some manual configuration may be required after the migration, depending on the complexity of the cPanel account.
## Troubleshooting
If you encounter any issues:
1. Check the script's output for error messages.
2. Verify that all prerequisites are met.
3. Ensure you have sufficient disk space and system resources.
4. Check the OpenPanel logs for any additional error information.
## Contributing
Contributions to improve the script are welcome. Please feel free to submit issues or pull requests.
## License
[MIT License](LICENSE)
## Disclaimer
This script is provided as-is, without any guarantees. Always test thoroughly in a non-production environment before using in production.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,35 @@
import os
import json # will use for get to return data
import socket
from flask import Flask, Response, abort, render_template, request, send_file, g, jsonify, session, url_for, flash, redirect, get_flashed_messages
import subprocess
from app import app, is_license_valid, login_required_route
from modules.helpers import get_all_plans, is_username_unique
@app.route('/import/cpanel', methods=['GET', 'POST'])
@login_required
def import_cpanel_whm_account():
if request.method == 'POST':
path = request.form.get('path')
plan_name = request.form.get('plan_name')
if not path or not plan_name:
flash('Both path to the cPanel backup file (.tar.gz) and plan name are required!', 'error')
return redirect('/import/cpanel')
try:
file_name = os.path.basename(path)
log_file_name = f"cpanel_import_log_{os.path.splitext(file_name)[0]}"
log_file_path = f"/var/log/openpanel/admin/{log_file_name}.log"
# Run the subprocess command and redirect stdout and stderr to the log file
with open(log_file_path, 'w') as log_file:
subprocess.Popen(['opencli', 'user-import', 'cpanel', path, plan_name], stdout=log_file, stderr=log_file)
flash(f'Import started! To track the progress open the log file: {log_file_path}', 'success')
except Exception as e:
flash(f'An error occurred: {str(e)}', 'error')
return redirect('/import/cpanel')
else:
# on GET we will list the sessions in progress..
return render_template('cpanel-import.html', title='Import cPanel account')

View File

@@ -0,0 +1,47 @@
import sys
import re
def remove_localhost_lines_and_replace_grant_usage(input_path, output_path):
try:
with open(input_path, 'r') as infile:
lines = infile.readlines()
with open(output_path, 'w') as outfile:
for line in lines:
if 'localhost' in line:
continue
# Replace escaped underscores
line = line.replace(r'\_', '_')
# Check if the line starts with "GRANT USAGE ON"
if line.startswith('GRANT USAGE ON'):
# Extract the username and password from the line using regex
match = re.match(r"GRANT USAGE ON \*\.\* TO '([^']+)'@'([^']+)' IDENTIFIED BY PASSWORD '([^']+)'", line)
if match:
user = match.group(1)
host = match.group(2)
password = match.group(3)
new_line = f"CREATE USER '{user}'@'{host}' IDENTIFIED WITH 'mysql_native_password' AS '{password}';\n"
outfile.write(new_line)
else:
# If regex doesn't match, write the original line
outfile.write(line)
else:
outfile.write(line)
print(f"Processed file saved to {output_path}")
except FileNotFoundError:
print(f"Error: The file {input_path} was not found.")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python script.py <input_sql_file> <output_sql_file>")
else:
input_path = sys.argv[1]
output_path = sys.argv[2]
remove_localhost_lines_and_replace_grant_usage(input_path, output_path)

View File

@@ -0,0 +1,19 @@
{% extends 'base.html' %}
{% block content %}
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-warning alert-dismissible" role="alert">
{{ message }}
<a class="btn-close" data-bs-dismiss="alert" aria-label="close"></a>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% endblock %}

View File

@@ -0,0 +1 @@
^LICENSE\.md$

View File

@@ -0,0 +1 @@
@marketplace-eng

View File

@@ -0,0 +1,27 @@
# 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.

43
integrations/digitalocean/.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# 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

View File

@@ -0,0 +1,812 @@
## CHANGELOG
### Remove fabric and add support for Ubuntu 20.04
> Branches Affected: master
> Tags Affected:
> Date: Fri, 8 Mar 2021 09:50:00 +0000
> Author: Mauricio Vargas (@pachamaltese)
- add support to image test script for Ubuntu 20.04
- removes fabric because of problems with Python 3
- updated documentation
### Add Support for Debian 9
> Branches Affected: master
> Tags Affected:
> Date: Fri, 29 Mar 2019 18:06:36 +0000
> Author: root (root@debian-s-1vcpu-1gb-nyc1-01.localdomain)
> Committer: root (root@debian-s-1vcpu-1gb-nyc1-01.localdomain)
- add support to image test script for Debian
- update README's
- for more info on changelog hooks please ref: https://github.com/dOpensource/dsiprouter/tree/dev/resources/git
---
### Update build-an-image-fabric.md
> Branches Affected: master
> Tags Affected:
> Date: Mon, 25 Mar 2019 15:41:52 +0100
> Author: Divyendu Singh (divyendu.z@gmail.com)
> Committer: GitHub (noreply@github.com)
---
### IMG-535 - Ensure validation script exits with proper status codes
> Branches Affected: master
> Tags Affected:
> Date: Thu, 21 Mar 2019 13:19:37 -0500
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Add step to cleanup to clear root mailbox if any mail is present
> Branches Affected: master
> Tags Affected:
> Date: Thu, 21 Mar 2019 13:13:49 -0500
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### also allow CentOs convention for locked password on root
> Branches Affected: master
> Tags Affected:
> Date: Mon, 18 Mar 2019 18:40:25 -0400
> Author: Ken Bingham (w@qrk.us)
> Committer: Ken Bingham (w@qrk.us)
---
### img_check.sh: Print date that script is run
> Branches Affected: master
> Tags Affected:
> Date: Wed, 13 Mar 2019 16:03:30 -0400
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
Assists in debugging efforts that may come up between DO and Vendors, as well as in DO's listing submission process.
---
### build-an-image.md: doctl instructions added
> Branches Affected: master
> Tags Affected:
> Date: Wed, 13 Mar 2019 14:40:54 -0400
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
---
### removed .backup file
> Branches Affected: master
> Tags Affected:
> Date: Mon, 11 Mar 2019 11:17:59 -0500
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Added Packer example and reorganized template folders
> Branches Affected: master
> Tags Affected:
> Date: Mon, 11 Mar 2019 11:16:37 -0500
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### update img_check.sh exclude lfd.log log
> Branches Affected: master
> Tags Affected:
> Date: Wed, 6 Mar 2019 23:25:48 +1000
> Author: George Liu (eva2000@centminmod.com)
> Committer: George Liu (eva2000@centminmod.com)
---
### img_check.sh centos 7 & csf firewall compatibility
> Branches Affected: master
> Tags Affected:
> Date: Wed, 6 Mar 2019 22:00:27 +1000
> Author: George Liu (eva2000@centminmod.com)
> Committer: George Liu (eva2000@centminmod.com)
---
### README.md: Link out to snapshots documentation
> Branches Affected: master
> Tags Affected:
> Date: Mon, 4 Mar 2019 16:38:06 -0500
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
---
### Update build-an-image-fabric.md
> Branches Affected: master
> Tags Affected:
> Date: Thu, 28 Feb 2019 12:03:14 -0500
> Author: Rijk van Zanten (rijkvanzanten@me.com)
> Committer: GitHub (noreply@github.com)
Hey there! I ran into some versioning issues while working with the example project. I hope this note will make sure other people don't spend 30min messing around as I did 🙂
---
### Update 001_onboot
> Branches Affected: master
> Tags Affected:
> Date: Fri, 22 Feb 2019 14:29:46 -0600
> Author: Ryan Quinn (ryan@digitalocean.com)
> Committer: GitHub (noreply@github.com)
Fix for typo reported by @kspearrin
---
### Fix for IMG-559 / Issue 16 - Fail if private key found
> Branches Affected: master
> Tags Affected:
> Date: Fri, 22 Feb 2019 08:16:47 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### remove shutdown func add exit to build func and add copy for user
> Branches Affected: master
> Tags Affected:
> Date: Fri, 15 Feb 2019 10:30:09 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### stop apt-get from prompting about config files when upgrading packages
> Branches Affected: master
> Tags Affected:
> Date: Fri, 15 Feb 2019 10:04:01 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### replace per-once with per-instance
> Branches Affected: master
> Tags Affected:
> Date: Fri, 15 Feb 2019 09:57:35 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Fix to change recommendation from per-once to per-instance on firstboot scripts
> Branches Affected: master
> Tags Affected:
> Date: Wed, 13 Feb 2019 13:14:51 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Ensure instance folder is removed on build system
> Branches Affected: master
> Tags Affected:
> Date: Tue, 12 Feb 2019 15:05:55 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Fix for permissions not being copied and adding noninteractive env var
> Branches Affected: master
> Tags Affected:
> Date: Tue, 12 Feb 2019 15:04:53 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### moved postfix install fix to run if uncommented before apt installs
> Branches Affected: master
> Tags Affected:
> Date: Tue, 12 Feb 2019 12:41:02 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### build-an-image.md: typo
> Branches Affected: master
> Tags Affected:
> Date: Mon, 4 Feb 2019 17:31:02 -0500
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
---
### Couple of extra fixes, emphasis on MOTD
> Branches Affected: master
> Tags Affected:
> Date: Fri, 1 Feb 2019 15:28:23 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### Add updates and upgrades to cleanup example script for manual build + remove byoi_validation leftover
> Branches Affected: master
> Tags Affected:
> Date: Tue, 29 Jan 2019 14:28:20 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### Delete BYOI version of the script
> Branches Affected: master
> Tags Affected:
> Date: Tue, 22 Jan 2019 16:49:31 -0500
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
Will be confusing for users and we don't plan to push anyone to BYOI in the near term. Can always pull it back into master later on.
---
### Initial release
> Branches Affected: master
> Tags Affected: v1.0
> Date: Wed, 16 Jan 2019 15:46:02 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### removed .vscode directory
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 14:38:55 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### typos *facepalm*
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 15:35:56 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### repo README.md updates to just point to the new docs gonna merge the branch shortly
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 15:34:26 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### formatting typos
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 15:28:42 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### tweaks to fabric doc
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 15:26:53 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### revision of the manual image build documentation
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 14:13:08 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### adding some simple img_check and snapshot instructions
> Branches Affected: master
> Tags Affected:
> Date: Wed, 16 Jan 2019 13:52:45 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### just getting my various draft changes into order for Ryan
> Branches Affected: master
> Tags Affected:
> Date: Tue, 15 Jan 2019 15:25:41 -0500
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### img_check.sh: list the uninstalled security packages
> Branches Affected: master
> Tags Affected:
> Date: Thu, 10 Jan 2019 16:15:45 -0500
> Author: John Gannon (jgannon@digitalocean.com)
> Committer: GitHub (noreply@github.com)
---
### removed backup file
> Branches Affected: master
> Tags Affected:
> Date: Mon, 7 Jan 2019 12:44:24 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### changed name from "example" to "template"
> Branches Affected: master
> Tags Affected:
> Date: Mon, 7 Jan 2019 12:43:21 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Adding simple fabric example
> Branches Affected: master
> Tags Affected:
> Date: Mon, 7 Jan 2019 12:40:08 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### fix a typo
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 12:50:59 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### language pass, samples subfolder for various scripts
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 12:49:42 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### tweaker
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 12:42:34 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### prefix tweak
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 12:37:58 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### tweaks
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 11:57:44 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### moving docs around a bit
> Branches Affected: master
> Tags Affected:
> Date: Thu, 3 Jan 2019 11:17:55 -0800
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### Fix for Issue 8 - script crashes if apt-cache is not present on Ubuntu
> Branches Affected: master
> Tags Affected:
> Date: Tue, 4 Dec 2018 09:02:39 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Starter doc
> Branches Affected: master
> Tags Affected:
> Date: Fri, 9 Nov 2018 10:19:00 -0800
> Author: Nick Wade (nwade@digitalocean.com)
> Committer: Nick Wade (nwade@digitalocean.com)
---
### update for IMG-533 (check for do-agent)
> Branches Affected: master
> Tags Affected:
> Date: Fri, 9 Nov 2018 11:02:09 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Print authorized keys file
> Branches Affected: master
> Tags Affected:
> Date: Fri, 9 Nov 2018 10:21:30 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### print contents of authorized keys file if detected
> Branches Affected: master
> Tags Affected:
> Date: Fri, 9 Nov 2018 10:16:53 -0600
> Author: rquinn (ryan@digitalocean.com)
> Committer: rquinn (ryan@digitalocean.com)
---
### Spelling mistakes correction
> Branches Affected: master
> Tags Affected:
> Date: Sun, 21 Oct 2018 15:50:28 +0530
> Author: akshaybengani789 (33260831+akshaybengani789@users.noreply.github.com)
> Committer: GitHub (noreply@github.com)
---
### remove final undefined BYOI references
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 15:11:05 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### update to version 0.1 and release tarball
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 15:03:09 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### no message
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 14:59:34 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### last heading fix
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 14:45:56 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### headings skipped
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 14:44:54 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### woops, heading space required
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 14:43:18 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### A few typo and consistency changes, along with fixing the image asset path
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 14:41:29 -0700
> Author: Nick Wade (9014043+wadenick@users.noreply.github.com)
> Committer: Nick Wade (9014043+wadenick@users.noreply.github.com)
---
### Initial open sourcing
> Branches Affected: master
> Tags Affected:
> Date: Thu, 18 Oct 2018 11:11:17 -0700
> Author: Nick Wade (nwade@digitalocean.com)
> Committer: Nick Wade (nwade@digitalocean.com)
---
### Update README.md
> Branches Affected: master
> Tags Affected:
> Date: Tue, 16 Oct 2018 13:39:36 -0700
> Author: wadenick (9014043+wadenick@users.noreply.github.com)
> Committer: GitHub (noreply@github.com)
---
### Initial commit
> Branches Affected: master
> Tags Affected:
> Date: Tue, 16 Oct 2018 13:20:47 -0700
> Author: wadenick (9014043+wadenick@users.noreply.github.com)
> Committer: GitHub (noreply@github.com)
---

View File

@@ -0,0 +1,194 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,449 @@
# DigitalOcean Marketplace Partner Tools
[![Apache license](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Pull Requests Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com)
This repository contains resources for [DigitalOcean Marketplace](https://marketplace.digitalocean.com/) partners, like documentation on image requirements and creation, tools for image cleanup and validation, and templates for build automation.
## Getting Started creating your Kubernetes based 1-Click App
Please visit [our Kubernetes Marketplace repo](https://github.com/digitalocean/marketplace-kubernetes/blob/master/CONTRIBUTING.md) for instructions on how to create and submit your Kubernetes based 1-Click App.
Kubernetes 1-Click Apps can be submitted via our [Marketplace Kubernetes Github repo](https://github.com/digitalocean/marketplace-kubernetes).
## Getting Started creating your Droplet based 1-Click App
The overall process for creating an image that you can submit as a Droplet based 1-Click App is as follows:
1. Create and configure a build Droplet manually first to make sure your configuration works. You can create a build Droplet with any method, like the [control panel](https://cloud.digitalocean.com/), the [API](https://developers.digitalocean.com/), or command-line tools like [`doctl`](https://github.com/digitalocean/doctl). **We strongly encourage you to use a $6 Droplet as your build Droplet.** Using a $6 Droplet as your build Droplet will ensure that all Droplet types will be available for usage with your 1-Click App.
2. Clean up and validate the build Droplet with the provided scripts, `cleanup.sh` and `img_check.sh`. The scripts will check for and fix potential security concerns and verify that the image will be compatible with Marketplace.
3. Use Packer to create a fresh [snapshot](https://www.digitalocean.com/docs/images/snapshots/) of the image that you want to create. While there are several ways to create an image, we recommend using Packer as the most simple and consistent option.
4. Submit your final image to the Marketplace team for review. This can be made through [our Vendor Portal](https://cloud.digitalocean.com/vendorportal). If you've signed expressed interest in joining the Marketplace through [the form on this page](https://marketplace.digitalocean.com/vendors) but you've not received a login for the Vendor Portal, please reach out to one-clicks-team@digitalocean.com and we'll help you out.
<a name="dbaas-integration"></a>
5. **(Optional)** Integrate a DigitalOcean Managed Database into your 1-Click App</h4>
As a Vendor, you can offer a DigitalOcean Managed Database (DBaaS) to any DigitalOcean customer at the time they spin up your Droplet based 1-Click App. You can customize your app image to integrate with the managed database directly, or let your customers complete the configuration themselves after the 1-Click App boots up. This benefits your customers in terms of database scalability and ease of management, and reduces the burden of database support for you as a vendor.
To enable this option, use the checkboxes that are shown in the Vendor Portal related to enablement of managed databases. If youve checked at least 1 box, when a user attempts to create your 1-Click App, theyll receive a prompt like this one:
![dbaas_1_click_offer](/images/dbaas_1_click_offer.png)
When a user selects the Managed Database option, DigitalOcean handles the creation of the database cluster as well as the user's Droplet. The Droplet will have a `DATABASE_URL` environment variable configured including a database connection string, such as:
`postgresql://doadmin:<password>@dbaas-db-11111-do-user-1111111-1.b.db.ondigitalocean.com:25060/defaultdb?sslmode=require`
The users managed database configuration and credentials will be stored in `/root/.digitalocean_dbaas_credentials` in the following format.
```
db_protocol=
db_username=
db_password=
db_host=
db_port=
db_database=
```
To disable this feature for any new users of your 1-Click App, simply visit the Vendor Portal to edit your 1-Click App, removing the checkboxes next to all database engines. Once you save the edits, customers will no longer be given the option to add a managed database. Note that existing customers who have already deployed a managed database in conjunction with your 1-Click App will not be affected by that removal, and their managed databases will continue to operate.
## Build Automation with Packer
[Packer](https://www.packer.io/intro) is a tool for creating images from a single source configuration. Using this Packer template reduces the entire process of creating, configuring, validating, and snapshotting a build Droplet to a single command:
```
packer build marketplace-image.json
```
By doing this, there is a reduced likelihood of having to submit an image multiple times as a result of falling in any of the next steps:
- Installing OS updates
- Deleting bash history.
- Removing log files and SSH keys from the root user
- Enabling the firewall (i.e. ufw if you use Ubuntu)
This repository is itself a Packer template for a LAMP stack. You can modify this template to use as a starting point for your image. Note that not all of the scripts/files in this repository are strictly necessary, as this aim at covering a broad case of application.
## Usage
To run the LAMP stack in this template, you'll need to [install Packer](https://www.packer.io/intro/getting-started/install.html) and [create a DigitalOcean personal access token](https://docs.digitalocean.com/reference/api/create-personal-access-token/) and set it to the `DIGITALOCEAN_TOKEN` environment variable. Running `packer build marketplace-image.json` without any other modifications will create a build Droplet configured with LAMP, clean and verify it, then power it down and snapshot it.
To start adapting this template for your own image, you can customize some variables in `marketplace-image.json`:
* `apt_packages` lists the APT packages to install on the build Droplet.
* `image_name` defines the name of the resulting snapshot, which by default is `marketplace-snapshot-` with a UNIX timestamp appended.
You can also modify these variables at runtime by using [the `-var` flag](https://www.packer.io/docs/templates/legacy_json_templates/user-variables).
Please see the [RStudio Server 1-Click Scripts](https://github.com/pachadotdev/rstudio-server-droplet) to see an example of Packer usage.
A successful run would look like this output:
```
pacha@pop-os:~/github/marketplace-partners$ packer build marketplace-image.json
digitalocean output will be in this color.
==> digitalocean: Creating temporary ssh key for droplet...
==> digitalocean: Creating droplet...
==> digitalocean: Waiting for droplet to become active...
==> digitalocean: Using ssh communicator to connect: 165.227.211.66
==> digitalocean: Waiting for SSH to become available...
==> digitalocean: Connected to SSH!
==> digitalocean: Provisioning with shell script: /tmp/packer-shell581341144
digitalocean: .............................................................
digitalocean: status: done
==> digitalocean: Uploading files/etc/ => /etc/
==> digitalocean: Uploading files/var/ => /var/
==> digitalocean: Provisioning with shell script: /tmp/packer-shell079619818
digitalocean:
digitalocean: WARNING: apt does not have a stable CLI interface. Use with caution in scripts.
...
digitalocean: The following NEW packages will be installed:
digitalocean: linux-headers-5.4.0-66 linux-headers-5.4.0-66-generic
digitalocean: The following packages will be upgraded:
digitalocean: accountsservice alsa-ucm-conf apport apt apt-utils base-files bind9-dnsutils
... MANY MORE LINES OMITTED HERE ...
digitalocean: Distribution: Ubuntu
digitalocean: Version: 20.04
digitalocean:
digitalocean: [PASS] Supported Operating System Detected: Ubuntu
digitalocean: [PASS] Supported Release Detected: 20.04
digitalocean: [PASS] Cloud-init is installed.
digitalocean: [PASS] Firewall service (ufw) is active
digitalocean:
digitalocean: Updating apt package database to check for security updates, this may take a minute...
digitalocean:
digitalocean: [PASS] There are no pending security updates for this image.
digitalocean:
digitalocean:
digitalocean: Checking for log files in /var/log
digitalocean:
digitalocean: [WARN] un-cleared log file, /var/log/auth.log found
digitalocean: [WARN] un-cleared log file, /var/log/ufw.log found
digitalocean:
digitalocean:
digitalocean: Checking all user-created accounts...
digitalocean:
digitalocean:
digitalocean: Checking the root account...
digitalocean: [PASS] User root has no password set.
digitalocean: [ OK ] User root has no SSH keys present
digitalocean: [PASS] root's Bash History appears to have been cleared
digitalocean: [PASS] DigitalOcean Monitoring agent was not found
digitalocean: [PASS] MongoDB is not installed
digitalocean:
digitalocean: ---------------------------------------------------------------------------------------------------
digitalocean: Scan Complete.
digitalocean: Some non-critical tests failed. Please review these items.
digitalocean: ---------------------------------------------------------------------------------------------------
digitalocean: 8 Tests PASSED
digitalocean: 2 WARNINGS
digitalocean: 0 Tests FAILED
digitalocean: ---------------------------------------------------------------------------------------------------
digitalocean: Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission
digitalocean:
==> digitalocean: Gracefully shutting down droplet...
==> digitalocean: Creating snapshot: lemp-20-04-snapshot-1615212919
==> digitalocean: Waiting for snapshot to complete...
==> digitalocean: Destroying droplet...
==> digitalocean: Deleting temporary ssh key...
Build 'digitalocean' finished.
```
See that this output has two acceptable warnings, but something like having ufw disabled or present SSH keys means we couldn't accept the image.
## Configuration Details
By using [Packer's DigitalOcean Builder](https://www.packer.io/docs/builders/digitalocean.html) to integrate with the [DigitalOcean API](https://developers.digitalocean.com/), this template fully automates Marketplace image creation.
This template uses Packer's [file provisioner](https://www.packer.io/docs/provisioners/file.html) to upload complete directories to the Droplet. The contents of `files/var/` will be uploaded to `/var/`. Likewise, the contents of `files/etc/` will be uploaded to `/etc/`. One important thing to note about the file provisioner, from Packer's docs:
> The destination directory must already exist. If you need to create it, use a shell provisioner just prior to the file provisioner in order to create the directory. If the destination directory does not exist, the file provisioner may succeed, but it will have undefined results.
This template also uses Packer's [shell provisioner](https://www.packer.io/docs/provisioners/shell.html) to run scripts from the `/scripts` directory and install APT packages using an inline task.
Learn more about using Packer in [the official Packer documentation](https://www.packer.io/docs/).
## Other Examples
We also use Packer to build some of the Marketplace 1-Click Apps that DigitalOcean maintains. You can see the source code for these scripts [in this repo.](https://github.com/digitalocean/droplet-1-clicks)
## Update your App Image via API
The Vendor API makes it possible to update existing droplet 1-click apps programmatically. You can use this to ensure your listing features the most recently released version by tying it into your existing CI/CD pipeline to push an update to DigitalOcean.
Not all listing information can be updated via the API. For any changes outside the parameters of this request, you will still need to visit the Vendor Portal.
To update your app via the API, send a PATCH request to https://api.digitalocean.com/api/v1/vendor-portal/apps/<app_id>/versions/<version>
App ID can be obtained from your apps listing in Vendor Portal, via the URL. An invalid app ID will return a 404 Not Found error.
Apps in pending or in review state cannot be updated. Attempting to update an app in one of these states will return a 400 Bad Request error.
An authorization header with a bearer token is required to make the request. See [API documentation](https://docs.digitalocean.com/reference/api/api-reference/#section/Authentication) for more information on how to obtain this token.
### Request
**Authorizations**: bearer_auth (write)
**Request Body schema**: application/json
#### Parameters:
**imageId**
_required_
_integer_
ID of the image to use for your app. The image must be a snapshot already uploaded to your DigitalOcean account, under the team you use to access the vendor portal. In addition to Packer, snapshots can be created via the API through [droplet actions](https://docs.digitalocean.com/reference/api/api-reference/#tag/Droplet-Actions). For additional information about manipulating snapshots via the API, view the [API documentation](https://docs.digitalocean.com/reference/api/api-reference/#tag/Snapshots) for snapshots.
**reasonForUpdate**
_string_
A brief description of the changes made which necessitate this update.
**version**
_string_
The version to mark this update as.
**osVersion**
_string_
The version of the operating system your app runs on. A null value will not overwrite an existing value, but a blank string will.
**softwareIncluded**
_Array of Software (see below)_
Software types and versions included with your app. A null value will not overwrite an existing value, but an empty array will.
**Software**:
**name**
_string_
Name of this software. A null value will not overwrite an existing value, but a blank string will.
**version**
_string_
Version of this software in use by your app. A null value will not overwrite an existing value, but a blank string will.
**releaseNotes**
_string_
Any release notes to include alongside this software in your apps listing. A null value will not overwrite an existing value, but a blank string will.
**website**
_string_
The website for this software, for further information. A null value will not overwrite an existing value, but a blank string will.
**licenseType**
_string_
The type of license this software uses. A null value will not overwrite an existing value, but a blank string will.
**licenseLink**
_string_
A link to details of the license, if applicable. A null value will not overwrite an existing value, but a blank string will.
#### Example request:
```
curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $DIGITALOCEAN_API_TOKEN" \
-d '{
"reasonForUpdate": "example",
"version": "3",
"imageId": 413639,
"softwareIncluded": [
{
"name": "Ubuntu Linux",
"version": "22.04"
}
]
}' \
https://api.digitalocean.com/api/v1/vendor-portal/apps/$APP_ID/versions/$VERSION_ID
```
### Responses
**200 - success**
Returns app data in response.
_Example response body:_
```
{
"appId": "60089fc6d333037bffa70d9b",
"name": "Example App ",
"version": 4,
"type": "droplet",
...
"status":
{
"value":"pending",
"lastUpdated":1692302119556,
"modifiedBy":"administrator",
"reason":"example"
},
"emergencyContacts":
[
...
],
"customData":
{
"version": "3",
"osVersion": "Ubuntu 20.04",
"description": "app desc",
"summary": "app summary",
"imageLabel": "sample-20-04",
"imageId": 417346,
"imageName": "Sample on Ubuntu 20.04",
"imageDescription": "Sample 3 on Ubuntu 20.04",
"reasonForUpdate": "Sample",
...
}
}
```
**400 - Bad Request**
- Image ID was missing from request, or
- App in request was not a droplet 1-click, or
- Attempted an update on an app in pending or in-review status
**403 - Unauthorized**
- Authentication token was invalid, or
- App or image does not belong to requestors team
**404 - Not Found**
- App ID was not found or is invalid
### Automating with Packer
If you are using Packer, you can add a set of post-processor actions to automatically submit your new image to update your app, via the [manifest](https://developer.hashicorp.com/packer/docs/post-processors/manifest) and [shell-local](https://developer.hashicorp.com/packer/docs/post-processors/shell-local) post-processors, such as:
```
post-processors {
post-processor "manifest" {
output = "manifest.json"
strip_path = true
}
post-processor "shell-local" {
inline = [ "sh mp-submit.sh" ]
}
}
```
where mp-submit.sh is
```
#!/bin/bash
IMG_ID=$(jq '.builds[-1].artifact_id | split(":")[1] | tonumber' manifest.json)
curl -X PATCH -H "Content-Type: application/json" -H "Authorization: Bearer ${DIGITALOCEAN_API_TOKEN}" -d "{\"reasonForUpdate\": \"new version\", \"version\": \"${APP_VERSION}\", \"imageId\": ${IMG_ID}}" https://api.digitalocean.com/api/v1/vendor-portal/apps/${APP_ID}/versions/${APP_VERSION}
```
You will need to set env variables DIGITALOCEAN_API_TOKEN, APP_VERSION, and APP_ID in your terminal to use this script. It uses jq to parse the manifest Packer creates for the snapshot ID, then uses cURL to submit it to the API endpoint.
## Supported Operating Systems
To maintain compatibility with Marketplace tools and processes, we support a limited number of Linux distributions and releases for Marketplace images. These options provide either `deb`- or `rpm`-based packaging and will have security patches and updates for a reasonable time period.
We currently support the following OSes:
- Debian 9 (stretch)
- Debian 10 (buster)
- Ubuntu 24.04 (LTS)
- Ubuntu 22.04 (LTS)
- Ubuntu 20.04 (LTS)
- Ubuntu 18.04 (LTS)
- Ubuntu 16.04 (LTS)
- CentOS 7.x
- CentOS 8.x
- CentOS Stream 8
- CentOS Stream 9
- AlmaLinux 8.x
- AlmaLinux 9.x
- Rocky Linux 8.x
- Rocky Linux 9.x
All supported operating systems are available as base images to build on in the DigitalOcean cloud.
## Software Prerequisites
The following software packages are necessary for the initial configuration of new Droplets and to ensure connectivity:
- `cloud-init` 0.76 or higher (0.79 or higher recommended)
- `openssh-server` (SFTP-enabled configuration recommended)
All of these packages are provided by default in the default DigitalOcean base images.
## Image Configuration
### Running Commands on First Boot
You can often pre-load much of what your image will need in your build system, but some setup (like setting database passwords or configuration that needs the Droplet's assigned IP address) will need to be run for each new Droplet created from your image.
You can create scripts that run on first boot using cloud-init. Droplets will attempt to run any scripts located in the `/var/lib/cloud/scripts/per-instance` directory when they're first created. Scripts in that directory are run in alphanumerical order, so we recommend using a number as the beginning of the file names (e.g. `01-example-script.sh`).
Make sure you can run the script from the command line successfully and that it has execute permissions.
### Running Commands on First Login
Some of your image setup may require information that you can't get automatically, like the domain name to use for a service. You may also need to run interactive third-party scripts, like LetsEncrypt's Certbot.
To run a script on the user's first login, we recommend adding a line to the root `.bashrc` file that runs the script and adding a line to the script that removes the line from the root `.bashrc` file.
More specifically, at the end of the script you want to run on first login, add the following line. For consistency, we recommend putting first login scripts in the `/opt/your_company_name` directory. Make sure the script has execute permissions.
```
cp -f /etc/skel/.bashrc /root/.bashrc
```
Then add a line to the end of `/root/.bashrc` that runs your script by specifying the full path to the script.
When the user first logs in, the system runs `.bashrc`, which will automatically run your script. The last line of the script overwrites the root `.bashrc` with the default `.bashrc` from the `/etc/skel` directory so the call to run your script no longer exists. Using this method, your script only runs once the first time the user logs in, but the file remains in the filesystem if they need to re-run or reference it later.
### Recommendations
* **Use the smallest suitable disk size**.
We don't support decreasing the size of a Droplet's disk because it poses data integrity issues. Building your image using the smallest disk size appropriate for your use case lets your users choose from the widest variety of Droplet plans.
* **Do not enable unnecessary DigitalOcean features on your build Droplet**.
By not enabling features like monitoring, IPv6, or private networking when you create your build Droplet, you retain more of your distribution's standard configuration, meaning you'll need to do less cleanup before you create the final image.
* **Install software updates from the distribution's repositories** before creating your final image.
This secures the system and can save your users time when they create new Droplets from your image.
* **Use official package repositories** or well-maintained third-party repositories whenever possible. Packages installed through other means may not provide a mechanism for applying timely security updates.
For official distribution packages, we recommend maintaining the `mirrors.digitalocean.com` mirrors, which are direct mirrors of the distribution's package archive. These mirrors are provided by default and provide faster downloads because the mirrors are stored within our infrastructure.
* **If you need to provide a password to your user, consider configuring it so that it is randomly generated at boot time** and explain to users via your Getting Started instructions how to access the password. Here's an example of how you can generate a high quality, 12 character password on a Linux Droplet, and store it in a file on the Droplet.
```sh
gpg --gen-random --armor 2 12 > /root/.secrets.txt
```
* **Add a message of the day (MOTD)**, which is text displayed when a user logs into their Droplet. We recommend writing an MOTD which introduces your image's features and points users to its documentation.
You can add an MOTD to your image by creating a text file in `/etc/update-motd.d`. Naming the file beginning with `99`, like `99-image-readme`, will display the MOTD as the last text the user sees before the login prompt.
## Contributing
We'd love to have your contribution to this project! [You can find more details here](https://github.com/digitalocean/marketplace-partners/blob/master/.github/CONTRIBUTING.md).
## Caveats
Avoid building architecture specific components into your 1-Click App, as your App may be run by DigitalOcean customers across a variety of operating systems and underlying hypervisors. You should use generic machine architecture to ensure consistent use across all infrastructure.
For example, avoid building ruby gems with native extensions as the underlying machine architecture may use flags that don't exist across all hypervisors.

View File

@@ -0,0 +1,95 @@
# You may add here your
# server {
# ...
# }
# statements for each of your virtual hosts to this file
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# http://wiki.nginx.org/Pitfalls
# http://wiki.nginx.org/QuickStart
# http://wiki.nginx.org/Configuration
#
# Generally, you will want to move this file somewhere, and start with a clean
# file but keep this around for reference. Or just disable in sites-enabled.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
server {
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
root /var/www/html;
index index.php index.html index.htm;
# Make site accessible from http://localhost/
server_name localhost;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
# Uncomment to enable naxsi on this location
# include /etc/nginx/naxsi.rules
}
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;
# root html;
# index index.html index.htm;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}
# HTTPS server
#
#server {
# listen 443;
# server_name localhost;
#
# root html;
# index index.html index.htm;
#
# ssl on;
# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;
#
# ssl_session_timeout 5m;
#
# ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
# ssl_ciphers "HIGH:!aNULL:!MD5 or HIGH:!aNULL:!MD5:!3DES";
# ssl_prefer_server_ciphers on;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}

View File

@@ -0,0 +1,27 @@
#!/bin/sh
#
# Configured as part of the DigitalOcean 1-Click Image build process
myip=$(hostname -I | awk '{print$1}')
cat <<EOF
********************************************************************************
Welcome to DigitalOcean's 1-Click LEMP Droplet.
To keep this Droplet secure, the UFW firewall is enabled.
All ports are BLOCKED except 22 (SSH), 80 (HTTP), and 443 (HTTPS).
In a web browser, you can view:
* The LEMP 1-Click Quickstart guide: https://do.co/2GOFe5J#start
* Your LEMP website: http://$myip
On the server:
* The default web root is located at /var/www/html
* The MySQL root password is saved in /root/.digitalocean_password
* Certbot is preinstalled. Run it to configure HTTPS. See
https://do.co/2GOFe5J#enable-https for more detail.
For help and more information, visit https://do.co/2GOFe5J
********************************************************************************
To delete this message of the day: rm -rf $(readlink -f ${0})
EOF

View File

@@ -0,0 +1,43 @@
#!/bin/bash
#Generate Mysql root password.
root_mysql_pass=$(openssl rand -hex 24)
debian_sys_maint_mysql_pass=$(openssl rand -hex 24)
# Save the passwords
cat > /root/.digitalocean_password <<EOM
root_mysql_pass="${root_mysql_pass}"
EOM
# Configure MySQL root password
mysqladmin -u root -h localhost password ${root_mysql_pass}
mysql -uroot -p${root_mysql_pass} \
-e "ALTER USER 'debian-sys-maint'@'localhost' IDENTIFIED BY '${debian_sys_maint_mysql_pass}'"
cat > /etc/mysql/debian.cnf <<EOM
# Automatically generated for Debian scripts. DO NOT TOUCH!
[client]
host = localhost
user = debian-sys-maint
password = ${debian_sys_maint_mysql_pass}
socket = /var/run/mysqld/mysqld.sock
[mysql_upgrade]
host = localhost
user = debian-sys-maint
password = ${debian_sys_maint_mysql_pass}
socket = /var/run/mysqld/mysqld.sock
EOM
myip=$(hostname -I | awk '{print$1}')
sed -e "s|server_name localhost|servername ${myip}|g" \
-i /etc/nginx/sites-available/00-digitalocean
systemctl restart nginx
# Remove the ssh force logout command
sed -e '/Match User root/d' \
-e '/.*ForceCommand.*droplet.*/d' \
-i /etc/ssh/sshd_config
systemctl restart ssh

View File

@@ -0,0 +1,103 @@
<html>
<head>
<style>
body {
font-family: ProximaNova;
font-size: 15px;
font-style: normal;
font-stretch: normal;
line-height: 1;
letter-spacing: normal;
margin: 0;
}
.button {
border-radius: 3px;
background-color: #0069ff;
color: #ffffff;
display: flex;
flex-direction: column;
height: 48px;
justify-content: center;
text-decoration: none;
width: 148px;
}
.content {
align-items: center;
border: solid 2px #f1f1f1;
border-radius: 3px;
display: flex;
flex-direction: column;
margin: 32px auto;
padding: 32px;
text-align: center;
width: 960px;;
}
.copyright {
color: #99999999;
font-size: 13px;
margin-left: 10px;
}
.description {
color: #676767;
}
.empty-access {
height: 220px;
margin-bottom: -20px;
}
.header {
align-items: center;
display: flex;
margin: 15px;
}
.logo {
height: 30px;
color: #999999;
width: 30px;
}
.title {
font-family: ProximaNova;
font-size: 21px;
font-weight: 600;
color: #444444;
}
</style>
</head>
<body>
<div class="header">
<svg class="logo" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" enable-background="new 0 0 30 30" xml:space="preserve">
<g id="XMLID_17_">
<g id="XMLID_18_">
<g>
<g id="XMLID_225_">
<g id="XMLID_233_">
<path id="XMLID_234_" fill="#0080FF" d="M15,30v-5.8c6.2,0,10.9-6.1,8.6-12.6c-0.9-2.4-2.8-4.3-5.2-5.2
C11.9,4.1,5.8,8.8,5.8,15l0,0L0,15C0,5.2,9.5-2.5,19.8,0.7c4.5,1.4,8.1,5,9.5,9.5C32.5,20.5,24.8,30,15,30z"/>
</g>
<polygon id="XMLID_232_" fill="#0080FF" points="15,24.2 9.2,24.2 9.2,18.4 9.2,18.4 15,18.4 15,18.4"/>
<polygon id="XMLID_228_" fill="#0080FF" points="9.2,28.7 4.8,28.7 4.8,28.7 4.8,24.2 9.2,24.2"/>
<polygon id="XMLID_226_" fill="#0080FF" points="4.8,24.2 1,24.2 1,24.2 1,20.5 1,20.5 4.8,20.5 4.8,20.5"/>
</g>
</g>
</g>
</g>
</svg>
<div class="copyright">&copy; 2018 DigitalOcean, LLC. All rights reserved.</div>
</div>
<div class="content">
<svg id="svg" class="empty-access" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><defs><style>.cls-1,.cls-11,.cls-6,.cls-8{fill:#d7e9ff;}.cls-1{stroke:#d7e9ff;}.cls-1,.cls-10,.cls-13,.cls-14,.cls-15,.cls-16,.cls-17,.cls-18,.cls-2,.cls-3,.cls-4,.cls-6,.cls-7,.cls-9{stroke-linejoin:round;}.cls-1,.cls-13,.cls-14,.cls-2,.cls-3,.cls-6{stroke-width:1.6px;}.cls-1,.cls-5,.cls-7,.cls-8,.cls-9{fill-rule:evenodd;}.cls-12,.cls-2{fill:#8fbeff;}.cls-10,.cls-14,.cls-2,.cls-4,.cls-6,.cls-7{stroke:#8fbeff;}.cls-3{fill:#4894ff;}.cls-13,.cls-3{stroke:#4894ff;}.cls-13,.cls-14,.cls-15,.cls-16,.cls-18,.cls-4,.cls-7,.cls-9{fill:none;}.cls-15,.cls-16,.cls-18,.cls-4,.cls-6,.cls-7,.cls-9{stroke-linecap:round;}.cls-4{stroke-width:6px;}.cls-10,.cls-17,.cls-5{fill:#fff;}.cls-10,.cls-15,.cls-17,.cls-18,.cls-7,.cls-9{stroke-width:2px;}.cls-15,.cls-9{stroke:#0069ff;}.cls-16{stroke:#fff;stroke-width:1.6px;}.cls-17{stroke:#a5d1f7;}.cls-18{stroke:#1f8ced;}</style></defs><path class="cls-1" d="M148.23,190.5a5.31,5.31,0,0,1-5.31-5.31,5.31,5.31,0,0,1-5.31,5.31,5.31,5.31,0,0,1,5.31,5.31A5.31,5.31,0,0,1,148.23,190.5Z"/><path class="cls-1" d="M200.83,166.47a5.31,5.31,0,0,1-5.31-5.31,5.31,5.31,0,0,1-5.31,5.31,5.31,5.31,0,0,1,5.31,5.31A5.31,5.31,0,0,1,200.83,166.47Z"/><circle class="cls-2" cx="187.2" cy="213.07" r="1.11"/><circle class="cls-2" cx="199.83" cy="104.53" r="1.11"/><circle class="cls-3" cx="209.32" cy="188.1" r="1.11"/><circle class="cls-4" cx="166.27" cy="88.39" r="24.99" transform="translate(-0.6 175.63) rotate(-55.6)"/><polygon class="cls-5" points="121.26 188.1 161.43 156.45 176.42 107.55 144.73 85.85 104.56 117.51 89.57 166.4 121.26 188.1"/><rect class="cls-6" x="84.18" y="117.77" width="97.63" height="38.41" transform="translate(-55.16 169.32) rotate(-55.6)"/><polyline class="cls-7" points="161.43 156.45 176.42 107.55 144.73 85.85 104.56 117.51 89.57 166.4 121.26 188.1 153.98 162.32"/><path class="cls-8" d="M193,113a21.74,21.74,0,0,1-7-8.62,21.14,21.14,0,0,0-38.37,0,21.74,21.74,0,0,1-7,8.61,16.4,16.4,0,0,0-6.82,14.78,16,16,0,0,0,5,10.32,9.14,9.14,0,0,1,3,6.62h0a8.84,8.84,0,0,0,8.83,8.84l32.3,0a8.84,8.84,0,0,0,8.84-8.83h0a9.14,9.14,0,0,1,3-6.61,16,16,0,0,0,5-10.32A16.4,16.4,0,0,0,193,113Z"/><path class="cls-9" d="M181.81,98.41a21.14,21.14,0,0,0-34.21,6,21.74,21.74,0,0,1-7,8.61,16.4,16.4,0,0,0-6.82,14.78,16,16,0,0,0,5,10.32,9.14,9.14,0,0,1,3,6.62h0a8.84,8.84,0,0,0,8.83,8.84l32.3,0a8.84,8.84,0,0,0,8.84-8.83h0a9.14,9.14,0,0,1,3-6.61,16,16,0,0,0,5-10.32A16.4,16.4,0,0,0,193,113a20.5,20.5,0,0,1-3.61-3.32"/><circle class="cls-10" cx="166.78" cy="113.3" r="6.55"/><path class="cls-9" d="M166.78,106.75a6.55,6.55,0,0,1,0,13.1"/><path class="cls-5" d="M157.75,153.56l0,60.61a6.43,6.43,0,0,1,6.43,6.43h11.57l0-67h-18Z"/><rect class="cls-11" x="157.75" y="153.56" width="18" height="7.6"/><rect class="cls-11" x="164.16" y="153.56" width="5.14" height="67.04"/><rect class="cls-12" x="164.18" y="153.56" width="5.14" height="7.6"/><rect class="cls-13" x="164.16" y="153.56" width="5.14" height="67.04"/><line class="cls-14" x1="164.18" y1="161.16" x2="164.14" y2="220.6"/><path class="cls-9" d="M175.74,180.6l0-27h-18l0,60.61a6.43,6.43,0,0,1,6.43,6.43h11.57V208.51"/><line class="cls-15" x1="175.73" y1="203.51" x2="175.74" y2="185.6"/><path class="cls-4" d="M166.6,113.38a25,25,0,0,0,13.79-45.61"/><path class="cls-16" d="M166.6,113.38a25,25,0,0,0,13.79-45.61"/><path class="cls-16" d="M176.42,65.55a24.82,24.82,0,0,0-7-2"/><rect class="cls-17" x="152.27" y="130.97" width="29" height="8.5"/><line class="cls-18" x1="181.27" y1="130.97" x2="181.27" y2="139.47"/></svg>
<h1 class="title">Please log into your Droplet with SSH to configure the LEMP installation.</h1>
<p class="description">See the LEMP 1-Click Quickstart guide for detailed assistance.</p>
<a class="button" href="https://do.co/2GOFe5J#start">Quickstart Guide</a>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

View File

@@ -0,0 +1,72 @@
{
"variables": {
"do_token": "{{env `DIGITALOCEAN_TOKEN`}}",
"image_name": "lemp-20-04-snapshot-{{timestamp}}",
"apt_packages": "fail2ban mysql-server nginx php-apcu php-curl php-mysql php-fpm postfix python3-certbot-nginx software-properties-common",
"application_name": "LEMP",
"application_version": ""
},
"sensitive-variables": ["do_token"],
"builders": [
{
"type": "digitalocean",
"api_token": "{{user `do_token`}}",
"image": "ubuntu-20-04-x64",
"region": "nyc3",
"size": "s-1vcpu-1gb",
"ssh_username": "root",
"snapshot_name": "{{user `image_name`}}"
}
],
"provisioners": [
{
"type": "shell",
"inline": [
"cloud-init status --wait"
]
},
{
"type": "file",
"source": "files/etc/",
"destination": "/etc/"
},
{
"type": "file",
"source": "files/var/",
"destination": "/var/"
},
{
"type": "shell",
"environment_vars": [
"DEBIAN_FRONTEND=noninteractive",
"LC_ALL=C",
"LANG=en_US.UTF-8",
"LC_CTYPE=en_US.UTF-8"
],
"inline": [
"apt -qqy update",
"apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' full-upgrade",
"apt -qqy -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' install {{user `apt_packages`}}",
"apt-get -qqy clean"
]
},
{
"type": "shell",
"environment_vars": [
"application_name={{user `application_name`}}",
"application_version={{user `application_version`}}",
"DEBIAN_FRONTEND=noninteractive",
"LC_ALL=C",
"LANG=en_US.UTF-8",
"LC_CTYPE=en_US.UTF-8"
],
"scripts": [
"scripts/01-lemp.sh",
"scripts/02-ufw-nginx.sh",
"scripts/03-force-ssh-logout.sh",
"scripts/90-cleanup.sh",
"scripts/99-img-check.sh"
]
}
]
}

View File

@@ -0,0 +1,14 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
rm -rvf /etc/nginx/sites-enabled/default
ln -s /etc/nginx/sites-available/digitalocean \
/etc/nginx/sites-enabled/digitalocean
rm -rf /var/www/html/index*debian.html
chown -R www-data: /var/www

View File

@@ -0,0 +1,10 @@
#!/bin/sh
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
ufw limit ssh
ufw allow 'Nginx Full'
ufw --force enable

View File

@@ -0,0 +1,10 @@
#!/bin/sh
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
cat >> /etc/ssh/sshd_config <<EOM
Match User root
ForceCommand echo "Please wait while we get your droplet ready..."
EOM

View File

@@ -0,0 +1,49 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
set -o errexit
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
if [[ ! -d /tmp ]]; then
mkdir /tmp
fi
chmod 1777 /tmp
if [ -n "$(command -v yum)" ]; then
yum update -y
yum clean all
elif [ -n "$(command -v apt-get)" ]; then
export DEBIAN_FRONTEND=noninteractive
apt-get -y update
apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes
apt-get -y autoremove
apt-get -y autoclean
fi
rm -rf /tmp/* /var/tmp/*
history -c
cat /dev/null > /root/.bash_history
unset HISTFILE
find /var/log -mtime -1 -type f -exec truncate -s 0 {} \;
rm -rf /var/log/*.gz /var/log/*.[0-9] /var/log/*-????????
rm -rf /var/lib/cloud/instances/*
rm -f /root/.ssh/authorized_keys /etc/ssh/*key*
touch /etc/ssh/revoked_keys
chmod 600 /etc/ssh/revoked_keys
# Securely erase the unused portion of the filesystem
GREEN='\033[0;32m'
NC='\033[0m'
printf "\n${GREEN}Writing zeros to the remaining disk space to securely
erase the unused portion of the file system.
Depending on your disk size this may take several minutes.
The secure erase will complete successfully when you see:${NC}
dd: writing to '/zerofile': No space left on device\n
Beginning secure erase now\n"
dd if=/dev/zero of=/zerofile bs=4096 || rm /zerofile

View File

@@ -0,0 +1,640 @@
#!/bin/bash
# DigitalOcean Marketplace Image Validation Tool
# © 2021-2022 DigitalOcean LLC.
# This code is licensed under Apache 2.0 license (see LICENSE.md for details)
VERSION="v. 1.8.1"
RUNDATE=$( date )
# Script should be run with SUDO
if [ "$EUID" -ne 0 ]
then echo "[Error] - This script must be run with sudo or as the root user."
exit 1
fi
STATUS=0
PASS=0
WARN=0
FAIL=0
# $1 == command to check for
# returns: 0 == true, 1 == false
cmdExists() {
if command -v "$1" > /dev/null 2>&1; then
return 0
else
return 1
fi
}
function getDistro {
if [ -f /etc/os-release ]; then
# freedesktop.org and systemd
# shellcheck disable=SC1091
. /etc/os-release
OS=$NAME
VER=$VERSION_ID
elif type lsb_release >/dev/null 2>&1; then
# linuxbase.org
OS=$(lsb_release -si)
VER=$(lsb_release -sr)
elif [ -f /etc/lsb-release ]; then
# For some versions of Debian/Ubuntu without lsb_release command
# shellcheck disable=SC1091
. /etc/lsb-release
OS=$DISTRIB_ID
VER=$DISTRIB_RELEASE
elif [ -f /etc/debian_version ]; then
# Older Debian/Ubuntu/etc.
OS=Debian
VER=$(cat /etc/debian_version)
elif [ -f /etc/SuSe-release ]; then
# Older SuSE/etc.
:
elif [ -f /etc/redhat-release ]; then
# Older Red Hat, CentOS, etc.
VER=$(cut -d" " -f3 < /etc/redhat-release | cut -d "." -f1)
d=$(cut -d" " -f1 < /etc/redhat-release | cut -d "." -f1)
if [[ $d == "CentOS" ]]; then
OS="CentOS Linux"
fi
else
# Fall back to uname, e.g. "Linux <version>", also works for BSD, etc.
OS=$(uname -s)
VER=$(uname -r)
fi
}
function loadPasswords {
SHADOW=$(cat /etc/shadow)
}
function checkAgent {
# Check for the presence of the DO directory in the filesystem
if [ -d /opt/digitalocean ];then
echo -en "\e[41m[FAIL]\e[0m DigitalOcean directory detected.\n"
((FAIL++))
STATUS=2
if [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; then
echo "To uninstall the agent: 'sudo yum remove droplet-agent'"
echo "To remove the DO directory: 'find /opt/digitalocean/ -type d -empty -delete'"
elif [[ $OS == "Ubuntu" ]] || [[ $OS == "Debian" ]]; then
echo "To uninstall the agent and remove the DO directory: 'sudo apt-get purge droplet-agent'"
fi
else
echo -en "\e[32m[PASS]\e[0m DigitalOcean Monitoring agent was not found\n"
((PASS++))
fi
}
function checkLogs {
cp_ignore="/var/log/cpanel-install.log"
echo -en "\nChecking for log files in /var/log\n\n"
# Check if there are log archives or log files that have not been recently cleared.
for f in /var/log/*-????????; do
[[ -e $f ]] || break
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
done
for f in /var/log/*.[0-9];do
[[ -e $f ]] || break
echo -en "\e[93m[WARN]\e[0m Log archive ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
done
for f in /var/log/*.log; do
[[ -e $f ]] || break
if [[ "${f}" = '/var/log/lfd.log' && "$(grep -E -v '/var/log/messages has been reset| Watching /var/log/messages' "${f}" | wc -c)" -gt 50 ]]; then
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [[ "${f}" != '/var/log/lfd.log' && "$(wc -c < "${f}")" -gt 50 ]]; then
if [ "${f}" != "${cp_ignore}" ]; then
echo -en "\e[93m[WARN]\e[0m un-cleared log file, ${f} found; Contents:\n"
cat "${f}"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
}
function checkTMP {
# Check the /tmp directory to ensure it is empty. Warn on any files found.
return 1
}
function checkRoot {
user="root"
uhome="/root"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account.\n"
((FAIL++))
STATUS=2
fi
fi
done
if [ -d ${uhome}/ ]; then
if [ -d ${uhome}/.ssh/ ]; then
if ls ${uhome}/.ssh/*> /dev/null 2>&1; then
for key in "${uhome}"/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$(wc -c < "${key}")" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory at \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a populated known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
if [ -f /root/.bash_history ];then
BH_S=$(wc -c < /root/.bash_history)
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
return 1;
else
echo -en "\e[32m[PASS]\e[0m The Root User's Bash History is not present\n"
((PASS++))
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
echo -en "\n\n"
return 1
}
function checkUsers {
# Check each user-created account
awk -F: '$3 >= 1000 && $1 != "nobody" {print $1}' < /etc/passwd | while IFS= read -r user;
do
# Skip some other non-user system accounts
if [[ $user == "centos" ]]; then
:
elif [[ $user == "nfsnobody" ]]; then
:
else
echo -en "\nChecking user: ${user}...\n"
for usr in $SHADOW
do
IFS=':' read -r -a u <<< "$usr"
if [[ "${u[0]}" == "${user}" ]]; then
if [[ ${u[1]} == "!" ]] || [[ ${u[1]} == "!!" ]] || [[ ${u[1]} == "*" ]]; then
echo -en "\e[32m[PASS]\e[0m User ${user} has no password set.\n"
# shellcheck disable=SC2030
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m User ${user} has a password set on their account. Only system users are allowed on the image.\n"
# shellcheck disable=SC2030
((FAIL++))
STATUS=2
fi
fi
done
#echo "User Found: ${user}"
uhome="/home/${user}"
if [ -d "${uhome}/" ]; then
if [ -d "${uhome}/.ssh/" ]; then
if ls "${uhome}/.ssh/*"> /dev/null 2>&1; then
for key in "${uhome}"/.ssh/*
do
if [ "${key}" == "${uhome}/.ssh/authorized_keys" ]; then
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a populated authorized_keys file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
fi
elif [ "${key}" == "${uhome}/.ssh/id_rsa" ]; then
if [ "$(wc -c < "${key}")" -gt 0 ]; then
echo -en "\e[41m[FAIL]\e[0m User \e[1m${user}\e[0m has a private key file in \e[93m${key}\e[0m\n"
akey=$(cat "${key}")
echo "File Contents:"
echo "$akey"
echo "--------------"
((FAIL++))
STATUS=2
else
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has empty private key file in \e[93m${key}\e[0m\n"
# shellcheck disable=SC2030
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
elif [ "${key}" != "${uhome}/.ssh/known_hosts" ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a file in their .ssh directory named \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
else
if [ "$(wc -c < "${key}")" -gt 50 ]; then
echo -en "\e[93m[WARN]\e[0m User \e[1m${user}\e[0m has a known_hosts file in \e[93m${key}\e[0m\n"
((WARN++))
if [[ $STATUS != 2 ]]; then
STATUS=1
fi
fi
fi
done
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m has no SSH keys present\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have an .ssh directory\n"
fi
else
echo -en "\e[32m[ OK ]\e[0m User \e[1m${user}\e[0m does not have a directory in /home\n"
fi
# Check for an uncleared .bash_history for this user
if [ -f "${uhome}/.bash_history" ]; then
BH_S=$(wc -c < "${uhome}/.bash_history")
if [[ $BH_S -lt 200 ]]; then
echo -en "\e[32m[PASS]\e[0m ${user}'s Bash History appears to have been cleared\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${user}'s Bash History should be cleared to prevent sensitive information from leaking\n"
((FAIL++))
STATUS=2
fi
echo -en "\n\n"
fi
fi
done
}
function checkFirewall {
if [[ $OS == "Ubuntu" ]]; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
# shellcheck disable=SC2031
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
# shellcheck disable=SC2031
((WARN++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; then
if [ -f /usr/lib/systemd/system/csf.service ]; then
fw="csf"
if [[ $(systemctl status $fw >/dev/null 2>&1) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
elif cmdExists "firewall-cmd"; then
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
fw="firewalld"
if [[ $(systemctl is-active firewalld >/dev/null 2>&1 && echo 1 || echo 0) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
elif [[ "$OS" =~ Debian.* ]]; then
# user could be using a number of different services for managing their firewall
# we will check some of the most common
if cmdExists 'ufw'; then
fw="ufw"
ufwa=$(ufw status |head -1| sed -e "s/^Status:\ //")
if [[ $ufwa == "active" ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
elif cmdExists "firewall-cmd"; then
fw="firewalld"
if [[ $(systemctl is-active --quiet $fw) ]]; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
else
# user could be using vanilla iptables, check if kernel module is loaded
fw="iptables"
if lsmod | grep -q '^ip_tables' 2>/dev/null; then
FW_VER="\e[32m[PASS]\e[0m Firewall service (${fw}) is active\n"
((PASS++))
else
FW_VER="\e[93m[WARN]\e[0m No firewall is configured. Ensure ${fw} is installed and configured\n"
((WARN++))
fi
fi
fi
}
function checkUpdates {
if [[ $OS == "Ubuntu" ]] || [[ "$OS" =~ Debian.* ]]; then
# Ensure /tmp exists and has the proper permissions before
# checking for security updates
# https://github.com/digitalocean/marketplace-partners/issues/94
if [[ ! -d /tmp ]]; then
mkdir /tmp
fi
chmod 1777 /tmp
echo -en "\nUpdating apt package database to check for security updates, this may take a minute...\n\n"
apt-get -y update > /dev/null
uc=$(apt-get --just-print upgrade | grep -i "security" -c)
if [[ $uc -gt 0 ]]; then
update_count=$(( uc / 2 ))
else
update_count=0
fi
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
echo -en
echo -en "Here is a list of the security updates that are not installed:\n"
sleep 2
apt-get --just-print upgrade | grep -i security | awk '{print $2}' | awk '!seen[$0]++'
echo -en
# shellcheck disable=SC2031
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n\n"
((PASS++))
fi
elif [[ $OS == "CentOS Linux" ]] || [[ $OS == "CentOS Stream" ]] || [[ $OS == "Rocky Linux" ]] || [[ $OS == "AlmaLinux" ]]; then
echo -en "\nChecking for available security updates, this may take a minute...\n\n"
update_count=$(yum check-update --security --quiet | wc -l)
if [[ $update_count -gt 0 ]]; then
echo -en "\e[41m[FAIL]\e[0m There are ${update_count} security updates available for this image that have not been installed.\n"
((FAIL++))
STATUS=2
else
echo -en "\e[32m[PASS]\e[0m There are no pending security updates for this image.\n"
((PASS++))
fi
else
echo "Error encountered"
exit 1
fi
return 1;
}
function checkCloudInit {
if hash cloud-init 2>/dev/null; then
CI="\e[32m[PASS]\e[0m Cloud-init is installed.\n"
((PASS++))
else
CI="\e[41m[FAIL]\e[0m No valid verison of cloud-init was found.\n"
((FAIL++))
STATUS=2
fi
return 1
}
function version_gt() { test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1"; }
clear
echo "DigitalOcean Marketplace Image Validation Tool ${VERSION}"
echo "Executed on: ${RUNDATE}"
echo "Checking local system for Marketplace compatibility..."
getDistro
echo -en "\n\e[1mDistribution:\e[0m ${OS}\n"
echo -en "\e[1mVersion:\e[0m ${VER}\n\n"
ost=0
osv=0
if [[ $OS == "Ubuntu" ]]; then
ost=1
if [[ $VER == "24.04" ]] || [[ $VER == "22.10" ]] || [[ $VER == "22.04" ]] || [[ $VER == "20.04" ]] || [[ $VER == "18.04" ]] || [[ $VER == "16.04" ]]; then
osv=1
fi
elif [[ "$OS" =~ Debian.* ]]; then
ost=1
case "$VER" in
9)
osv=1
;;
10)
osv=1
;;
11)
osv=1
;;
12)
osv=1
;;
*)
osv=2
;;
esac
elif [[ $OS == "CentOS Linux" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
elif [[ $VER == "7" ]]; then
osv=1
elif [[ $VER == "6" ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "CentOS Stream" ]]; then
ost=1
if [[ $VER == "8" ]]; then
osv=1
elif [[ $VER == "9" ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "Rocky Linux" ]]; then
ost=1
if [[ $VER =~ 8\. ]] || [[ $VER =~ 9\. ]]; then
osv=1
else
osv=2
fi
elif [[ $OS == "AlmaLinux" ]]; then
ost=1
if [[ "$VERSION" =~ 8.* ]] || [[ "$VERSION" =~ 9.* ]]; then
osv=1
else
osv=2
fi
else
ost=0
fi
if [[ $ost == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Operating System Detected: ${OS}\n"
((PASS++))
else
echo -en "\e[41m[FAIL]\e[0m ${OS} is not a supported Operating System\n"
((FAIL++))
STATUS=2
fi
if [[ $osv == 1 ]]; then
echo -en "\e[32m[PASS]\e[0m Supported Release Detected: ${VER}\n"
((PASS++))
elif [[ $ost == 1 ]]; then
echo -en "\e[41m[FAIL]\e[0m ${OS} ${VER} is not a supported Operating System Version\n"
((FAIL++))
STATUS=2
else
echo "Exiting..."
exit 1
fi
checkCloudInit
echo -en "${CI}"
checkFirewall
echo -en "${FW_VER}"
checkUpdates
loadPasswords
checkLogs
echo -en "\n\nChecking all user-created accounts...\n"
checkUsers
echo -en "\n\nChecking the root account...\n"
checkRoot
checkAgent
# Summary
echo -en "\n\n---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "Scan Complete.\n\e[32mAll Tests Passed!\e[0m\n"
elif [[ $STATUS == 1 ]]; then
echo -en "Scan Complete. \n\e[93mSome non-critical tests failed. Please review these items.\e[0m\e[0m\n"
else
echo -en "Scan Complete. \n\e[41mOne or more tests failed. Please review these items and re-test.\e[0m\n"
fi
echo "---------------------------------------------------------------------------------------------------"
echo -en "\e[1m${PASS} Tests PASSED\e[0m\n"
echo -en "\e[1m${WARN} WARNINGS\e[0m\n"
echo -en "\e[1m${FAIL} Tests FAILED\e[0m\n"
echo -en "---------------------------------------------------------------------------------------------------\n"
if [[ $STATUS == 0 ]]; then
echo -en "We did not detect any issues with this image. Please be sure to manually ensure that all software installed on the base system is functional, secure and properly configured (or facilities for configuration on first-boot have been created).\n\n"
exit 0
elif [[ $STATUS == 1 ]]; then
echo -en "Please review all [WARN] items above and ensure they are intended or resolved. If you do not have a specific requirement, we recommend resolving these items before image submission\n\n"
exit 0
else
echo -en "Some critical tests failed. These items must be resolved and this scan re-run before you submit your image to the DigitalOcean Marketplace.\n\n"
exit 1
fi

View File

@@ -0,0 +1,68 @@
#!/bin/bash
# should be run on clean droplet installation of ubuntu24
# it will:
#
# install openpanel
# delete admin account
# setup script to create new admin on first ssh login
# cleanup the image
# trigger api to convert droplet to iso
# add iso to existing marketplace item
# install latest panel!
bash <(curl -sSL https://openpanel.org) --hostname=demo.openpanel.org
# remove admin accounts
truncate -s 0 /etc/openpanel/openadmin/users.db
# cleanup logs
rm -rf /etc/openpanel/admin/*
rm -rf /etc/openpanel/user/*
rm -rf /root/openpanel_install.log
# do image cleanup
bash scripts/03-force-ssh-logout.sh
bash scripts/03-force-ssh-logout.sh
bash scripts/99-img-check.sh
# get droplet id
droplet_id=$(curl http://169.254.169.254/metadata/v1/id)
echo "DROPLET ID: $droplet_id"
# fun stuff
DO_API_TOKEN="TOKEN_HEREEEEEEEEEEEE"
# create snapshot
snapshot_name="openpanel-$(date +%Y-%m-%d-%H-%M-%S)"
response=$(curl -X POST -H "Authorization: Bearer $DO_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"snapshot", "name":"'$snapshot_name'"}' \
"https://api.digitalocean.com/v2/droplets/$droplet_id/actions")
action_id=$(echo "$response" | jq -r '.action.id')
echo "Action ID: $action_id"
status="in-progress"
while [ "$status" == "in-progress" ]; do
sleep 10
response=$(curl -X GET -H "Authorization: Bearer $DO_API_TOKEN" \
"https://api.digitalocean.com/v2/actions/$action_id")
status=$(echo "$response" | jq -r '.action.status')
echo "Snapshot status: $status"
done
snapshot_id=$(curl -X GET -H "Authorization: Bearer $DO_API_TOKEN" \
"https://api.digitalocean.com/v2/droplets/$droplet_id/snapshots" | jq -r '.snapshots[0].id')
echo "Snapshot ID: $snapshot_id"

View File

@@ -0,0 +1,538 @@
<?php
/**
* Copyright 2022-2024 FOSSBilling
* Copyright 2011-2021 BoxBilling, Inc.
* SPDX-License-Identifier: Apache-2.0.
*
* @copyright FOSSBilling (https://www.fossbilling.org)
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0
*/
use Random\RandomException;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
/**
* OpenPanel API.
*
* @see https://dev.openpanel.co/api/
*/
class Server_Manager_Openpanel extends Server_Manager
{
/**
* Returns the form configuration for the OpenPanel server manager.
*
* @return array the form configuration as an associative array
*/
public static function getForm(): array
{
return [
'label' => 'OpenPanel',
'form' => [
'credentials' => [
'fields' => [
[
'name' => 'username',
'type' => 'text',
'label' => 'Username',
'placeholder' => 'Username used to connect to the server',
'required' => true,
],
[
'name' => 'password',
'type' => 'text',
'label' => 'Password / Login Key',
'placeholder' => 'Password or login key used to connect to the server',
'required' => true,
],
],
],
],
];
}
/**
* Initializes the OpenPanel server manager.
* Checks if the necessary configuration options are set and throws an exception if any are missing.
*
* @throws Server_Exception if any necessary configuration options are missing
*/
public function init(): void
{
if (empty($this->_config['host'])) {
throw new Server_Exception('The ":server_manager" server manager is not fully configured. Please configure the :missing', [':server_manager' => 'OpenPanel', ':missing' => 'hostname'], 2001);
}
if (empty($this->_config['username'])) {
throw new Server_Exception('The ":server_manager" server manager is not fully configured. Please configure the :missing', [':server_manager' => 'OpenPanel', ':missing' => 'username'], 2001);
}
if (empty($this->_config['password']) && empty($this->_config['accesshash'])) {
throw new Server_Exception('The ":server_manager" server manager is not fully configured. Please configure the :missing', [':server_manager' => 'OpenPanel', ':missing' => 'authentication credentials'], 2001);
}
// If port not set, use OpenPanel default.
$this->_config['port'] = empty($this->_config['port']) ? '2087' : $this->_config['port'];
}
function getAuthToken() {
$apiProtocol = $this->_config['secure'] ? 'https://' : 'http://';
$host = $this->_config['host'];
$username = $this->_config['username'];
$password = $this->_config['password'];
$authEndpoint = $apiProtocol . $host . ':' . $this->getPort() . '/api/';
// Prepare cURL request to authenticate
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $authEndpoint,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode(array(
'username' => $username,
'password' => $password
)),
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 $token;
}
function makeApiRequest($endpoint, $data = null, $method = 'GET') {
$apiProtocol = $this->_config['secure'] ? 'https://' : 'http://';
$host = $this->_config['host'];
$baseUrl = $apiProtocol . $host . ':' . $this->getPort() . '/api/';
$url = $baseUrl . $endpoint;
$token = $this->getAuthToken();
if (!$token) {
error_log("Failed to retrieve auth token");
return false;
}
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_POSTFIELDS => $data,
CURLOPT_HTTPHEADER => array(
'Content-Type: application/json',
'Authorization: Bearer ' . $token
),
));
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
/**
* Returns the login URL for a OpenPanel account.
*
* @param Server_Account|null $account The account for which to get the login URL. This parameter is currently not used.
*
* @return string the login URL
*/
public function getLoginUrl(Server_Account $account = null): string
{
$host = $this->_config['host'];
$protocol = $this->_config['secure'] ? 'https://' : 'http://';
return $protocol . $host . ':2083';
}
/**
* Returns the login URL for a OpenAdmin reseller account.
*
* @param Server_Account|null $account The account for which to get the login URL. This parameter is currently not used.
*
* @return string the login URL
*/
public function getResellerLoginUrl(Server_Account $account = null): string
{
$host = $this->_config['host'];
$protocol = $this->_config['secure'] ? 'https://' : 'http://';
$url = $protocol . $this->_config['host'] . ':' . $this->getPort() . '/api/';
return $protocol . $host . ':2087';
}
# OpenAdmin can use custom port
public function getPort(): int|string
{
$port = $this->_config['port'];
if (filter_var($port, FILTER_VALIDATE_INT) !== false && $port >= 0 && $port <= 65535) {
return $this->_config['port'];
} else {
return 2087;
}
}
/**
* Tests the connection to the OpenPanel server.
* Sends a request to the OpenPanel server to get its version.
*
* @return true if the connection was successful
*
* @throws Server_Exception if an error occurs during the request
*/
public function testConnection(): bool
{
$response = $this->makeApiRequest(null);
$response = json_decode($response);
if($response->message === "API is working!") {
return true;
}
throw new Server_Exception('Can\'t connect to the server');
return false;
}
/**
* Generates a username for a new account on the OpenPanel server.
* The username is generated based on the domain name, with some modifications to comply with OpenPanel's username restrictions.
*
* @param string $domain the domain name for which to generate a username
*
* @return string the generated username
*
* @throws RandomException if an error occurs during the generation of a random number
*/
public function generateUsername(string $domain): string
{
$processedDomain = strtolower(preg_replace('/[^A-Za-z0-9]/', '', $domain));
$username = substr($processedDomain, 0, 7) . random_int(0, 9);
// OpenPanel doesn't allow usernames to start with "test", so replace it with a random string if it does (test3456 would then become something like a62f93456).
if (str_starts_with($username, 'test')) {
$username = substr_replace($username, 'a' . bin2hex(random_bytes(2)), 0, 5);
}
return $username;
}
/**
* Synchronizes an account with the OpenPanel server.
* Sends a request to the OpenPanel server to get the account's details and updates the Server_Account object accordingly.
*
* @param Server_Account $account the account to be synchronized
*
* @return Server_Account the updated account
*
* @throws Server_Exception if an error occurs during the request, or if the account does not exist on the OpenPanel server
*/
public function synchronizeAccount(Server_Account $account)
{
return false;
}
/**
* Creates a new account on the OpenPanel server.
* Sends a request to the OpenPanel server to create a new account with the details provided in the Server_Account object.
* If the account is a reseller account, it also sets up the reseller and assigns the appropriate ACL list.
*
* @param Server_Account $account The account to be created. This object should contain all the necessary details for the new account.
*
* @return bool returns true if the account was successfully created, false otherwise
*
* @throws Server_Exception if an error occurs during the request, or if the response from the OpenPanel server indicates an error
*/
public function createAccount(Server_Account $account)
{
$client = $account->getClient();
$package = $account->getPackage();
$this->getLog()->info('Creating account ' . $client->getUsername());
$data = json_encode(array(
"email" => $client->getEmail(),
'username' => $account->getUsername(),
'password' => $account->getPassword(),
"plan_name" => $package->getName()
));
$response = $this->makeApiRequest("users" , $data, 'POST');
$response = json_decode($response);
if (!empty($response->success)) {
return true;
}
throw new Server_Exception('Error when creating ' . $client->getUsername() . ': ' . $response->error);
}
/**
* Suspends an account on the OpenPanel server.
*
* @param Server_Account $account the account to be suspended
*
* @return bool returns true if the account was successfully suspended
*
* @throws Server_Exception if an error occurs during the request
*/
public function suspendAccount(Server_Account $account): bool
{
// Log the suspension
$this->getLog()->info('Suspending account ' . $account->getUsername());
$client = $account->getClient();
$data = json_encode(array("action" => "suspend"));
$response = $this->makeApiRequest("users/" . $account->getUsername() , $data, 'PATCH');
$response = json_decode($response);
if ($response->success == 1 || $response->success == true ) {
return true;
}
throw new Server_Exception('Error when suspending ' . $client->getUsername() . ': ' . json_encode($response));
}
/**
* Unsuspends an account on the OpenPanel server.
*
* @param Server_Account $account the account to be unsuspended
*
* @return bool returns true if the account was successfully unsuspended
*
* @throws Server_Exception if an error occurs during the request
*/
public function unsuspendAccount(Server_Account $account): bool
{
// Log the unsuspension
$this->getLog()->info('Activating account ' . $account->getUsername());
$client = $account->getClient();
$data = json_encode(array("action" => "unsuspend"));
$response = $this->makeApiRequest("users/". $account->getUsername() , $data, 'PATCH');
$response = json_decode($response);
if ($response->success == 1 || $response->success == true ) {
return true;
}
throw new Server_Exception('Failed to unsuspend ' . $client->getUsername() . ': ' . $response->error);
}
/**
* Cancels an account on the OpenPanel server.
*
* @param Server_Account $account the account to be cancelled
*
* @return bool returns true if the account was successfully cancelled
*
* @throws Server_Exception if an error occurs during the request
*/
public function cancelAccount(Server_Account $account): bool
{
// Log the cancellation
$this->getLog()->info('Canceling account ' . $account->getUsername());
$response = $this->makeApiRequest(endpoint: "users/". $account->getUsername(), method: 'DELETE');
$response = json_decode($response);
if ($response->success) {
return true;
}
$client = $account->getClient();
throw new Server_Exception('Failed to canceling ' . $client->getUsername() . ': ' . $response->error);
}
/**
* Changes the package of an account on the OpenPanel server.
*
* @param Server_Account $account the account for which to change the package
* @param Server_Package $package the new package
*
* @return bool returns true if the package was successfully changed
*
* @throws Server_Exception if an error occurs during the request
*/
public function changeAccountPackage(Server_Account $account, Server_Package $package)
{
// Log the package change
$this->getLog()->info('Changing account ' . $account->getUsername() . ' package');
$data = json_encode(array("plan_name" => $package->getName()));
$response = $this->makeApiRequest("users/". $account->getUsername(),$data , 'PUT');
$response = json_decode($response);
if ($response->success) {
return true;
}
$client = $account->getClient();
throw new Server_Exception('Failed to change package for user ' . $client->getUsername() . ' | Error: ' . $response->error);
}
/**
* Changes the password of an account on the OpenPanel server.
*
* @param Server_Account $account the account for which to change the password
* @param string $newPassword the new password
*
* @return bool returns true if the password was successfully changed
*
* @throws Server_Exception if an error occurs during the request
*/
public function changeAccountPassword(Server_Account $account, string $newPassword)
{
// Log the password change
$this->getLog()->info('Changing account ' . $account->getUsername() . ' password');
$data = json_encode(array("password" => $newPassword));
$response = $this->makeApiRequest("users/". $account->getUsername(),$data , 'PATCH');
$response = json_decode($response);
if ($response->success) {
return true;
}
$client = $account->getClient();
throw new Server_Exception('Failed to change package for user ' . $client->getUsername() . ' | Error: ' . $response->error);
}
/**
* Changes the username of an account on the OpenPanel server.
*
* @param Server_Account $account the account for which to change the username
* @param string $newUsername the new username
*
* @return bool returns true if the username was successfully changed
*
* @throws Server_Exception if an error occurs during the request
*/
public function changeAccountUsername(Server_Account $account, string $newUsername): bool
{
throw new Server_Exception('OpenPanel does not supporting changing username');
}
/**
* Changes the domain of an account on the OpenPanel server.
*
* @param Server_Account $account the account for which to change the domain
* @param string $newDomain the new domain
*
* @return bool returns true if the domain was successfully changed
*
* @throws Server_Exception if an error occurs during the request
*/
public function changeAccountDomain(Server_Account $account, string $newDomain): bool
{
throw new Server_Exception('OpenPanel does not supporting account domain');
}
/**
* Changes the IP of an account on the OpenPanel server.
*
* @param Server_Account $account the account for which to change the IP
* @param string $newIp the new IP
*
* @return bool returns true if the IP was successfully changed
*
* @throws Server_Exception if an error occurs during the request
*/
public function changeAccountIp(Server_Account $account, string $newIp): bool
{
throw new Server_Exception('OpenPanel does not supporting change account IP');
}
}

View File

@@ -0,0 +1,41 @@
# FOSSBilling-OpenPanel Server Manager
> [!NOTE]
> Tested with [FOSSBilling](https://github.com/FOSSBilling/FOSSBilling) v0.6.22
>
## Installation
- Download or git clone the OpenPanel.php file to your [FOSSBilling](https://github.com/FOSSBilling/FOSSBilling) installation at the following location: /library/Server/Manager
## Features
#### Server
- ✅ Verify Connection
- ✅ Create account
- ✅ Cancel account
- ✅ Suspend/Unsuspend account
- ✅ Change account package
- ✅ Change account password
#### Website Functions
- ❌ Create Website (This will also create the user in OpenPanel)
- ❌ Change Website Package
- ❌ Suspend/Un-suspend Website
#### User Functions
- ❌ Change User Password
#### Things The Don't Work Due To Lack Of API
- ❌ Changing Account Username
- ❌ Changing Account Domain
- ❌ Synchronizing Accounts
## Important Notes
- This community-maintained package isn't affiliated with [FOSSBilling](https://github.com/FOSSBilling/FOSSBilling). Please report issues here rather than on the [FOSSBilling](https://github.com/FOSSBilling/FOSSBilling) repo.
- Reseller support in the API is limited. It does support creating Reseller accounts, though the API doesn't seem to provide a way to get all the domains/users hosted by the Reseller to suspend/un-suspend them. I'm not really sure if this happens if the reseller's website get suspended.
- For questions, concerns, or issues with this server manager, please open an issue on GitHub.
- If [FOSSBilling](https://github.com/FOSSBilling/FOSSBilling) updates and breaks this server manager, report the issue here, and I'll update it to work with the latest version.

View File

@@ -0,0 +1,2 @@
# openpanel-paymenter.org
OpenPanel account provisioning module for paymenter.org

View File

@@ -0,0 +1,208 @@
<?php
################################################################################
# Name: OpenPanel paymenter.org Module
# Usage: https://openpanel.com/docs/articles/extensions/openpanel-and-paymenter.org/
# Source: https://github.com/stefanpejcic/openpanel-paymenter.org
# Author: Stefan Pejcic
# Created: 09.10.2024
# Last Modified: 09.10.2024
# Company: openpanel.com
# Copyright (c) Stefan Pejcic
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
################################################################################
namespace App\Extensions\Servers\OpenPanel;
use App\Classes\Extensions\Server;
use App\Helpers\ExtensionHelper;
use Illuminate\Support\Facades\Http;
class OpenPanel extends Server
{
public function getMetadata()
{
return [
'display_name' => 'OpenPanel',
'version' => '1.0.0',
'author' => 'Paymenter',
'website' => 'https://paymenter.org',
];
}
public function getConfig()
{
return [
[
'name' => 'host',
'friendlyName' => 'Url to OpenPanel server (with port)',
'type' => 'text',
'required' => true,
],
[
'name' => 'username',
'friendlyName' => 'Username',
'type' => 'text',
'required' => true,
],
[
'name' => 'password',
'friendlyName' => 'Password',
'type' => 'text',
'required' => true,
]
];
}
public function getProductConfig($options)
{
return [
[
'name' => 'packageName',
'friendlyName' => 'Package Name',
'type' => 'text',
'required' => true,
'description' => 'Package Name for the OpenPanel server',
],
];
}
public function getUserConfig()
{
return [
[
'name' => 'domain',
'friendlyName' => 'Domain',
'type' => 'text',
'required' => true,
'description' => 'Domain for the webhost',
],
[
'name' => 'username',
'friendlyName' => 'Username',
'type' => 'text',
'required' => true,
'description' => 'Username to login to the website',
],
[
'name' => 'password',
'friendlyName' => 'Password',
'type' => 'text',
'required' => true,
'description' => 'Password to login to the website',
]
];
}
public function createServer($user, $params, $order, $product, $configurableOptions)
{
list($jwtToken, $error) = $this->getAuthToken();
if (!$jwtToken) {
ExtensionHelper::error('OpenPanel', 'Failed to get authentication token: ' . $error);
return;
}
$createUserEndpoint = $this->getApiProtocol($params["host"]) . $params["host"] . ':2087/api/users';
$response = Http::withToken($jwtToken)
->post($createUserEndpoint, [
'username' => $params['config']['username'],
'password' => $params['config']['password'],
'email' => $user->email,
'plan_name' => $params['packageName'],
]);
if (!$response->successful()) {
ExtensionHelper::error('OpenPanel', 'Failed to create server: ' . $response->body());
}
}
public function suspendServer($user, $params, $order, $product, $configurableOptions)
{
list($jwtToken, $error) = $this->getAuthToken();
if (!$jwtToken) {
ExtensionHelper::error('OpenPanel', 'Failed to get authentication token: ' . $error);
return;
}
$suspendUserEndpoint = $this->getApiProtocol($params["host"]) . $params["host"] . ':2087/api/users/' . $params["config"]["username"] . '/suspend';
$response = Http::withToken($jwtToken)->patch($suspendUserEndpoint);
if (!$response->successful()) {
ExtensionHelper::error('OpenPanel', 'Failed to suspend server: ' . $response->body());
}
}
public function unsuspendServer($user, $params, $order, $product, $configurableOptions)
{
list($jwtToken, $error) = $this->getAuthToken();
if (!$jwtToken) {
ExtensionHelper::error('OpenPanel', 'Failed to get authentication token: ' . $error);
return;
}
$unsuspendUserEndpoint = $this->getApiProtocol($params["host"]) . $params["host"] . ':2087/api/users/' . $params["config"]["username"] . '/unsuspend';
$response = Http::withToken($jwtToken)->patch($unsuspendUserEndpoint);
if (!$response->successful()) {
ExtensionHelper::error('OpenPanel', 'Failed to unsuspend server: ' . $response->body());
}
}
public function terminateServer($user, $params, $order, $product, $configurableOptions)
{
list($jwtToken, $error) = $this->getAuthToken();
if (!$jwtToken) {
ExtensionHelper::error('OpenPanel', 'Failed to get authentication token: ' . $error);
return;
}
$deleteUserEndpoint = $this->getApiProtocol($params["host"]) . $params["host"] . ':2087/api/users/' . $params["config"]["username"];
$response = Http::withToken($jwtToken)->delete($deleteUserEndpoint);
if (!$response->successful()) {
ExtensionHelper::error('OpenPanel', 'Failed to terminate server: ' . $response->body());
}
}
private function getApiProtocol($hostname)
{
return filter_var($hostname, FILTER_VALIDATE_IP) === false ? 'https://' : 'http://';
}
private function getAuthToken()
{
$authEndpoint = $this->getApiProtocol(ExtensionHelper::getConfig('OpenPanel', 'host')) . ExtensionHelper::getConfig('OpenPanel', 'host') . ':2087/api/auth';
$response = Http::post($authEndpoint, [
'username' => ExtensionHelper::getConfig('OpenPanel', 'username'),
'password' => ExtensionHelper::getConfig('OpenPanel', 'password'),
]);
if (!$response->successful()) {
return [false, 'Failed to authenticate: ' . $response->body()];
}
$responseData = $response->json();
return [$responseData['access_token'] ?? false, 'Token not found in response'];
}
}

View File

@@ -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)

View File

@@ -0,0 +1,666 @@
<?php
################################################################################
# Name: OpenPanel WHMCS Module
# Usage: https://openpanel.com/docs/articles/extensions/openpanel-and-whmcs/
# Source: https://github.com/stefanpejcic/openpanel-whmcs-module
# Author: Stefan Pejcic
# Created: 01.05.2024
# Last Modified: 09.10.2024
# Company: openpanel.com
# Copyright (c) Stefan Pejcic
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
################################################################################
if (!defined("WHMCS")) {
die("This file cannot be accessed directly");
}
############### CORE STUFF ##################
# BASIC AUTH, SHOULD BE REUSED IN ALL ROUTES
function getApiProtocol($hostname) {
return filter_var($hostname, FILTER_VALIDATE_IP) === false ? 'https://' : 'http://';
}
function getAuthToken($params) {
$apiProtocol = getApiProtocol($params["serverhostname"]);
$authEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/';
// Prepare cURL request to authenticate
$curl = curl_init();
curl_setopt_array($curl, array(
CURLOPT_URL => $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) {
return $error; // Return the error message as a plain string
}
try {
$apiProtocol = getApiProtocol($params["serverhostname"]);
$changePlanEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/api/users/' . $params["username"];
$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 changing plan
$planData = array('plan_name' => $packageName);
// Make API request to change plan
$response = apiRequest($changePlanEndpoint, $jwtToken, $planData, 'PUT');
// Log the API request and response
logModuleCall(
'openpanel',
'ChangePackage',
$planData,
$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 package change.';
}
} catch (Exception $e) {
// Log the exception
logModuleCall(
'openpanel',
'ChangePackage Exception',
$params,
$e->getMessage(),
$e->getTraceAsString()
);
// Return the exception message
return 'Error: ' . $e->getMessage();
}
}
############### AUTOLOGIN LINKS ##############
# LOGIN FOR USERS ON FRONT
function openpanel_ClientArea($params) {
list($jwtToken, $error) = getAuthToken($params);
if (!$jwtToken) {
return '<p>Error: ' . $error . '</p>';
}
$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 = '<script>
function loginOpenPanelButton() {
var openpanel_btn = document.getElementById("loginLink");
openpanel_btn.textContent = "Logging in...";
document.getElementById("refreshMessage").style.display = "block";
}
</script>';
$code .= '<a id="loginLink" class="btn btn-primary" href="' . $response["link"] . '" target="_blank" onclick="loginOpenPanelButton()">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 213.000000 215.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,215.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none"><path d="M990 2071 c-39 -13 -141 -66 -248 -129 -53 -32 -176 -103 -272 -158 -206 -117 -276 -177 -306 -264 -17 -50 -19 -88 -19 -460 0 -476 0 -474 94 -568 55 -56 124 -98 604 -369 169 -95 256 -104 384 -37 104 54 532 303 608 353 76 50 126 113 147 184 8 30 12 160 12 447 0 395 -1 406 -22 461 -34 85 -98 138 -317 264 -104 59 -237 136 -295 170 -153 90 -194 107 -275 111 -38 2 -81 0 -95 -5z m205 -561 c66 -38 166 -95 223 -127 l102 -58 0 -262 c0 -262 0 -263 -22 -276 -13 -8 -52 -31 -88 -51 -36 -21 -126 -72 -200 -115 l-135 -78 -3 261 -3 261 -166 95 c-91 52 -190 109 -219 125 -30 17 -52 34 -51 39 3 9 424 256 437 255 3 0 59 -31 125 -69z"></path></g></svg> &nbsp; Login to OpenPanel
</a>';
$code .= '<p id="refreshMessage" style="display: none;">One-time login link has already been used, please refresh the page to login again.</p>';
} else {
$code = '<p>Error: Unable to generate login link for OpenPanel. Please try again later.</p>';
if (isset($response["message"])) {
$code .= '<p>Server Response: ' . htmlentities($response["message"]) . '</p>';
}
}
return $code;
}
# LOGIN FROM admin/configservers.php
function openpanel_AdminLink($params) {
$apiProtocol = getApiProtocol($params["serverhostname"]);
$adminLoginEndpoint = $apiProtocol . $params["serverhostname"] . ':2087/login';
$code = '<form action="' . $adminLoginEndpoint . '" method="post" target="_blank">
<input type="hidden" name="username" value="' . $params["serverusername"] . '" />
<input type="hidden" name="password" value="' . $params["serverpassword"] . '" />
<input type="submit" value="Login to OpenAdmin" />
</form>';
return $code;
}
# LOGIN FOR ADMINS FROM BACKEND
function openpanel_LoginLink($params) {
list($jwtToken, $error) = getAuthToken($params);
if (!$jwtToken) {
return '<p>Error: ' . $error . '</p>';
}
$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 = '<script>
function loginOpenPanelButton() {
var openpanel_btn = document.getElementById("loginLink");
openpanel_btn.textContent = "Logging in...";
document.getElementById("refreshMessage").style.display = "block";
}
</script>';
$code .= '<a id="loginLink" class="btn btn-primary" href="' . $response["link"] . '" target="_blank" onclick="loginOpenPanelButton()">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 213.000000 215.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,215.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none"><path d="M990 2071 c-39 -13 -141 -66 -248 -129 -53 -32 -176 -103 -272 -158 -206 -117 -276 -177 -306 -264 -17 -50 -19 -88 -19 -460 0 -476 0 -474 94 -568 55 -56 124 -98 604 -369 169 -95 256 -104 384 -37 104 54 532 303 608 353 76 50 126 113 147 184 8 30 12 160 12 447 0 395 -1 406 -22 461 -34 85 -98 138 -317 264 -104 59 -237 136 -295 170 -153 90 -194 107 -275 111 -38 2 -81 0 -95 -5z m205 -561 c66 -38 166 -95 223 -127 l102 -58 0 -262 c0 -262 0 -263 -22 -276 -13 -8 -52 -31 -88 -51 -36 -21 -126 -72 -200 -115 l-135 -78 -3 261 -3 261 -166 95 c-91 52 -190 109 -219 125 -30 17 -52 34 -51 39 3 9 424 256 437 255 3 0 59 -31 125 -69z"></path></g></svg> &nbsp; Login to OpenPanel
</a>';
$code .= '<p id="refreshMessage" style="display: none;">One-time login link has already been used, please refresh the page to login again.</p>';
} else {
// Log or print the response in case of error
$code = '<p>Error: Unable to generate the login link. Please try again later.</p>';
if (isset($response["message"])) {
$code .= '<p>Server Response: ' . htmlentities($response["message"]) . '</p>';
}
}
return $code;
}
############### 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;
}
?>

View File

@@ -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\/"
}
]
}

23
translations/README.md Normal file
View File

@@ -0,0 +1,23 @@
### Available Translations
- [🇪🇸](https://community.openpanel.co/d/59-hablas-espanol-spanish-translation-for-openpanel)
- [🇫🇷](https://community.openpanel.co/d/45-parles-tu-francais-french-translation-for-openpanel)
- [🇩🇪](https://community.openpanel.co/d/25-sprichst-du-deutsch-german-translation-for-openpanel)
- [🇹🇷](https://community.openpanel.co/d/31-turkce-konusuyor-musun-turkish-translation-for-openpanel)
-----
## OpenPanel Translations
OpenPanel needs you! If you would like to support OpenPanel, help us with the translation. 💖
### How can you provide a translation for OpenPanel?
1. Fork [this repository](https://github.com/stefanpejcic/openpanel-translations/).
2. Copy `en_us` to your locale e.g. `es_es`
3. Translate the messages.pot file
4. Send a pull request

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@

Binary file not shown.

File diff suppressed because it is too large Load Diff

81
translations/install.sh Normal file
View File

@@ -0,0 +1,81 @@
#!/bin/bash
###
# This script will help you install any desired locale
#
# Usage:
#
# Installing single locale: bash <(curl -sSL https://raw.githubusercontent.com/stefanpejcic/openpanel-translations/main/install.sh) sr-sr
#
# Installing multiple locales at once: bash <(curl -sSL https://raw.githubusercontent.com/stefanpejcic/openpanel-translations/main/install.sh) sr-sr tr-tr
#
###
# might change in future
github_repo="stefanpejcic/openpanel-translations"
# locales dir since OpenPanel v.0.2.1
babel_translations="/etc/openpanel/openpanel/translations"
# at least 1 locale is needed
if [ "$#" -lt 1 ]; then
if ! command -v jq &> /dev/null; then
echo "jq command is required to parse JSON responses. Please install jq to use this feature."
exit 1
fi
echo "Please provide at least one locale to the command, or a list"
echo ""
# list available locales from github repo
echo "Available locales:"
locales=$(curl -s "https://api.github.com/repos/$github_repo/contents" | jq -r '.[] | select(.type == "dir") | .name')
echo "$locales"
echo ""
echo "Example for a single locale (DE): opencli locale de-de"
echo ""
echo "Example for multiple locales (DE & ES): opencli locale de-de es-es"
echo ""
exit 0
fi
cd /usr/local/panel
validate_locale() {
# validate format (LL-LL or ll-ll)
if [[ "$1" =~ ^[a-z]{2}-[a-z]{2}$ ]] || [[ "$1" =~ ^[A-Z]{2}-[A-Z]{2}$ ]]; then
return 0 # ok
else
return 1 # not ok
fi
}
# Loop through each provided locale
for locale in "$@"
do
# must be lowercase
formatted_locale=$(echo "$locale" | tr '[:upper:]' '[:lower:]')
if validate_locale "$formatted_locale"; then
# babel supports just 2 letters
two_letter=$(echo "$locale" | cut -d'-' -f1 | tr '[:upper:]' '[:lower:]')
echo "Creating directory for $formatted_locale locale.."
echo ""
mkdir -p $babel_translations/"$two_letter"/LC_MESSAGES/ &>/dev/null
echo "Downloading $formatted_locale locale from https://raw.githubusercontent.com/$github_repo/main/$formatted_locale/messages.pot"
wget -O $babel_translations/"$two_letter"/LC_MESSAGES/messages.pot "https://raw.githubusercontent.com/$github_repo/main/$formatted_locale/messages.pot" &>/dev/null
pybabel init -i $babel_translations/$two_letter/LC_MESSAGES/messages.pot -d $babel_translations -l "$two_letter" &>/dev/null
echo ""
else
echo "Invalid locale format: $locale. Skipping."
fi
done
# Do this only once
echo "Compiling .mo files for all available locales in $babel_translations directory.."
pybabel compile -f -d $babel_translations &>/dev/null
echo "Restarting OpenPanel to apply translations.."
docker restart openpanel &>/dev/null
echo "DONE"

Binary file not shown.

File diff suppressed because it is too large Load Diff