diff --git a/include/class-FTP.php b/include/class-FTP.php index 52402a7..7fb31f5 100644 --- a/include/class-FTP.php +++ b/include/class-FTP.php @@ -1,106 +1,148 @@ . // load dependent traits class_exists( 'Domain' ); /** * Methods for an FTP class */ trait FTPTrait { use DomainTrait; /** * Get the FTP login username * * @return string E-mail */ public function getFTPLogin() { return $this->get( 'ftp_login' ); } /** * Check if this FTP account is active * * @return bool */ public function isFTPActive() { return $this->get( 'ftp_active' ); } /** * Get the mailbox permalink * * @return string */ public function getFTPPermalink( $absolute = false ) { return FTP::permalink( $this->getDomainName(), $this->getFTPLogin() ); } protected function normalizeFTP() { $this->normalizeDomain(); $this->booleans( 'ftp_active' ); $this->integers( 'ftp_ulbandwidth', 'ftp_dlbandwidth', 'ftp_quotasize', 'ftp_quotafiles' ); } } /** * An FTP user */ class FTP extends Queried { use FTPTrait; /** * Table name */ const T = 'ftp'; /** * Constructor * * Normalize the object obtained from the database */ public function __construct() { $this->normalizeFTP(); } /** * Get the FTP permalink from domain name and FTP login * * @param string $domain Domain name * @param string $login FTP user login * @return string */ public static function permalink( $domain, $login = null ) { $url = sprintf( '%s/%s', ROOT . '/ftp.php', $domain ); if( $login ) { $url .= "/$login"; } return $url; } + /** + * Encrypt an FTP user password + * + * @param $password string Clear text password + * @return string One-way encrypted password + */ + public static function encryptPassword( $password ) { + global $HOSTING_CONFIG; + + // the FTP password encryption mechanism can be customized + if( isset( $HOSTING_CONFIG->FTP_ENCRYPT_PWD ) ) { + return call_user_func( $HOSTING_CONFIG->FTP_ENCRYPT_PWD, $password ); + } + + // or then just a default behaviour + + /** + * The default behaviour is to adopt the crypt() encryption mechanism + * with SHA512 and some random salt. It's strong enough nowadays. + * + * Read your FTP server documentation, whatever you are using. + * We don't know how your infrastructure works, so we don't know + * how you want your password encrypted in the database and what kind + * of password encryption mechanisms your FTP server supports. + * + * In short if you are using PureFTPd this default configuration may work + * because you may have PureFTPd configured as follow: + * ... + * MYSQLCrypt crypt + * ... + * + * You can read more here: + * https://download.pureftpd.org/pub/pure-ftpd/doc/README.MySQL + * + * Anyway you can use whatever FTP server that talks with a MySQL database + * and so you should adopt the most stronger encryption mechanism available. + */ + + $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); + return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); + } + } diff --git a/include/class-Mailbox.php b/include/class-Mailbox.php index dae1da5..228b919 100644 --- a/include/class-Mailbox.php +++ b/include/class-Mailbox.php @@ -1,136 +1,164 @@ . // load dependent traits class_exists( 'Domain' ); trait MailboxTrait { use DomainTrait; /** * Get the mailbox username * * @return string */ public function getMailboxUsername() { return $this->get( 'mailbox_username' ); } /** * Get the mailbox address * * @return string E-mail */ public function getMailboxAddress() { return sprintf( "%s@%s", $this->get( 'mailbox_username' ), $this->get( 'domain_name' ) ); } /** * Get the mailbox permalink * * @return string */ public function getMailboxPermalink( $absolute = false ) { return Mailbox::permalink( $this->get( 'domain_name' ), $this->get( 'mailbox_username' ) ); } /** * Update this mailbox password * * @param string $password * @return string */ public function updateMailboxPassword( $password = null ) { if( ! $password ) { $password = generate_password(); } $enc_password = Mailbox::encryptPassword( $password ); // update ( new MailboxAPI() ) ->whereMailbox( $this ) ->update( [ new DBCol( 'mailbox_password', $enc_password, 's' ), ] ); return $password; } /** * Normalize a Mailbox after being fetched from database */ protected function normalizeMailbox() { $this->normalizeDomain(); $this->booleans( 'mailbox_receive' ); } } /** * A mailbox */ class Mailbox extends Queried { use MailboxTrait; /** * Database table */ const T = 'mailbox'; /** * Constructor */ public function __construct() { $this->normalizeMailbox(); } /** * Get the mailbox permalink * * @param $domain string * @param $mailbox string * @param $absolute boolean * @return string */ public static function permalink( $domain, $mailbox = null, $absolute = false ) { $part = site_page( 'mailbox.php', $absolute ) . _ . $domain; if( $mailbox ) { $part .= _ . $mailbox; } return $part; } /** * Encrypt a password * - * TODO: do not hardcode to my Dovecot configuration + * @param string $password Clear text password + * @return string One-way encrypted password */ public static function encryptPassword( $password ) { + global $HOSTING_CONFIG; + + // the Mailbox password encryption mechanism can be customized + if( isset( $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD ) ) { + return call_user_func( $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD, $password ); + } + + // or then just a default behaviour + + /** + * The default behaviour is to adopt the crypt() encryption mechanism + * with SHA512 and some random salt. It's strong enough nowadays. + * + * Read your MTA/MDA documentation, whatever you are using. + * We don't know how your infrastructure works, so we don't know + * how you want your password encrypted in the database and what kind + * of password encryption mechanisms your MTA/MDA supports. + * + * In short if you are using Postfix this default configuration may work + * because you may have Postfix configured as follow: + * + * Anyway you can use whatever MTA/MDA that talks with a MySQL database + * and so you should adopt the most stronger encryption mechanism available. + * + * https://doc.dovecot.org/configuration_manual/authentication/password_schemes/ + */ + $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); } } diff --git a/load-example.php b/load-example.php index c828a0d..7fac38f 100644 --- a/load-example.php +++ b/load-example.php @@ -1,52 +1,67 @@ . /* * This is an example configuration file * * Please fill this file and save as 'load.php'! */ // database credentials $username = 'insert here database username'; $password = 'insert here database password'; $database = 'insert here database name'; $location = 'localhost'; // database prefix (if any) $prefix = ''; // your contact e-mail define( 'CONTACT_EMAIL', 'services@example.org' ); // your SMTP credentials define( 'MAIL_FROM', 'noreply@example.org' ); define( 'SMTP_USERNAME', 'noreply@example.org' ); define( 'SMTP_PASSWORD', 'insert here smtp password' ); define( 'SMTP_AUTH', 'PLAIN' ); define( 'SMTP_TLS', true ); define( 'SMTP_SERVER', 'mail.example.org' ); define( 'SMTP_PORT', 465 ); // absolute path to the project directory without trailing slash define( 'ABSPATH', __DIR__ ); // absolute web directory without trailing slash define( 'ROOT', '' ); +// other specific configuration about your hosting environments +$HOSTING_CONFIG = new stdClass(); + +// Mailbox password encryption custom mechanism (you can leave this commented for the default) +# $HOSTING_CONFIG->MAILBOX_ENCRYPT_PWD = function ( $password ) { +# $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); +# return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); +# }; + +// FTP password encryption custom mechanism (you can leave this commented for the default) +# $HOSTING_CONFIG->FTP_ENCRYPT_PWD = function ( $password ) { +# $salt = bin2hex( openssl_random_pseudo_bytes( 3 ) ); +# return '{SHA512-CRYPT}' . crypt( $password, "$6$$salt" ); +# }; + // path to the boz-php framework require '/usr/share/php/suckless-php/load.php'; diff --git a/www/ftp.php b/www/ftp.php index b57aeee..371c57c 100644 --- a/www/ftp.php +++ b/www/ftp.php @@ -1,152 +1,164 @@ . /* * This is the single FTP account creation/edit page */ // load framework require '../load.php'; // wanted informations -$domain = null; -$ftp = null; +$domain = null; +$ftp = null; +$ftp_password = null; // URL paramenters (maximum both domain and FTP login, minimum just domain) list( $domain_name, $ftp_login ) = url_parts( 2, 1 ); // eventually retrieve mailforward from database if( $ftp_login ) { $ftp = ( new FTPAPI() ) ->select( [ 'domain.domain_ID', 'domain_name', 'ftp_login', ] ) ->joinFTPDomain() ->whereDomainName( $domain_name ) ->whereFTPLogin( $ftp_login ) ->whereDomainIsEditable() ->queryRow(); // 404 $ftp or PageNotFound::spawn(); // recycle the mailforward object that has domain informations $domain = $ftp; } // eventually retrieve domain from database if( ! $domain ) { $domain = ( new DomainAPI() ) ->select( [ 'domain.domain_ID', 'domain.domain_name', ] ) ->whereDomainName( $domain_name ) ->whereDomainIsEditable() ->queryRow(); // 404 $domain or PageNotFound::spawn(); } if( ! $ftp ) { // to create an FTP user, must edit all FTP users require_permission( 'edit-ftp-all' ); } // save destination action if( is_action( 'ftp-save' ) ) { // save source only during creation if( ! $ftp ) { - // sanitize - if( ! isset( $_POST[ 'ftp_login' ] ) ) { + // sanitize data + if( !isset( $_POST['ftp_login'] ) || !is_string( $_POST['ftp_login'] ) ) { BadRequest::spawn( __( "missing parameter" ) ); } - $username = luser_input( $_POST[ 'ftp_login' ], 128 ); - if( ! validate_mailbox_username( $username ) ) { + + // generate the username (must start with domain name) + $username = generate_slug( $domain->getDomainName() ) . '_' . $_POST[ 'ftp_login' ]; + $username = luser_input( $username, 128 ); + + // validate username + if( !validate_mailbox_username( $username ) ) { BadRequest::spawn( __( "invalid mailbox name" ) ); } // check existence - $ftp_exists = ( new FTPAPI ) + $ftp_exists = ( new FTPAPI() ) ->select( 1 ) ->whereDomain( $domain ) ->whereFTPLogin( $username ) ->queryRow(); // die if exists if( $ftp_exists ) { BadRequest::spawn( __( "FTP account already existing" ) ); } + // generate a password and die + $ftp_password = generate_password(); + $ftp_password_safe = FTP::encryptPassword( $ftp_password ); + // insert as new row insert_row( 'ftp', [ - new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ), - new DBCol( 'ftp_login', $username, 's' ), + new DBCol( 'domain_ID', $domain->getDomainID(), 'd' ), + new DBCol( 'ftp_login', $username, 's' ), + new DBCol( 'ftp_password', $ftp_password_safe, 's' ), ] ); // POST/redirect/GET http_redirect( FTP::permalink( $domain->getDomainName(), $username, true ), 303 ); } } // delete action if( $ftp ) { // action fired when deleting a whole mailforward if( is_action( 'ftp-delete' ) ) { // delete the account ( new FTPAPI() ) ->whereFTP( $ftp ) ->delete(); // POST/redirect/GET http_redirect( $domain->getDomainPermalink( true ), 303 ); } } // spawn header Header::spawn( [ 'uid' => false, 'title-prefix' => __( "FTP user" ), 'title' => $ftp ? $ftp->getFTPLogin() : __( "create" ), 'breadcrumb' => [ new MenuEntry( null, $domain->getDomainPermalink(), $domain->getDomainName() ), ], ] ); // spawn the page content template( 'ftp', [ - 'domain' => $domain, - 'ftp' => $ftp, + 'domain' => $domain, + 'ftp' => $ftp, + 'password' => $ftp_password, ] ); // spawn the footer Footer::spawn(); diff --git a/www/user.php b/www/user.php index a0c39cd..7156f9f 100644 --- a/www/user.php +++ b/www/user.php @@ -1,172 +1,204 @@ . /* * This is the single User creation/edit page */ // load framework require '../load.php'; // require the permission to see the backend require_permission( 'backend' ); // wanted informations $user = null; // URL paramenters (user_uid) list( $user_uid ) = url_parts( 1, 0 ); // eventually retrieve mailforward from database if( $user_uid ) { $user = ( new UserAPI() ) ->whereUserUID( $user_uid ) ->whereUserIsEditable() ->queryRow(); // 404 if( !$user || !$user->isUserEditable() ) { PageNotFound::spawn(); } } else { // to create an FTP user, must edit all FTP users require_permission( 'edit-user-all' ); } // save destination action -if( is_action( 'user-save' ) ) { - +if( is_action( 'save-user' ) ) { + + $email = $_POST['email'] ?? null; + $uid = $_POST['uid'] ?? null; + $name = $_POST['name'] ?? null; + $surname = $_POST['surname'] ?? null; + + if( $email && $uid && $name && $surname ) { + $email = (string) $email; + + // data to be saved + $data = []; + $data['user_email'] = $email; + $data['user_name'] = $name; + $data['user_surname'] = $surname; + + if( $user ) { + // update existing User + ( new UserAPI() ) + ->whereUser( $user ) + ->update( $data ); + } else { + // insert new User + $data['user_uid'] = $uid; + $data['user_active'] = 1; + $data['user_password'] = '!'; + $data['user_role'] = 'user'; + $data[] = new DBCol( 'user_registration_date', 'NOW()', '-' ); + + ( new UserAPI() ) + ->insertRow( $data ); + } + } } // add a Domain to the user if( is_action( 'add-domain' ) ){ // check for permissions if( !has_permission( 'edit-user-all' ) ) { error_die( "Not authorized to add a Domain" ); } // get the Domain by name $domain_name = $_POST['domain_name'] ?? null; if( !$domain_name ) { die( "Please fill that damn Domain name" ); } // search the Domain name $domain = ( new DomainAPI() ) ->whereDomainName( $domain_name ) ->queryRow(); query( 'START TRANSACTION' ); // domain ID to be assigned to the User $domain_ID = null; // does the Domain already exist? if( $domain ) { $domain_ID = $domain->getDomainID(); } else { // can I add this Domain? if( has_permission( 'edit-domain-all' ) ) { // add this Domain ( new DomainAPI() ) ->insertRow( [ 'domain_name' => $domain_name, 'domain_active' => 1, new DBCol( 'domain_born', 'NOW()', '-' ), ] ); $domain_ID = last_inserted_ID(); } } if( $domain_ID ) { $is_domain_mine = ( new DomainUserAPI() ) ->whereUserIsMe() ->whereDomainID( $domain_ID ) ->queryRow(); // is it already mine? if( !$is_domain_mine ) { // associate this domain to myself ( new DomainUserAPI() ) ->insertRow( [ 'domain_ID' => $domain_ID, 'user_ID' => $user->getUserID(), new DBCol( 'domain_user_creation_date', 'NOW()', '-' ), ] ); } } else { die( "this Domain is not registered and can't be added" ); } query( 'COMMIT' ); // end add Domain to User } // register action to generate a new password $new_password = null; if( is_action( 'change-password' ) && $user ) { // generate a new password and save $new_password = generate_password(); $encrypted = User::encryptPassword( $new_password ); ( new UserAPI() ) ->whereUser( $user ) ->update( [ - new DBCol( User::PASSWORD, $encrypted, 's' ), + User::IS_ACTIVE => 1, + User::PASSWORD => $encrypted, ] ); } // expose the User domains $user_domains = []; if( $user ) { // get User domains $user_domains = ( new DomainUserAPI() ) ->joinDomain() ->whereUser( $user ) ->orderByDomainName() ->queryGenerator(); } // spawn header Header::spawn( [ 'uid' => false, 'title-prefix' => __( "User" ), 'title' => $user ? $user->getUserUID() : __( "create" ), ] ); // spawn the page content template( 'user', [ 'user' => $user, 'new_password' => $new_password, 'user_domains' => $user_domains, ] ); // spawn the footer Footer::spawn();