1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    /**
     * Provides common functionality associated with vsFTPd
     *
     * @package core
     */
    class Ftp_Module extends Module_Skeleton implements \Opcenter\Contracts\Hookable
    {
        const DEPENDENCY_MAP = [
            'siteinfo',
            'users'
        ];

        /**
         * {{{ void __construct(void)
         *
         * @ignore
         */
        const VSFTPD_CONF_DIR = '/etc/vsftpd';
        const VSFTPD_CHROOT_FILE = '/etc/vsftpd.chroot_list';
        const PAM_SVC_NAME = 'ftp';

        public function __construct()
        {
            parent::__construct();
            $this->exportedFunctions = array(
                '*' => PRIVILEGE_SITE
            );
        }

        public function jail_user($user, $dir = '')
        {
            if (!IS_CLI) {
                return $this->query('ftp_jail_user', $user, $dir);
            }
            if (!$this->user_exists($user)) {
                return error('user ' . $user . ' does not exist');
            }

            $chroot_file = $this->domain_fs_path() . self::VSFTPD_CHROOT_FILE;
            $chroot_users = array();
            if (file_exists($chroot_file)) {
                $chroot_users = preg_split("/[\r\n]+/", trim(file_get_contents($chroot_file)));
            }

            if (in_array($user, $chroot_users)) {
                if (!$dir) {
                    return warn('user ' . $user . ' already jailed');
                }
            } else {
                $chroot_users[] = $user;
            }

            file_put_contents($this->domain_fs_path(self::VSFTPD_CHROOT_FILE),
                join("\n", $chroot_users) . "\n");
            if ($dir) {
                if (!$this->file_exists($dir)) {
                    $this->file_create_directory($dir, 0755, true);
                } else {
                    $stat = $this->file_stat($dir);
                    if ($stat['link']) {
                        info("target is symlink, converted jailed path `%s' to `%s'",
                            $dir,
                            $stat['referent']
                        );
                        $dir = $stat['referent'];
                    }
                }
                $this->file_chown($dir, $user) && $this->set_option($user, 'local_root', $dir);
            }

            return true;
        }

        public function set_option($user, $c_directive, $c_val = null)
        {
            if (!IS_CLI) {
                return $this->query('ftp_set_option', $user, $c_directive, $c_val);
            }

            if (!$this->user_exists($user)) {
                return error('user ' . $user . ' does not exist');
            }

            return $this->_set_option_real($user, $c_directive, $c_val);
        }

        protected function _set_option_real($user, $c_directive, $c_val = null)
        {
            $user_conf = self::VSFTPD_CONF_DIR . '/' . $user;

            if (!file_exists($this->domain_fs_path() . $user_conf) &&
                ($status = file_put_contents($this->domain_fs_path(self::VSFTPD_CONF_DIR . '/' . $user), '') === false)
            ) {
                return $status;
            }

            $fp = fopen($this->domain_fs_path() . $user_conf, 'r');
            if (!$fp) {
                return error(self::VSFTPD_CONF_DIR . '/' . $user . ': cannot access file');
            }
            $new = true;
            for ($buffer = array(); !feof($fp);) {
                $line = trim((string)fgets($fp));
                if (!$line) {
                    continue;
                }

                if (false !== strpos($line, '=')) {
                    list($lval, $rval) = explode('=', $line, 2);
                } else {
                    $rval = '';
                    $lval = $line;
                }

                if ($lval == $c_directive) {
                    $new = false; // value already set
                    if (!$c_val) {
                        continue;
                    }
                    $rval = $c_val;
                }
                $buffer[] = $lval . ($rval ? '=' . $rval : '');
            }
            if ($new) {
                $buffer[] = $c_directive . ($c_val ? '=' . $c_val : '');
            }
            $path = $this->domain_fs_path() . $user_conf;
            file_put_contents($path, join("\n", $buffer) . "\n");
            // make sure configuration is owned by root on v6+ platforms
            // no more custom patches
            chown($path, 'root');

            return true;
        }

        public function deny_user($user)
        {
            return (new Util_Pam($this->getAuthContext()))->remove($user, $this->getPamServiceName());
        }

        /**
         * Wrapper for backwards compatibility during dev
         *
         * @todo yank before 3.0.0 release
         *
         * @return string
         */
        protected function getPamServiceName(): string
        {
            //@xxx temporary backwards compatibility
            if (version_compare(platform_version(), '7.5', '<')) {
                return 'proftpd';
            }

            return static::PAM_SVC_NAME;
        }

        public function permit_user($user)
        {
            if ($this->auth_is_demo()) {
                return error('FTP disabled for demo account');
            }

            return (new Util_Pam($this->getAuthContext()))->add($user, $this->getPamServiceName());
        }

        public function _edit_user(string $user, string $usernew, array $pwd)
        {
            if ($user === $usernew) {
                return;
            }
            if (!$this->user_enabled($user)) {
                return true;
            }
            (new Util_Pam($this->getAuthContext()))->remove($user, $this->getPamServiceName());
            (new Util_Pam($this->getAuthContext()))->add($usernew, $this->getPamServiceName());

            $home = $pwd['home'];
            if ($this->_user_jailed_real($user)) {
                $jailhome = null;
                if ($this->has_configuration($user)) {
                    $jailhome = $this->get_option($user, 'local_root');
                    if (!strncmp($jailhome, $home, strlen($home))) {
                        $newhome = preg_replace('!' . DIRECTORY_SEPARATOR . $user . '!',
                            DIRECTORY_SEPARATOR . $usernew, $jailhome, 1);
                        $this->set_option($user, 'local_root', $newhome);
                        $jailhome = $newhome;
                    }
                }
                $jailconf = $this->domain_fs_path() . '/' . self::VSFTPD_CHROOT_FILE;
                $conf = file_get_contents($jailconf);
                $conf = preg_replace('/^' . $user . '$/m', $usernew, $conf);
                file_put_contents($jailconf, $conf);
            }
            if ($this->has_configuration($user)) {
                $ftpconfdir = $this->domain_fs_path() . self::VSFTPD_CONF_DIR;
                if (file_exists($ftpconfdir . '/' . $user)) {
                    rename($ftpconfdir . '/' . $user, $ftpconfdir . '/' . $usernew);
                }
            }

            return true;
        }

        public function user_enabled($user)
        {
            if (!$this->getConfig('ftp', 'enabled')) {
                return false;
            }

            return (new Util_Pam($this->getAuthContext()))->check($user, $this->getPamServiceName());
        }

        protected function _user_jailed_real($user)
        {
            $chroot_file = $this->domain_fs_path() . self::VSFTPD_CHROOT_FILE;
            if (!file_exists($chroot_file)) {
                return false;
            }

            return (bool)preg_match('/\b' . $user . '\b/', file_get_contents($chroot_file));
        }

        public function has_configuration($user)
        {
            $path = $this->domain_fs_path() . self::VSFTPD_CONF_DIR . '/' . $user;

            return file_exists($path);
        }

        public function get_option($user, $c_directive)
        {
            if (!IS_CLI) {
                return $this->query('ftp_get_option', $user, $c_directive);
            }

            if (!$this->user_exists($user)) {
                return error('user ' . $user . ' does not exist');
            }

            return $this->_get_option_real($user, $c_directive);
        }

        protected function _get_option_real($user, $c_directive)
        {
            $conf_file = $this->domain_fs_path() . self::VSFTPD_CONF_DIR . '/' . $user;
            if (!file_exists($conf_file)) {
                warn('no configuration set for user ' . $user);

                return null;
            }
            $user_conf = file_get_contents($conf_file);
            $conf_val = null;

            if (!preg_match('/^\b' . preg_quote($c_directive) . '(?:\s*=\s*)(.+)$/m', $user_conf, $conf_val)) {
                return null;
            }

            $conf_val = $conf_val[1];

            return $conf_val;
        }

        public function _reload($what = null)
        {
            // ssl = system ssl
            if ($what === Ssl_Module::SYS_RHOOK) {
                \Opcenter\Ftp\Vsftpd::restart(HTTPD_RELOAD_DELAY);
            }

            return true;
        }

        public function _delete_user(string $user)
        {
            if ($this->user_jailed($user)) {
                $this->unjail_user($user);
            }
            $ftp_conf = join(DIRECTORY_SEPARATOR,
                array(
                    $this->domain_fs_path(),
                    self::VSFTPD_CONF_DIR,
                    $user
                )
            );
            if (file_exists($ftp_conf)) {
                unlink($ftp_conf);
            }

            return true;
        }

        public function user_jailed($user)
        {
            if (!$this->user_exists($user)) {
                return error('user ' . $user . ' does not exist');
            }

            return $this->_user_jailed_real($user);
        }

        public function unjail_user($user)
        {
            if (!IS_CLI) {
                return $this->query('ftp_unjail_user', $user);
            }
            if (!$this->user_exists($user)) {
                return error('user ' . $user . ' does not exist');
            }
            if (!file_exists($this->domain_fs_path() . self::VSFTPD_CHROOT_FILE)) {
                return warn('chroot file ' . self::VSFTPD_CHROOT_FILE . ' not found');
            }

            $fp = fopen($this->domain_fs_path() . self::VSFTPD_CHROOT_FILE, 'r');
            $buffer = [];
            for ($seen = false; !feof($fp);) {
                $line = trim((string)fgets($fp));
                if (!$line) {
                    continue;
                } else if ($user === $line) {
                    $seen = true;
                    continue;
                }
                $buffer[] = $line;
            }
            fclose($fp);

            if (!$seen) {
                warn("user `%s' not found in jail conf", $user);
            }
            $prefix = $this->domain_fs_path();
            $path = $prefix . self::VSFTPD_CHROOT_FILE;
            $size = file_put_contents($path, join("\n", $buffer) . "\n", LOCK_EX);

            return $size !== false;
        }

        public function _create()
        {
            // stupid thor...
            $conf = $this->getAuthContext()->getAccount()->new;
            $admin = $conf['siteinfo']['admin_user'];
            $pam = new Util_Pam($this->getAuthContext());
            if ($this->auth_is_demo() && $pam->check($admin, $this->getPamServiceName())) {
                $pam->remove($admin, $this->getPamServiceName());
            }
        }

        public function enabled() {

            return (bool)$this->getConfig(\Opcenter\SiteConfiguration::getModuleRemap('proftpd'), 'enabled', true);
        }

        public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
        {
            return true;
        }

        public function _delete()
        {

        }

        public function _edit()
        {
        }

        public function _create_user(string $user)
        {
        }

    }