#!/bin/bash # Convert the supplied JetBackup 5 backup file to a cPanel-compatible backup. # # Originally created by TheLazyAdmin, https://thelazyadmin.blog # # ** USE AT YOUR OWN RISK ** # MIT License # # Copyright (c) 2022 TheLazyAdmin # Copyright (c) 2023,2024 Christopher Handley # # 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. function Error { echo "" echo "$1" exit 1 } function ErrorHelp { echo "" echo "$1" echo " How to use: jb5_to_cpanel_convertor.sh JETBACKUP5_BACKUP [DESTINATION_FOLDER] JETBACKUP5_BACKUP = Source JetBackup file DESTINATION_FOLDER = Optional destination folder for cPanel backup, defaults to /home/, if you are root, otherwise to ~/ e.g. jb5_to_cpanel_convertor.sh /home/download_jb5user_1663238955_28117.tar.gz " exit 1 } function Untar() { BackupPath="$1" DestPath="$2" tar -xf "$BackupPath" -C "$DestPath" Err=$? [[ $Err -gt 0 ]] && Error "Unable to untar the file '$BackupPath'" } function Extract() { FilePath="$1" gunzip $FilePath # FilePath not quoted so can expand wildcard Err=$? [[ $Err -gt 0 ]] && Error "Unable to extract files" } function MoveDir() { Src="$1" Dst="$2" echo "Converting folder '$Src'" # Fixed an issue where the Dst directory did not exist, causing problems with MySQL DB/DB user conversion mkdir -p "$Dst" mv $Src "$Dst" # Src not quoted so can expand wildcard Err=$? [[ $Err -gt 0 ]] && Error "An error occurred" } function Archive() { TarName="$1" echo "Creating archive '$DestDir/$TarName'" if [ -f "$DestDir/$TarName" ]; then rm "$DestDir/$TarName"; fi # Ensure create a new archive from scratch cd "$UnzipDest" || Error "Failed to CD to '$UnzipDest'" tar -czf "$DestDir/$TarName" "cpmove-$AccountName" >/dev/null 2>&1 Err=$? [[ $Err != 0 ]] && Error "Unable to create tar file" } function CreateFTPaccount() { DirPath="$1" ConfigPath="$2" HomeDir="$( cat "$ConfigPath/meta/homedir_paths" )" User="$( ls "$ConfigPath/cp/")" for FILE in "$DirPath"/*.acct; do [ -f "$FILE" ] || continue Username="$(grep -Po '(?<=name: )(\w\D+)' "$FILE")" Password="$(grep -Po '(?<=password: )([A-Za-z0-9!@#$%^&*,()\/\\.])+' "$FILE")" WebRootPath="$(grep -Po '(?<=path: )([A-Za-z0-9\/_.-]+)' "$FILE")" echo "Creating FTP account '$Username'"; printf "%s:%s:0:0:%s:%s/%s:/bin/ftpsh" "$Username" "$Password" "$User" "$HomeDir" "$WebRootPath" >> "$CPanelDir/proftpdpasswd" done } function CreateMySQLfile() { DirPath="$1" SQL_FilePath="$2" for FILE in "$DirPath"/*.user; do [ -f "$FILE" ] || continue Username="$(grep -Po '(?<=name: )([a-zA-Z0-9!@#$%^&*(\)\_\.-]+)' "$FILE")" Database="$(grep -Po '(?<=database `)([_a-zA-Z0-9]+)' "$FILE")" User="$(grep -Po '(?<=name: )([a-zA-Z0-9!#$%^&*(\)\_\.]+)' "$FILE")" Domain="$(echo "$Username" | grep -Po '(?<=@)(.*)$')" Password="$(grep -Po '(?<=password: )([a-zA-Z0-9*]+)' "$FILE")" Permissions="$(grep -Po '(?<=:)[A-Z ,]+$' "$FILE")" echo "Creating DB '$Database' & adding DB user '$User'" echo "GRANT USAGE ON *.* TO '$User'@'$Domain' IDENTIFIED BY PASSWORD '$Password';" >> "$SQL_FilePath" echo "GRANT$Permissions ON \`$Database\`.* TO '$User'@'$Domain';" >> "$SQL_FilePath" done } function CreateEmailAccount() { BackupEmailPath="$1" DestEmailPath="$2" echo "Creating email accounts" for JsonFile in "$BackupEmailPath"/*.conf; do [ -f "$JsonFile" ] || continue MailUser="$(jq -r '.account' "$JsonFile" | base64 --decode)" MailDomain="$(jq -r '.domain' "$JsonFile" | base64 --decode)" MailPassword="$(jq -r '.password' "$JsonFile" | base64 --decode)" if [ -z "$MailUser" ] || [ -z "$MailDomain" ]; then echo " Skipping email account in '$(basename "$JsonFile")' (missing account or domain)" continue fi # Ensure the per-domain mail dir exists before appending credentials, # otherwise the redirect below fails silently and the password is lost mkdir -p "$DestEmailPath/${MailDomain}" echo "${MailUser}:${MailPassword}:::::::" >>"$DestEmailPath/${MailDomain}/shadow" done } function CreateDomains() { DirPath="$1" ConfigPath="$2" echo "Creating domains" # find primary domain PrimaryDomain="" for JSON_FILE in "$DirPath"/*.conf; do Domain="$(jq -r '.domain' "$JSON_FILE" | base64 --decode)" Type="$(jq -r '.type' "$JSON_FILE" | base64 --decode)" if [ "$Type" = "1" ]; then PrimaryDomain="$Domain" fi done if [ -z "$PrimaryDomain" ]; then Error "Failed to find the primary domain of account '$AccountName'"; fi # OR COULD JUST DO: PrimaryDomain="$( cat "$CPanelDir/cp/$AccountName" | grep -Po '(?<=DNS=)([A-Za-z0-9-.]+)')" # write info about sub-domains of the primary domain echo -n "" >"$ConfigPath"/sds echo -n "" >"$ConfigPath"/sds2 for JSON_FILE in "$DirPath"/*.conf; do #WebRoot="$(jq -r '.public_dir' "$JSON_FILE" | base64 --decode)" Domain="$(jq -r '.domain' "$JSON_FILE" | base64 --decode)" Type="$(jq -r '.type' "$JSON_FILE" | base64 --decode)" if [[ "$Type" == "3" && "$Domain" == *.$PrimaryDomain ]]; then # (sub-domain) echo " Adding sub-domain '$Domain'" echo "${Domain/./_}" >>"$ConfigPath"/sds echo "${Domain/./_}=$Domain" >>"$ConfigPath"/sds2 fi done # write info about addon & parked domains echo -n "" >"$ConfigPath"/addons echo -n "" >"$ConfigPath"/pds for JSON_FILE in "$DirPath"/*.conf; do #WebRoot="$(jq -r '.public_dir' "$JSON_FILE" | base64 --decode)" Domain="$(jq -r '.domain' "$JSON_FILE" | base64 --decode)" Type="$(jq -r '.type' "$JSON_FILE" | base64 --decode)" if [ "$Type" = "1" ]; then # (primary domain) so ignore it : elif [ "$Type" = "2" ]; then # (addon domain) echo " Adding addon domain '$Domain'" echo "$Domain=${Domain/./_}.$PrimaryDomain" >>"$ConfigPath"/addons echo "${Domain/./_}.$PrimaryDomain" >>"$ConfigPath"/sds echo "${Domain/./_}.$PrimaryDomain=$Domain" >>"$ConfigPath"/sds2 elif [ "$Type" = "3" ]; then # (sub-domain) so ignore for the moment : elif [ "$Type" = "4" ]; then # (parked/alias domain) echo "$Domain" >>"$ConfigPath"/pds else # (unknown domain type) Error "Domain '$Domain' has unknown type '$Type'" fi done # write info about sub-domains that are NOT of the primary domain for JSON_FILE in "$DirPath"/*.conf; do #WebRoot="$(jq -r '.public_dir' "$JSON_FILE" | base64 --decode)" Domain="$(jq -r '.domain' "$JSON_FILE" | base64 --decode)" Type="$(jq -r '.type' "$JSON_FILE" | base64 --decode)" if [[ "$Type" == "3" && ! "$Domain" == *.$PrimaryDomain ]]; then # (sub-domain) echo " Adding sub-domain '$Domain'" echo "${Domain/./_}" >>"$ConfigPath"/sds echo "${Domain/./_}=$Domain" >>"$ConfigPath"/sds2 fi done } function CreateSSLcerts() { DirPath="$1" ConfigPath="$2" echo "Creating SSL certificates" # indicates the account will use WHM’s SSL Storage Manager feature (WHM » Home » SSL/TLS » SSL Storage Manager) # And without this, restoring the account will fail to install the SSL certificate, reporting "An error prevented adding a record of type “crt” ... That certificate is already installed as ..." touch "$ConfigPath"/has_sslstorage # Rebuild apache_tls/ so the restore actually installs the cert on each # vhost. We derive everything from the cert/key files themselves (via openssl) # rather than from ssl.db, because ssl.db only records a 'modulus' for RSA keys -- # EC certs (and wildcard certs) cannot be matched the old modulus/commonName way. SSLDir="$ConfigPath/homedir/ssl" if ! compgen -G "$SSLDir/certs/*.crt" >/dev/null 2>&1; then echo " No SSL certificates in backup; nothing to install on vhosts" return fi if ! command -v openssl >/dev/null 2>&1; then echo " WARNING: openssl not found; skipping vhost SSL install (SSL store still carried over)" return fi mkdir -p "$ConfigPath/apache_tls" # Index private keys by their public-key fingerprint, so a key can be paired to a # cert by public key alone -- works for both RSA and EC. declare -A KeyByPub for K in "$SSLDir/keys/"*.key; do [ -f "$K" ] || continue PH="$(openssl pkey -in "$K" -pubout -outform DER 2>/dev/null | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')" [ -n "$PH" ] && KeyByPub["$PH"]="$K" done # _ssl_covers CERT_DOMAIN ACCOUNT_DOMAIN -> true if the (possibly *.wildcard) # cert domain covers the account domain (wildcard matches exactly one label). _ssl_covers() { local D="$1" X="$2" Suffix Head [ "$D" = "$X" ] && return 0 if [[ "$D" == \*.* ]]; then Suffix="${D#\*.}"; Head="${X%.$Suffix}" [ "$Head.$Suffix" = "$X" ] && [ "$Head" != "$X" ] && [[ "$Head" != *.* ]] && return 0 fi return 1 } for JSON_FILE in "$DirPath"/*.conf; do [ -f "$JSON_FILE" ] || continue Domain="$(jq -r '.domain' "$JSON_FILE" | base64 --decode)" [ -z "$Domain" ] && continue # Choose the newest cert that covers this domain AND has a matching key. BestCert=""; BestKey=""; BestStart=-1 for C in "$SSLDir/certs/"*.crt; do [ -f "$C" ] || continue CertDomains="$( { openssl x509 -in "$C" -noout -ext subjectAltName 2>/dev/null | grep -oE 'DNS:[^,]+' | sed 's/DNS://; s/ //g'; openssl x509 -in "$C" -noout -subject -nameopt multiline 2>/dev/null | sed -n 's/^ *commonName *= *//p'; } )" Covered=0 for D in $CertDomains; do _ssl_covers "$D" "$Domain" && { Covered=1; break; }; done [ "$Covered" -eq 1 ] || continue # require the private key to be present CPH="$(openssl x509 -in "$C" -noout -pubkey 2>/dev/null | openssl pkey -pubin -outform DER 2>/dev/null | openssl dgst -sha256 2>/dev/null | awk '{print $NF}')" MatchKey="${KeyByPub[$CPH]}" [ -n "$MatchKey" ] || continue Start="$(openssl x509 -in "$C" -noout -startdate 2>/dev/null | sed 's/notBefore=//')" StartEpoch="$(date -d "$Start" +%s 2>/dev/null || echo 0)" if [ "$StartEpoch" -ge "$BestStart" ]; then BestStart="$StartEpoch"; BestCert="$C"; BestKey="$MatchKey"; fi done [ -n "$BestCert" ] || continue echo " Installing SSL cert for '$Domain'" # cPanel "combined" format: certificate then key. (CA bundle is omitted -- # self-signed certs have none, and cPanel can fetch a CA's bundle itself.) { cat "$BestCert"; echo ""; cat "$BestKey"; echo ""; } >"$ConfigPath/apache_tls/$Domain" done unset -f _ssl_covers } function CreateDNSZones() { DirPath="$1" ConfigPath="$2" echo "Creating DNS zones" # Fix an issue where "dnszones" directory doesn't exist, preventing copy/move. echo "Ensuring $ConfigPath DNS Zone Directory is made" mkdir -p "$ConfigPath/dnszones" for FILE in "$DirPath"/*.zone; do FileName="${FILE##*/}" DstFile="$ConfigPath/dnszones/${FileName%.*}.db" echo "ZoneFile Re-name Step - Creating '$DstFile'" mv -v "$FILE" "$DstFile" Err=$? [[ $Err -gt 0 ]] && Error "An error occurred" done } # Parse arguments FilePath="$1" DestDir="$2" # Sanity check ! [[ -f "$FilePath" ]] && ErrorHelp "Invalid file provided" [[ "$DestDir" == "/" ]] && ErrorHelp "Error :: Don't use root folder as destination" # Default arguments if [ -z "$DestDir" ]; then if [ "$(whoami)" == "root" ]; then DestDir=/home else DestDir=~ fi fi # Extract username #AccountName=$(echo "$FilePath" | grep -oP '(?<=download_)([^_]+)') AccountName="$(echo "${FilePath##*/}" | cut -d_ -f2)" [[ -z "$AccountName" ]] && ErrorHelp "Could not determine the account name from '${FilePath##*/}'. Expected a JetBackup file like download___.tar.gz" if [ $(( $(du --block-size=1 "$FilePath" | cut -f1)*10 )) -lt $(df --block-size=1 --output=avail /tmp | tail -n1) ]; then # (Free space of /tmp is more than 10 times the compressed archive size) so should be safe to decompress there TmpDir=/tmp elif [ $(( $(du --block-size=1 "$FilePath" | cut -f1)*2 )) -gt $(df --block-size=1 --output=avail /tmp | tail -n1) ]; then # (Free space of /tmp is less than 2 times the compressed archive size) so definitely NOT enough space TmpDir=~ else echo "/tmp may not be big enough so checking the decompressed size of the archive..." if [ $(( $(time zcat "$FilePath" | wc -c)*2 )) -lt $(df --block-size=1 --output=avail /tmp | tail -n1) ]; then # (Free space of /tmp is more than 2 times the UNcompressed archive size) so definitely safe to decompress there TmpDir=/tmp else TmpDir=~ fi fi UnzipDest="$(mktemp --directory --tmpdir=$TmpDir "tmp_jb5_$AccountName.XXXXXXXX")" BackupPath="$FilePath" echo "Found backup path '$BackupPath'" echo "Found account '$AccountName'" echo "Creating temporary folder '$UnzipDest'" mkdir -p "$UnzipDest" || ErrorHelp "Destination directory error" ! [[ -d "$UnzipDest" ]] && ErrorHelp "Destination directory error" # Ensure we always clean-up the temporary dir on exit (success, error, or Ctrl-C). # The finished cPanel archive is written to "$DestDir", outside "$UnzipDest", so this is safe. trap 'rm -rf "$UnzipDest"' EXIT echo "Untaring '$BackupPath' into '$UnzipDest'" Untar "$BackupPath" "$UnzipDest" ! [[ -d "$UnzipDest/backup" ]] && Error "JetBackup5 backup directory '$UnzipDest/backup' not found" CPanelDir="$UnzipDest/cpmove-$AccountName" JB5Backup="$UnzipDest/backup" echo "Converting account '$AccountName'" echo "Working folder '$CPanelDir'" if ! [[ -d "$JB5Backup/config" ]]; then ErrorHelp "The backup does not contain the config directory" else MoveDir "$JB5Backup/config" "$CPanelDir/" # Extract any nested tar.gz archives in config/ (contents are flat, belong in cpmove root) for TGZ in "$CPanelDir/config/"*.tar.gz; do [ -f "$TGZ" ] || continue echo "Extracting nested config archive '$(basename "$TGZ")'" tar -xzf "$TGZ" -C "$CPanelDir/" # tar -xzf "$TGZ" -C "$UnzipDest/" Err=$? [[ $Err -gt 0 ]] && Error "Unable to extract '$TGZ'" rm "$TGZ" done echo "Copying config files to working folder" cp -rn $CPanelDir/config/* "$CPanelDir" # Remove config/ if it's now empty rmdir "$CPanelDir/config" 2>/dev/null fi if [[ -d "$JB5Backup/homedir" ]]; then if ! [[ -d "$CPanelDir/homedir" ]]; then MoveDir "$JB5Backup/homedir" "$CPanelDir" else rsync -ar "$JB5Backup/homedir" "$CPanelDir" fi # Extract any nested tar.gz archives in homedir/ (contents are flat home dir files) for TGZ in "$CPanelDir/homedir/"*.tar.gz; do [ -f "$TGZ" ] || continue echo "Extracting nested homedir archive '$(basename "$TGZ")'" tar -xzf "$TGZ" -C "$CPanelDir/homedir/" Err=$? [[ $Err -gt 0 ]] && Error "Unable to extract '$TGZ'" rm "$TGZ" done fi if [[ -d "$JB5Backup/database" ]] ; then echo "Converting databases" mkdir -p "$CPanelDir/mysql" # JB5 mixes MySQL dumps (gzipped *.sql.gz) and PostgreSQL dumps (pg_dump # custom format, misleadingly named *.tar). Detect by magic bytes rather # than by file name, since the file names are not trustworthy. for DB in "$JB5Backup/database/"*; do [ -f "$DB" ] || continue Name="${DB##*/}" if [ "$(head -c 5 "$DB")" = "PGDMP" ]; then # PostgreSQL custom-format dump -> convert to plain SQL in psql/ command -v pg_restore >/dev/null 2>&1 || Error "pg_restore not found; cannot convert PostgreSQL dump '$Name'" mkdir -p "$CPanelDir/psql" DbName="${Name%.tar}" echo "Converting PostgreSQL database '$DbName'" pg_restore -f "$CPanelDir/psql/$DbName.sql" "$DB" || Error "pg_restore failed for PostgreSQL database '$DbName'" else # MySQL dump (gzip or already-plain SQL) -> mysql/ mv "$DB" "$CPanelDir/mysql/" fi done # Decompress any gzipped MySQL dumps; leave already-plain .sql files untouched for GZ in "$CPanelDir/mysql/"*.gz; do [ -f "$GZ" ] || continue gunzip "$GZ" || Error "Unable to extract database file '$GZ'" done fi [[ -d "$JB5Backup/database_user" ]] && CreateMySQLfile "$JB5Backup/database_user" "$CPanelDir/mysql.sql" if [[ -d "$JB5Backup/email" ]]; then MoveDir "$JB5Backup/email" "$CPanelDir/homedir/mail" [[ -d "$JB5Backup/jetbackup.configs/email" ]] && CreateEmailAccount "$JB5Backup/jetbackup.configs/email" "$CPanelDir/homedir/etc" fi [[ -d "$JB5Backup/ftp" ]] && CreateFTPaccount "$JB5Backup/ftp" "$CPanelDir" if [[ -d "$JB5Backup/jetbackup.configs/domain" ]]; then CreateDomains "$JB5Backup/jetbackup.configs/domain" "$CPanelDir" CreateSSLcerts "$JB5Backup/jetbackup.configs/domain" "$CPanelDir" fi if [[ -d "$JB5Backup/domain" ]]; then CreateDNSZones "$JB5Backup/domain" "$CPanelDir" fi echo "Creating final cPanel backup archive..."; Archive "cpmove-$AccountName.tar.gz" echo "Converting Done!" echo "Temporary working folder '$UnzipDest' will be removed automatically." echo -e "Your cPanel backup:\n$DestDir/cpmove-$AccountName.tar.gz"